<commit_before>const {SendmailClient, Provider, Errors: {APIError}} = require('isomorphic-core')
const SyncbackTask = require('./syncback-task')
const {getReplyHeaders} = require('../../shared/message-factory')

async function deleteGmailSentMessages({db, imap, provider, headerMessageId}) {
  if (provider !== 'gmail') { return }

  const trash = await db.Folder.find({where: {role: 'trash'}});
  if (!trash) { throw new APIError(`Could not find folder with role 'trash'.`) }

  const allMail = await db.Folder.find({where: {role: 'all'}});
  if (!allMail) { throw new APIError(`Could not find folder with role 'all'.`) }

  // Move the message from all mail to trash and then delete it from there
  const steps = [
    {folder: allMail, deleteFn: (box, uid) => box.moveFromBox(uid, trash.name)},
    {folder: trash, deleteFn: (box, uid) => box.addFlags(uid, 'DELETED')},
  ]

  for (const {folder, deleteFn} of steps) {
    const box = await imap.openBox(folder.name);
    const uids = await box.search([['HEADER', 'Message-ID', headerMessageId]])
    for (const uid of uids) {
      await deleteFn(box, uid);
    }
    await box.closeBox();
  }
}

async function saveSentMessage({db, account, logger, imap, provider, sentPerRecipient, baseMessage}) {
  const {sequelize, Folder, Label} = db
  const thread = await baseMessage.getThread({
    include: [{model: Folder, as: 'folders'}, {model: Label, as: 'labels'}],
  })

  // Case 1. If non gmail, save the message to the `sent` folder using IMAP
  // Only gmail creates a sent message for us, so if we are using any other provider
  // we need to save it manually ourselves.
  if (provider !== 'gmail') {
    const sentFolder = await Folder.find({where: {role: 'sent'}});
    if (!sentFolder) { throw new APIError(`Can't find sent folder - could not save message to sent folder.`) }

    const sender = new SendmailClient(account, logger);
    const rawMime = await sender.buildMime(baseMessage);
    const box = await imap.openBox(sentFolder.name);
    await box.append(rawMime, {flags: 'SEEN'});

    // Finally, if IMAP succeeds, save the model updates
    await sequelize.transaction(async (transaction) => {
      await baseMessage.setFolder(sentFolder, {transaction})
      await baseMessage.save({transaction})
      baseMessage.folder = sentFolder
      if (thread) {
        await thread.updateFromMessages({messages: [baseMessage], transaction})
      }
    })
    return
  }

  // Showing as sent in gmail means adding the message to all mail and
  // adding the sent label
  const sentLabel = await Label.find({where: {role: 'sent'}});
  const allMailFolder = await Folder.find({where: {role: 'all'}});
  if (!sentLabel || !allMailFolder) {
    throw new APIError('Could not save message to sent folder.')
  }

  // Case 2. If gmail, even though gmail saves sent messages automatically,
  // if `sentPerRecipient` is true, it means we want to save the `baseMessage`
  // as sent. This is because that means that we sent a message per recipient for
  // tracking, but we actually /just/ want to show the baseMessage as sent
  if (sentPerRecipient) {
    const sender = new SendmailClient(account, logger);
    const rawMime = await sender.buildMime(baseMessage);
    const box = await imap.openBox(allMailFolder.name);

    await box.append(rawMime, {flags: 'SEEN'})

    const {headerMessageId} = baseMessage
    const uids = await box.search([['HEADER', 'Message-ID', headerMessageId]])
    // There should only be one uid in the array
    await box.setLabels(uids[0], '\\Sent');
  }

  // Finally, if IMAP succeeds, save the model updates
  await sequelize.transaction(async (transaction) => {
    await baseMessage.setFolder(allMailFolder, {transaction})
    await baseMessage.setLabels([sentLabel], {transaction})
    await baseMessage.save({transaction})
    baseMessage.folder = allMailFolder
    baseMessage.labels = [sentLabel]
    if (thread) {
      await thread.updateFromMessages({messages: [baseMessage], transaction})
    }
  })
}

async function setThreadingReferences(db, baseMessage) {
  const {Message, Reference} = db
  // TODO When the message was created for sending, we set the
  // `inReplyToLocalMessageId` if it exists, and we set the temporary properties
  // `inReplyTo` and `references` for sending.
  // Since these properties aren't saved to the model, we need to recreate
  // them again because they are necessary for building the correct raw mime
  // message to add to the sent folder
  // We should clean this up
  const replyToMessage = await Message.findById(
    baseMessage.inReplyToLocalMessageId,
    { include: [{model: Reference, as: 'references', attributes: ['id', 'rfc2822MessageId']}] }
  )
  if (replyToMessage) {
    const {inReplyTo, references} = getReplyHeaders(replyToMessage);
    baseMessage.inReplyTo = inReplyTo;
    baseMessage.references = references;
  }
  // TODO If the thread exists, let's also optimistically create any Reference
  // objects (these will also be detected when the message gets detected in
  // the sync loop)
  // We should clean this code, its duplicated from the message processor
  const thread = await baseMessage.getThread()
  if (thread) {
    const references = Array.from(new Set([
      ...baseMessage.references || [],
      baseMessage.headerMessageId,
      baseMessage.inReplyTo,
    ]))

    let existingReferences = [];
    if (references.length > 0) {
      existingReferences = await Reference.findAll({
        where: {
          rfc2822MessageId: baseMessage.references,
        },
      });
    }

    const refByMessageId = {};
    for (const ref of existingReferences) {
      refByMessageId[ref.rfc2822MessageId] = ref;
    }
    for (const mid of baseMessage.references) {
      if (!refByMessageId[mid]) {
        refByMessageId[mid] = await Reference.create({rfc2822MessageId: mid, threadId: thread.id});
      }
    }

    const referencesInstances = baseMessage.references.map(mid => refByMessageId[mid]);
    await baseMessage.addReferences(referencesInstances);
    baseMessage.referencesOrder = referencesInstances.map(ref => ref.id);
    await thread.addReferences(referencesInstances);
  }
}

/**
 * Ensures that sent messages show up in the sent folder.
 *
 * Gmail does this automatically. IMAP needs to do this manually.
 *
 * If we've `sentPerRecipient` that means we've actually sent many
 * messages (on per recipient). Gmail will have automatically created tons
 * of messages in the sent folder. We need to make it look like you only
 * sent 1 message. To do this we, delete all of the messages Gmail
 * automatically created (keyed by the same Meassage-Id header we set),
 * then stuff a copy of the original message in the sent folder.
 */
class EnsureMessageInSentFolderIMAP extends SyncbackTask {
  description() {
    return `EnsureMessageInSentFolder`;
  }

  affectsImapMessageUIDs() {
    return false
  }

  async run(db, imap) {
    const {Message} = db
    const {messageId, sentPerRecipient} = this.syncbackRequestObject().props
    const {account, logger} = imap
    if (!account) {
      throw new APIError('EnsureMessageInSentFolder: Failed, account not available on imap connection')
    }

    const baseMessage = await Message.findById(messageId, {
      include: [{model: db.Folder}, {model: db.Label}, {model: db.File}],
    });

    if (!baseMessage) {
      throw new APIError(`Couldn't find message ${messageId} to stuff in sent folder`, 500)
    }

    await setThreadingReferences(db, baseMessage)

    const {provider} = account
    const {headerMessageId} = baseMessage

    // Gmail automatically creates sent messages when sending, so we
    // delete each of the ones we sent to each recipient in the
    // `SendMessagePerRecipient` task
    //
    // Each participant gets a message, but all of those messages have the
    // same Message-ID header in them. This allows us to find all of the
    // sent messages and clean them up
    if (sentPerRecipient && provider === Provider.Gmail) {
      try {
        await deleteGmailSentMessages({db, imap, provider, headerMessageId})
      } catch (err) {
        // Even if this fails, we need to finish attempting to save the
        // baseMessage to the sent folder
        logger.error(err, 'EnsureMessageInSentFolder: Failed to delete Gmail sent messages');
      }
    }

    await saveSentMessage({db, account, logger, imap, provider, sentPerRecipient, baseMessage})
    return baseMessage.toJSON()
  }
}

module.exports = EnsureMessageInSentFolderIMAP;
<commit_msg>Add comment<commit_after>const {SendmailClient, Provider, Errors: {APIError}} = require('isomorphic-core')
const SyncbackTask = require('./syncback-task')
const {getReplyHeaders} = require('../../shared/message-factory')

async function deleteGmailSentMessages({db, imap, provider, headerMessageId}) {
  if (provider !== 'gmail') { return }

  const trash = await db.Folder.find({where: {role: 'trash'}});
  if (!trash) { throw new APIError(`Could not find folder with role 'trash'.`) }

  const allMail = await db.Folder.find({where: {role: 'all'}});
  if (!allMail) { throw new APIError(`Could not find folder with role 'all'.`) }

  // Move the message from all mail to trash and then delete it from there
  const steps = [
    {folder: allMail, deleteFn: (box, uid) => box.moveFromBox(uid, trash.name)},
    {folder: trash, deleteFn: (box, uid) => box.addFlags(uid, 'DELETED')},
  ]

  for (const {folder, deleteFn} of steps) {
    const box = await imap.openBox(folder.name);
    const uids = await box.search([['HEADER', 'Message-ID', headerMessageId]])
    for (const uid of uids) {
      await deleteFn(box, uid);
    }
    await box.closeBox();
  }
}

async function saveSentMessage({db, account, logger, imap, provider, sentPerRecipient, baseMessage}) {
  const {sequelize, Folder, Label} = db
  const thread = await baseMessage.getThread({
    include: [{model: Folder, as: 'folders'}, {model: Label, as: 'labels'}],
  })

  // Case 1. If non gmail, save the message to the `sent` folder using IMAP
  // Only gmail creates a sent message for us, so if we are using any other provider
  // we need to save it manually ourselves.
  if (provider !== 'gmail') {
    const sentFolder = await Folder.find({where: {role: 'sent'}});
    if (!sentFolder) { throw new APIError(`Can't find sent folder - could not save message to sent folder.`) }

    const sender = new SendmailClient(account, logger);
    const rawMime = await sender.buildMime(baseMessage);
    const box = await imap.openBox(sentFolder.name);
    await box.append(rawMime, {flags: 'SEEN'});

    // Finally, if IMAP succeeds, save the model updates
    await sequelize.transaction(async (transaction) => {
      await baseMessage.setFolder(sentFolder, {transaction})
      await baseMessage.save({transaction})
      baseMessage.folder = sentFolder
      if (thread) {
        await thread.updateFromMessages({messages: [baseMessage], transaction})
      }
    })
    return
  }

  // Showing as sent in gmail means adding the message to all mail and
  // adding the sent label
  const sentLabel = await Label.find({where: {role: 'sent'}});
  const allMailFolder = await Folder.find({where: {role: 'all'}});
  if (!sentLabel || !allMailFolder) {
    throw new APIError('Could not save message to sent folder.')
  }

  // Case 2. If gmail, even though gmail saves sent messages automatically,
  // if `sentPerRecipient` is true, it means we want to save the `baseMessage`
  // as sent. This is because that means that we sent a message per recipient for
  // tracking, but we actually /just/ want to show the baseMessage as sent
  if (sentPerRecipient) {
    const sender = new SendmailClient(account, logger);
    const rawMime = await sender.buildMime(baseMessage);
    const box = await imap.openBox(allMailFolder.name);

    await box.append(rawMime, {flags: 'SEEN'})

    const {headerMessageId} = baseMessage
    const uids = await box.search([['HEADER', 'Message-ID', headerMessageId]])
    // There should only be one uid in the array
    await box.setLabels(uids[0], '\\Sent');
  }

  // Finally, if IMAP succeeds, save the model updates
  await sequelize.transaction(async (transaction) => {
    await baseMessage.setFolder(allMailFolder, {transaction})
    await baseMessage.setLabels([sentLabel], {transaction})
    await baseMessage.save({transaction})
    baseMessage.folder = allMailFolder
    baseMessage.labels = [sentLabel]
    if (thread) {
      await thread.updateFromMessages({messages: [baseMessage], transaction})
    }
  })
}

async function setThreadingReferences(db, baseMessage) {
  const {Message, Reference} = db
  // TODO When the message was created for sending, we set the
  // `inReplyToLocalMessageId` if it exists, and we set the temporary properties
  // `inReplyTo` and `references` for sending.
  // Since these properties aren't saved to the model, we need to recreate
  // them again because they are necessary for building the correct raw mime
  // message to add to the sent folder
  // We should clean this up
  const replyToMessage = await Message.findById(
    baseMessage.inReplyToLocalMessageId,
    { include: [{model: Reference, as: 'references', attributes: ['id', 'rfc2822MessageId']}] }
  )
  if (replyToMessage) {
    const {inReplyTo, references} = getReplyHeaders(replyToMessage);
    baseMessage.inReplyTo = inReplyTo;
    baseMessage.references = references;
  }
  // TODO If the thread exists, let's also optimistically create any Reference
  // objects (these will also be detected when the message gets detected in
  // the sync loop)
  // We should clean this code, its duplicated from the message processor
  const thread = await baseMessage.getThread()
  // TODO create thread for this message if it doesn't exist!
  if (thread) {
    const references = Array.from(new Set([
      ...baseMessage.references || [],
      baseMessage.headerMessageId,
      baseMessage.inReplyTo,
    ]))

    let existingReferences = [];
    if (references.length > 0) {
      existingReferences = await Reference.findAll({
        where: {
          rfc2822MessageId: baseMessage.references,
        },
      });
    }

    const refByMessageId = {};
    for (const ref of existingReferences) {
      refByMessageId[ref.rfc2822MessageId] = ref;
    }
    for (const mid of baseMessage.references) {
      if (!refByMessageId[mid]) {
        refByMessageId[mid] = await Reference.create({rfc2822MessageId: mid, threadId: thread.id});
      }
    }

    const referencesInstances = baseMessage.references.map(mid => refByMessageId[mid]);
    await baseMessage.addReferences(referencesInstances);
    baseMessage.referencesOrder = referencesInstances.map(ref => ref.id);
    await thread.addReferences(referencesInstances);
  }
}

/**
 * Ensures that sent messages show up in the sent folder.
 *
 * Gmail does this automatically. IMAP needs to do this manually.
 *
 * If we've `sentPerRecipient` that means we've actually sent many
 * messages (on per recipient). Gmail will have automatically created tons
 * of messages in the sent folder. We need to make it look like you only
 * sent 1 message. To do this we, delete all of the messages Gmail
 * automatically created (keyed by the same Meassage-Id header we set),
 * then stuff a copy of the original message in the sent folder.
 */
class EnsureMessageInSentFolderIMAP extends SyncbackTask {
  description() {
    return `EnsureMessageInSentFolder`;
  }

  affectsImapMessageUIDs() {
    return false
  }

  async run(db, imap) {
    const {Message} = db
    const {messageId, sentPerRecipient} = this.syncbackRequestObject().props
    const {account, logger} = imap
    if (!account) {
      throw new APIError('EnsureMessageInSentFolder: Failed, account not available on imap connection')
    }

    const baseMessage = await Message.findById(messageId, {
      include: [{model: db.Folder}, {model: db.Label}, {model: db.File}],
    });

    if (!baseMessage) {
      throw new APIError(`Couldn't find message ${messageId} to stuff in sent folder`, 500)
    }

    await setThreadingReferences(db, baseMessage)

    const {provider} = account
    const {headerMessageId} = baseMessage

    // Gmail automatically creates sent messages when sending, so we
    // delete each of the ones we sent to each recipient in the
    // `SendMessagePerRecipient` task
    //
    // Each participant gets a message, but all of those messages have the
    // same Message-ID header in them. This allows us to find all of the
    // sent messages and clean them up
    if (sentPerRecipient && provider === Provider.Gmail) {
      try {
        await deleteGmailSentMessages({db, imap, provider, headerMessageId})
      } catch (err) {
        // Even if this fails, we need to finish attempting to save the
        // baseMessage to the sent folder
        logger.error(err, 'EnsureMessageInSentFolder: Failed to delete Gmail sent messages');
      }
    }

    await saveSentMessage({db, account, logger, imap, provider, sentPerRecipient, baseMessage})
    return baseMessage.toJSON()
  }
}

module.exports = EnsureMessageInSentFolderIMAP;
<|endoftext|>


<commit_before>/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId])
/******/ 			return installedModules[moduleId].exports;
/******/
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			exports: {},
/******/ 			id: moduleId,
/******/ 			loaded: false
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.loaded = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

	var react_accordion = __webpack_require__(1);
	__webpack_require__(2);

	var Accordion = react_accordion.Accordion;
	var Section = react_accordion.Section;
	var Heading = react_accordion.Heading;
	var Content = react_accordion.Content;

	React.render(
	  React.createElement(Accordion, null,
	    React.createElement(Section, null,
	      React.createElement(Heading, null, "Accordion Section 1"),
	      React.createElement(Content, null,
	        React.createElement("p", null, "Some text Content in a paragraph"),
	        React.createElement("button", null, "Button 1")
	      )
	    ),
	    React.createElement(Section, null,
	      React.createElement(Heading, null, "Accordion Section 2 ", React.createElement("button", null, "Activate Foo"), React.createElement("button", null, "Activate Bar")),
	      React.createElement(Content, null,
	        "Just some text content for Accordion Section 2. Nothing to see here."
	      )
	    ),
	    React.createElement(Section, null,
	      React.createElement(Heading, null, "Accordion Section 3"),
	      React.createElement(Content, null,
	        React.createElement("div", null, "Some text Content in a div"),
	        React.createElement("div", null, "Some more text Content in a div"),
	        React.createElement("div", null, "Even more text Content in a div")
	      )
	    )
	  ),
	  document.getElementById('content')
	);

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {

	function isElementType(element, expectedType) {
	  return getElementType(element) == expectedType;
	}

	function getElementType(element) {
	  return element.type.displayName;
	}

	var ONE_OR_NONE = 0;
	var ALWAYS_ONE = 1;
	var MULTIPLE = 2;

	var Accordion = React.createClass({displayName: "Accordion",
	  propTypes: {
	    expandMode: React.PropTypes.number,
	    expandedSection: React.PropTypes.number
	  },
	  getDefaultProps: function() {
	    return {
	      expandMode: ALWAYS_ONE,
	      expandedSection: 0
	    };
	  },
	  getInitialState: function() {
	    expanded = {};
	    expanded[this.props.expandedSection] = true;
	    return { expanded: expanded };
	  },
	  expandSections: function(expandedSectionId, isExpanded) {
	    var expanded = this.state.expanded;
	    var expandMode = this.props.expandMode;

	    if(expandMode === ALWAYS_ONE || expandMode === ONE_OR_NONE) {
	      expanded = {};
	    }

	    expanded[expandedSectionId] = isExpanded;

	    this.setState({ expanded: expanded });
	  },
	  componentWillMount: function() {
	    if(this.props.children === null || this.props.children.length === 0) {
	      throw new Error("No elements found in Accordion");
	    }

	    var errors = "";

	    var children = this.props.children;

	    if(children.constructor !== Array) {
	      children = [children];
	    }

	    children.forEach(function(child) {
	      if(!isElementType(child, "Section")) {
	        errors += "Found " + getElementType(child) + " element in Accordion. All elements should be 'Section'\r\n";
	      }
	    });

	    if(errors !== "") {
	      throw new Error(errors);
	    }
	  },
	  render: function() {
	    var children = this.props.children;

	    if(children.constructor !== Array) {
	      children = [children];
	    }

	    var sections = children.map(function (section, id) {
	      return (
	        React.createElement(Section, {clickHandler: this.expandSections, id: id, expanded: this.state.expanded[id], expandMode: this.props.expandMode},
	        	section.props.children
	        )
	      );
	    }, this);

	    return (
	      React.createElement("div", {className: "accordion"},
	      	sections
	      )
	    );
	  }
	});

	var Section = React.createClass({displayName: "Section",
	  componentWillMount: function() {
	    var errors = "";

	    var id = this.props.id;

	    if(this.props.children == null || this.props.children.constructor !== Array) {
	      throw new Error("Too few elements in Section " + id + ". Expected 2: 'Heading' followed by 'Content'");
	    }
	    else if(this.props.children.length > 2) {
	      throw new Error("Too many elements (" + this.props.children.length + ") in Section " + id + ". Expected 2: 'Heading' followed by 'Content'");
	    }

	    var expectedHeading = this.props.children[0];
	    var expectedContent = this.props.children[1];

	    if(!isElementType(expectedHeading, "Heading")) {
	      errors += "First element in Section " + id + " was type " + getElementType(expectedHeading) + ", expected 'Heading'\r\n";
	    }

	    if(!isElementType(expectedContent, "Content")) {
	      errors += "Second element in Section " + id + " was type " + getElementType(expectedContent) + ", expected 'Content'\r\n";
	    }

	    if(errors != "") {
	      throw new Error(errors);
	    }
	  },
	  render: function() {
	  	var heading = this.props.children[0];
	    var content = this.props.children[1];

	    return (
	      React.createElement("div", {className: "accordion-section"},
	        React.createElement(Heading, {clickHandler: this.props.clickHandler, id: this.props.id, expanded: this.props.expanded, expandMode: this.props.expandMode}, heading.props.children),
	         this.props.expanded ? content : null
	      )
	    )

	}
	});

	var Heading = React.createClass({displayName: "Heading",
	  headingClicked: function () {
	    var expanded = this.props.expanded;
	    var id = this.props.id;
	    var expandMode = this.props.expandMode;

	    if(!expanded) {
	      this.props.clickHandler(id, true);
	      return;
	    }

	    if(expandMode == ALWAYS_ONE) {
	      return;
	    }

	    this.props.clickHandler(id, false);
	  },
	  render: function() {
	    return (
	      React.createElement("div", {className: "accordion-heading", onClick: this.headingClicked}, this.props.children)
	    )
	  }
	});

	var Content = React.createClass({displayName: "Content",
	  render: function() {
	    return (
	      React.createElement("div", {className: "accordion-content"}, this.props.children)
	    )
	  }
	});

	module.exports.Accordion = Accordion;
	module.exports.Section = Section;
	module.exports.Heading = Heading;
	module.exports.Content = Content;

	module.exports.ONE_OR_NONE = ONE_OR_NONE;
	module.exports.ALWAYS_ONE = ALWAYS_ONE;
	module.exports.MULTIPLE = MULTIPLE;

/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {

	// style-loader: Adds some css to the DOM by adding a <style> tag

	// load the styles
	var content = __webpack_require__(3);
	if(typeof content === 'string') content = [[module.id, content, '']];
	// add the styles to the DOM
	var update = __webpack_require__(4)(content, {});
	// Hot Module Replacement
	if(false) {
		// When the styles change, update the <style> tags
		module.hot.accept("!!C:\\Users\\sns12_000\\Documents\\GitHub\\react-accordion\\node_modules\\css-loader\\index.js!C:\\Users\\sns12_000\\Documents\\GitHub\\react-accordion\\accordion-styles.css", function() {
			var newContent = require("!!C:\\Users\\sns12_000\\Documents\\GitHub\\react-accordion\\node_modules\\css-loader\\index.js!C:\\Users\\sns12_000\\Documents\\GitHub\\react-accordion\\accordion-styles.css");
			if(typeof newContent === 'string') newContent = [module.id, newContent, ''];
			update(newContent);
		});
		// When the module is disposed, remove the <style> tags
		module.hot.dispose(function() { update(); });
	}

/***/ },
/* 3 */
/***/ function(module, exports, __webpack_require__) {

	exports = module.exports = __webpack_require__(5)();
	exports.push([module.id, ".accordion {\r\n\tborder: 1px black dashed;\r\n\tborder-radius: 5px;\r\n\tpadding: 5px;\r\n\twidth: 600px;\r\n}\r\n\r\n.accordion-heading {\r\n\tborder: 1px white solid;\r\n\tbackground-color: black;\r\n\tcolor: white;\r\n\tborder-radius: 10px;\r\n\tpadding: 1px 5px;\r\n}\r\n\r\n.accordion-content {\r\n\tborder: 1px black solid;\r\n\tborder-radius: 10px;\r\n\tpadding: 10px;\r\n}", ""]);

/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {

	/*
		MIT License http://www.opensource.org/licenses/mit-license.php
		Author Tobias Koppers @sokra
	*/
	var stylesInDom = {},
		memoize = function(fn) {
			var memo;
			return function () {
				if (typeof memo === "undefined") memo = fn.apply(this, arguments);
				return memo;
			};
		},
		isIE9 = memoize(function() {
			return /msie 9\b/.test(window.navigator.userAgent.toLowerCase());
		}),
		getHeadElement = memoize(function () {
			return document.head || document.getElementsByTagName("head")[0];
		}),
		singletonElement = null,
		singletonCounter = 0;

	module.exports = function(list, options) {
		if(false) {
			if(typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment");
		}

		options = options || {};
		// Force single-tag solution on IE9, which has a hard limit on the # of <style>
		// tags it will allow on a page
		if (typeof options.singleton === "undefined") options.singleton = isIE9();

		var styles = listToStyles(list);
		addStylesToDom(styles, options);

		return function update(newList) {
			var mayRemove = [];
			for(var i = 0; i < styles.length; i++) {
				var item = styles[i];
				var domStyle = stylesInDom[item.id];
				domStyle.refs--;
				mayRemove.push(domStyle);
			}
			if(newList) {
				var newStyles = listToStyles(newList);
				addStylesToDom(newStyles, options);
			}
			for(var i = 0; i < mayRemove.length; i++) {
				var domStyle = mayRemove[i];
				if(domStyle.refs === 0) {
					for(var j = 0; j < domStyle.parts.length; j++)
						domStyle.parts[j]();
					delete stylesInDom[domStyle.id];
				}
			}
		};
	}

	function addStylesToDom(styles, options) {
		for(var i = 0; i < styles.length; i++) {
			var item = styles[i];
			var domStyle = stylesInDom[item.id];
			if(domStyle) {
				domStyle.refs++;
				for(var j = 0; j < domStyle.parts.length; j++) {
					domStyle.parts[j](item.parts[j]);
				}
				for(; j < item.parts.length; j++) {
					domStyle.parts.push(addStyle(item.parts[j], options));
				}
			} else {
				var parts = [];
				for(var j = 0; j < item.parts.length; j++) {
					parts.push(addStyle(item.parts[j], options));
				}
				stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts};
			}
		}
	}

	function listToStyles(list) {
		var styles = [];
		var newStyles = {};
		for(var i = 0; i < list.length; i++) {
			var item = list[i];
			var id = item[0];
			var css = item[1];
			var media = item[2];
			var sourceMap = item[3];
			var part = {css: css, media: media, sourceMap: sourceMap};
			if(!newStyles[id])
				styles.push(newStyles[id] = {id: id, parts: [part]});
			else
				newStyles[id].parts.push(part);
		}
		return styles;
	}

	function createStyleElement() {
		var styleElement = document.createElement("style");
		var head = getHeadElement();
		styleElement.type = "text/css";
		head.appendChild(styleElement);
		return styleElement;
	}

	function addStyle(obj, options) {
		var styleElement, update, remove;

		if (options.singleton) {
			var styleIndex = singletonCounter++;
			styleElement = singletonElement || (singletonElement = createStyleElement());
			update = applyToSingletonTag.bind(null, styleElement, styleIndex, false);
			remove = applyToSingletonTag.bind(null, styleElement, styleIndex, true);
		} else {
			styleElement = createStyleElement();
			update = applyToTag.bind(null, styleElement);
			remove = function () {
				styleElement.parentNode.removeChild(styleElement);
			};
		}

		update(obj);

		return function updateStyle(newObj) {
			if(newObj) {
				if(newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap)
					return;
				update(obj = newObj);
			} else {
				remove();
			}
		};
	}

	function replaceText(source, id, replacement) {
		var boundaries = ["/** >>" + id + " **/", "/** " + id + "<< **/"];
		var start = source.lastIndexOf(boundaries[0]);
		var wrappedReplacement = replacement
			? (boundaries[0] + replacement + boundaries[1])
			: "";
		if (source.lastIndexOf(boundaries[0]) >= 0) {
			var end = source.lastIndexOf(boundaries[1]) + boundaries[1].length;
			return source.slice(0, start) + wrappedReplacement + source.slice(end);
		} else {
			return source + wrappedReplacement;
		}
	}

	function applyToSingletonTag(styleElement, index, remove, obj) {
		var css = remove ? "" : obj.css;

		if(styleElement.styleSheet) {
			styleElement.styleSheet.cssText = replaceText(styleElement.styleSheet.cssText, index, css);
		} else {
			var cssNode = document.createTextNode(css);
			var childNodes = styleElement.childNodes;
			if (childNodes[index]) styleElement.removeChild(childNodes[index]);
			if (childNodes.length) {
				styleElement.insertBefore(cssNode, childNodes[index]);
			} else {
				styleElement.appendChild(cssNode);
			}
		}
	}

	function applyToTag(styleElement, obj) {
		var css = obj.css;
		var media = obj.media;
		var sourceMap = obj.sourceMap;

		if(sourceMap && typeof btoa === "function") {
			try {
				css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(JSON.stringify(sourceMap)) + " */";
				css = "@import url(\"data:stylesheet/css;base64," + btoa(css) + "\")";
			} catch(e) {}
		}

		if(media) {
			styleElement.setAttribute("media", media)
		}

		if(styleElement.styleSheet) {
			styleElement.styleSheet.cssText = css;
		} else {
			while(styleElement.firstChild) {
				styleElement.removeChild(styleElement.firstChild);
			}
			styleElement.appendChild(document.createTextNode(css));
		}
	}

/***/ },
/* 5 */
/***/ function(module, exports, __webpack_require__) {

	module.exports = function() {
		var list = [];
		list.toString = function toString() {
			var result = [];
			for(var i = 0; i < this.length; i++) {
				var item = this[i];
				if(item[2]) {
					result.push("@media " + item[2] + "{" + item[1] + "}");
				} else {
					result.push(item[1]);
				}
			}
			return result.join("");
		};
		return list;
	}

/***/ }
/******/ ])<commit_msg>Removed bundle.js (again)<commit_after><|endoftext|>


<commit_before>"use strict";

Epitrochoide.prototype.draw = function(ctx, t) {
    var theta = this.theta(t);
    var cx = this.position.x, cy = this.position.y;
    ctx.beginPath();
    ctx.arc(cx, cy, this.R, 2 * Math.PI, false);
    ctx.moveTo(this.position.x - 3, this.position.y + 0.5);
    ctx.lineTo(this.position.x + 4, this.position.y + 0.5);
    ctx.moveTo(this.position.x + 0.5, this.position.y - 3);
    ctx.lineTo(this.position.x + 0.5, this.position.y + 4);
    ctx.stroke();

    if(this.r > 0) {
        var p = new Vector();
        p.x = (this.R + this.r) * Math.cos(theta);
        p.y = (this.R + this.r) * Math.sin(theta);
        p.add(this.position);
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.r, 2 * Math.PI, false);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.d, 2 * Math.PI, false);
        ctx.stroke();

        var p2 = new Vector(p.x, p.y);
        p2.sub(this.position);
        p2.x -= this.d * Math.cos(theta * (this.R + this.r) / this.r + this.phase2);
        p2.y -= this.d * Math.sin(theta * (this.R + this.r) / this.r + this.phase2);
        p2.add(this.position);
        ctx.beginPath();
        ctx.moveTo(p.x, p.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.stroke();
    }

    var p = this.period();
    for(var i = 0; i <= p; ++i) {
        ctx.fillRect(Math.round(this.points[i].x), Math.round(this.points[i].y), 1, 1);
    }
};

Hypotrochoide.prototype.draw = function(ctx, t) {
    var theta = this.theta(t);
    var cx = this.position.x, cy = this.position.y;
    ctx.beginPath();
    ctx.arc(cx, cy, this.R, 2 * Math.PI, false);
    ctx.moveTo(this.position.x-3, this.position.y+0.5);
    ctx.lineTo(this.position.x+4, this.position.y+0.5);
    ctx.moveTo(this.position.x+0.5, this.position.y-3);
    ctx.lineTo(this.position.x+0.5, this.position.y+4);
    ctx.stroke();

    if(this.r > 0) {
        var p = new Vector();
        p.x = (this.R - this.r) * Math.cos(theta);
        p.y = (this.R - this.r) * Math.sin(theta);
        p.add(this.position);
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.r, 2 * Math.PI, false);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.d, 2 * Math.PI, false);
        ctx.stroke();

        var p2 = new Vector(p.x, p.y);
        p2.sub(this.position);
        p2.x += this.d * Math.cos(theta * (this.R - this.r) / this.r + this.phase2);
        p2.y -= this.d * Math.sin(theta * (this.R - this.r) / this.r + this.phase2);
        p2.add(this.position);
        ctx.beginPath();
        ctx.moveTo(p.x, p.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.stroke();
    }

    var p = this.period();
    for(var i = 0; i <= p; ++i) {
        ctx.fillRect(Math.round(this.points[i].x), Math.round(this.points[i].y), 1, 1);
    }
};

SimpleArticulatedArm.prototype.draw = function(ctx, leftHandle, rightHandle) {
    var intersections = circleIntersections(leftHandle.x, leftHandle.y, this.l, rightHandle.x, rightHandle.y, this.r);
    var intersect = intersections[0];

    ctx.beginPath();
    ctx.moveTo(leftHandle.x, leftHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.moveTo(rightHandle.x, rightHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.stroke();
};

Pantograph.prototype.draw = function(ctx, leftHandle, rightHandle) {
    var intersections = circleIntersections(leftHandle.x, leftHandle.y, this.l1, rightHandle.x, rightHandle.y, this.r1);

    var intersect = intersections[0];

    var leftArm = new Vector(intersect.x - leftHandle.x, intersect.y - leftHandle.y);
    var rightArm = new Vector(intersect.x - rightHandle.x, intersect.y - rightHandle.y);

    ctx.beginPath();
    ctx.moveTo(leftHandle.x, leftHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.moveTo(rightHandle.x, rightHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.stroke();

    var leftArm2 = new Vector(leftArm.x, leftArm.y);
    var rightArm2 = new Vector(rightArm.x, rightArm.y);

    leftArm2.normalize(); leftArm2.mult(this.l2); leftArm2.add(intersect);
    rightArm2.normalize(); rightArm2.mult(this.r2); rightArm2.add(intersect);

    ctx.beginPath();
    ctx.moveTo(intersect.x, intersect.y);
    ctx.lineTo(leftArm2.x, leftArm2.y);
    ctx.moveTo(intersect.x, intersect.y);
    ctx.lineTo(rightArm2.x, rightArm2.y);
    ctx.stroke();

    var intersections2 = circleIntersections(rightArm2.x, rightArm2.y, this.r3, leftArm2.x, leftArm2.y, this.l3);

    ctx.beginPath();
    ctx.moveTo(leftArm2.x, leftArm2.y);
    ctx.lineTo(intersections2[0].x, intersections2[0].y);
    ctx.moveTo(rightArm2.x, rightArm2.y);
    ctx.lineTo(intersections2[0].x, intersections2[0].y);
    ctx.stroke();
};

function CanvasPintograph(parameters, speed) {
    Pintograph.call(this, parameters);

    this.speed     = speed;
    this.animate   = false;
    this.running   = false;

    this.restart();

    this.updateObservable = new Observable();
}

CanvasPintograph.prototype = Object.create(Pintograph.prototype);
CanvasPintograph.prototype.constructor = Pintograph;

CanvasPintograph.prototype.draw = function(time) {
    this.updateObservable.notifyObservers('beginUpdate');

    var interval = (time - this.lastUpdateTime);

    if (this.animate) {
        var step = Math.floor(this.speed * interval / 1000);

        if (step >= 1) {
            this.lastUpdateTime = time;
            this.t = Math.min(this.t + step, this.points.length - 1);
        }
    } else {
        this.t = this.points.length - 1;
    }

    if (this.t == this.points.length - 1) { this.running = false; }

    this.ctx.setTransform(1,0,0,1,0,0);
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    //this.ctx.translate(0.5, 0.5);

    this.ctx.strokeStyle = 'orangered';
    this.ctx.lineWidth = 1;

    drawPath(this.ctx, this.points, this.t, true);

    this.ctx.strokeStyle = 'white';
    this.ctx.fillStyle = 'white';

    this.left.draw(this.ctx, this.t);
    this.right.draw(this.ctx, this.t);
    this.arm.draw(this.ctx, this.left.at(this.t), this.right.at(this.t));

    if (this.running) {
        this.animationFrameRequest = window.requestAnimationFrame(this.draw.bind(this));
    }

    this.updateObservable.notifyObservers('endUpdate');
};

CanvasPintograph.prototype.restart = function() {
    this.t = 0;
    this.lastUpdateTime = performance.now();
    this.running = false;
};

CanvasPintograph.prototype.redraw = function() {
    if (!this.animate) {
        this.draw(performance.now());
    } else if (!this.running) {
        this.running = true;
        this.draw(performance.now());
    }
};

CanvasPintograph.prototype.handleAnimate = function(value) {
    this.animate = value;
    if (value) {
        this.restart();
        this.redraw();
    } else {
        this.running = false;
    }
};

function CanvasHarmonograph(parameters, speed) {
    Harmonograph.call(this, parameters);

    this.speed     = speed;
    this.animate   = false;
    this.running   = false;

    this.restart();

    this.updateObservable = new Observable();
}

CanvasHarmonograph.prototype = Object.create(Harmonograph.prototype);
CanvasHarmonograph.prototype.constructor = Harmonograph;

CanvasHarmonograph.prototype.draw = function(time) {
    this.updateObservable.notifyObservers('beginUpdate');

    if (this.animate) {
        var interval = (time - this.lastUpdateTime);
        var pointsToDraw = Math.floor(this.speed * this.precision * interval / 1000);

        if (pointsToDraw >= 1) {
            this.lastUpdateTime = time;
            this.lastPoint = Math.min(this.lastPoint + pointsToDraw, this.points.length - 1);
        }
    } else {
        this.lastPoint = this.points.length - 1;
    }

    if (this.lastPoint == this.points.length - 1) { this.running = false; }

    this.ctx.setTransform(1,0,0,1,0,0);
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.ctx.translate(this.canvas.width / 2.0, this.canvas.height / 2.0)
    //this.ctx.translate(0.5, 0.5);

    this.ctx.strokeStyle = 'orangered';
    this.ctx.fillStyle = 'green';
    this.ctx.lineWidth = 1;

    var px = this.x.at(this.lastPoint / this.precision);
    var py = this.y.at(this.lastPoint / this.precision);

    this.ctx.beginPath();
    this.ctx.moveTo(px + 0.5, -this.canvas.height / 2.0);
    this.ctx.lineTo(px + 0.5,  this.canvas.height / 2.0);

    this.ctx.moveTo(-this.canvas.width / 2.0, py + 0.5);
    this.ctx.lineTo( this.canvas.width / 2.0, py + 0.5);
    this.ctx.stroke()

    this.board.transform(this.lastPoint / this.precision, this.ctx);

    drawPath(this.ctx, this.points, this.lastPoint, false);

    if (this.running) {
        this.animationFrameRequest = window.requestAnimationFrame(this.draw.bind(this));
    }

    this.updateObservable.notifyObservers('endUpdate');
};

CanvasHarmonograph.prototype.restart = function() {
    this.lastPoint = 0;
    this.lastUpdateTime = performance.now();
};

CanvasHarmonograph.prototype.redraw = function() {
    if (!this.animate) {
        this.draw(performance.now());
    } else if (!this.running) {
        this.running = true;
        this.draw(performance.now());
    }
};

CanvasHarmonograph.prototype.handleAnimate = function(value) {
    this.animate = value;
    if (value) {
        this.restart();
        this.redraw();
    } else {
        this.running = false;
    }
};<commit_msg>Paint the harmonograph's pen in white.<commit_after>"use strict";

Epitrochoide.prototype.draw = function(ctx, t) {
    var theta = this.theta(t);
    var cx = this.position.x, cy = this.position.y;
    ctx.beginPath();
    ctx.arc(cx, cy, this.R, 2 * Math.PI, false);
    ctx.moveTo(this.position.x - 3, this.position.y + 0.5);
    ctx.lineTo(this.position.x + 4, this.position.y + 0.5);
    ctx.moveTo(this.position.x + 0.5, this.position.y - 3);
    ctx.lineTo(this.position.x + 0.5, this.position.y + 4);
    ctx.stroke();

    if(this.r > 0) {
        var p = new Vector();
        p.x = (this.R + this.r) * Math.cos(theta);
        p.y = (this.R + this.r) * Math.sin(theta);
        p.add(this.position);
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.r, 2 * Math.PI, false);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.d, 2 * Math.PI, false);
        ctx.stroke();

        var p2 = new Vector(p.x, p.y);
        p2.sub(this.position);
        p2.x -= this.d * Math.cos(theta * (this.R + this.r) / this.r + this.phase2);
        p2.y -= this.d * Math.sin(theta * (this.R + this.r) / this.r + this.phase2);
        p2.add(this.position);
        ctx.beginPath();
        ctx.moveTo(p.x, p.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.stroke();
    }

    var p = this.period();
    for(var i = 0; i <= p; ++i) {
        ctx.fillRect(Math.round(this.points[i].x), Math.round(this.points[i].y), 1, 1);
    }
};

Hypotrochoide.prototype.draw = function(ctx, t) {
    var theta = this.theta(t);
    var cx = this.position.x, cy = this.position.y;
    ctx.beginPath();
    ctx.arc(cx, cy, this.R, 2 * Math.PI, false);
    ctx.moveTo(this.position.x-3, this.position.y+0.5);
    ctx.lineTo(this.position.x+4, this.position.y+0.5);
    ctx.moveTo(this.position.x+0.5, this.position.y-3);
    ctx.lineTo(this.position.x+0.5, this.position.y+4);
    ctx.stroke();

    if(this.r > 0) {
        var p = new Vector();
        p.x = (this.R - this.r) * Math.cos(theta);
        p.y = (this.R - this.r) * Math.sin(theta);
        p.add(this.position);
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.r, 2 * Math.PI, false);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.d, 2 * Math.PI, false);
        ctx.stroke();

        var p2 = new Vector(p.x, p.y);
        p2.sub(this.position);
        p2.x += this.d * Math.cos(theta * (this.R - this.r) / this.r + this.phase2);
        p2.y -= this.d * Math.sin(theta * (this.R - this.r) / this.r + this.phase2);
        p2.add(this.position);
        ctx.beginPath();
        ctx.moveTo(p.x, p.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.stroke();
    }

    var p = this.period();
    for(var i = 0; i <= p; ++i) {
        ctx.fillRect(Math.round(this.points[i].x), Math.round(this.points[i].y), 1, 1);
    }
};

SimpleArticulatedArm.prototype.draw = function(ctx, leftHandle, rightHandle) {
    var intersections = circleIntersections(leftHandle.x, leftHandle.y, this.l, rightHandle.x, rightHandle.y, this.r);
    var intersect = intersections[0];

    ctx.beginPath();
    ctx.moveTo(leftHandle.x, leftHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.moveTo(rightHandle.x, rightHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.stroke();
};

Pantograph.prototype.draw = function(ctx, leftHandle, rightHandle) {
    var intersections = circleIntersections(leftHandle.x, leftHandle.y, this.l1, rightHandle.x, rightHandle.y, this.r1);

    var intersect = intersections[0];

    var leftArm = new Vector(intersect.x - leftHandle.x, intersect.y - leftHandle.y);
    var rightArm = new Vector(intersect.x - rightHandle.x, intersect.y - rightHandle.y);

    ctx.beginPath();
    ctx.moveTo(leftHandle.x, leftHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.moveTo(rightHandle.x, rightHandle.y);
    ctx.lineTo(intersect.x, intersect.y);
    ctx.stroke();

    var leftArm2 = new Vector(leftArm.x, leftArm.y);
    var rightArm2 = new Vector(rightArm.x, rightArm.y);

    leftArm2.normalize(); leftArm2.mult(this.l2); leftArm2.add(intersect);
    rightArm2.normalize(); rightArm2.mult(this.r2); rightArm2.add(intersect);

    ctx.beginPath();
    ctx.moveTo(intersect.x, intersect.y);
    ctx.lineTo(leftArm2.x, leftArm2.y);
    ctx.moveTo(intersect.x, intersect.y);
    ctx.lineTo(rightArm2.x, rightArm2.y);
    ctx.stroke();

    var intersections2 = circleIntersections(rightArm2.x, rightArm2.y, this.r3, leftArm2.x, leftArm2.y, this.l3);

    ctx.beginPath();
    ctx.moveTo(leftArm2.x, leftArm2.y);
    ctx.lineTo(intersections2[0].x, intersections2[0].y);
    ctx.moveTo(rightArm2.x, rightArm2.y);
    ctx.lineTo(intersections2[0].x, intersections2[0].y);
    ctx.stroke();
};

function CanvasPintograph(parameters, speed) {
    Pintograph.call(this, parameters);

    this.speed     = speed;
    this.animate   = false;
    this.running   = false;

    this.restart();

    this.updateObservable = new Observable();
}

CanvasPintograph.prototype = Object.create(Pintograph.prototype);
CanvasPintograph.prototype.constructor = Pintograph;

CanvasPintograph.prototype.draw = function(time) {
    this.updateObservable.notifyObservers('beginUpdate');

    var interval = (time - this.lastUpdateTime);

    if (this.animate) {
        var step = Math.floor(this.speed * interval / 1000);

        if (step >= 1) {
            this.lastUpdateTime = time;
            this.t = Math.min(this.t + step, this.points.length - 1);
        }
    } else {
        this.t = this.points.length - 1;
    }

    if (this.t == this.points.length - 1) { this.running = false; }

    this.ctx.setTransform(1,0,0,1,0,0);
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    //this.ctx.translate(0.5, 0.5);

    this.ctx.strokeStyle = 'orangered';
    this.ctx.lineWidth = 1;

    drawPath(this.ctx, this.points, this.t, true);

    this.ctx.strokeStyle = 'white';
    this.ctx.fillStyle = 'white';

    this.left.draw(this.ctx, this.t);
    this.right.draw(this.ctx, this.t);
    this.arm.draw(this.ctx, this.left.at(this.t), this.right.at(this.t));

    if (this.running) {
        this.animationFrameRequest = window.requestAnimationFrame(this.draw.bind(this));
    }

    this.updateObservable.notifyObservers('endUpdate');
};

CanvasPintograph.prototype.restart = function() {
    this.t = 0;
    this.lastUpdateTime = performance.now();
    this.running = false;
};

CanvasPintograph.prototype.redraw = function() {
    if (!this.animate) {
        this.draw(performance.now());
    } else if (!this.running) {
        this.running = true;
        this.draw(performance.now());
    }
};

CanvasPintograph.prototype.handleAnimate = function(value) {
    this.animate = value;
    if (value) {
        this.restart();
        this.redraw();
    } else {
        this.running = false;
    }
};

function CanvasHarmonograph(parameters, speed) {
    Harmonograph.call(this, parameters);

    this.speed     = speed;
    this.animate   = false;
    this.running   = false;

    this.restart();

    this.updateObservable = new Observable();
}

CanvasHarmonograph.prototype = Object.create(Harmonograph.prototype);
CanvasHarmonograph.prototype.constructor = Harmonograph;

CanvasHarmonograph.prototype.draw = function(time) {
    this.updateObservable.notifyObservers('beginUpdate');

    if (this.animate) {
        var interval = (time - this.lastUpdateTime);
        var pointsToDraw = Math.floor(this.speed * this.precision * interval / 1000);

        if (pointsToDraw >= 1) {
            this.lastUpdateTime = time;
            this.lastPoint = Math.min(this.lastPoint + pointsToDraw, this.points.length - 1);
        }
    } else {
        this.lastPoint = this.points.length - 1;
    }

    if (this.lastPoint == this.points.length - 1) { this.running = false; }

    this.ctx.setTransform(1,0,0,1,0,0);
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.ctx.translate(this.canvas.width / 2.0, this.canvas.height / 2.0)
    //this.ctx.translate(0.5, 0.5);

    this.board.transform(this.lastPoint / this.precision, this.ctx);

    this.ctx.strokeStyle = 'orangered';
    this.ctx.fillStyle = 'orangered';
    this.ctx.lineWidth = 1;

    drawPath(this.ctx, this.points, this.lastPoint, false);

    this.ctx.strokeStyle = 'white';
    this.ctx.fillStyle = 'white';

    this.ctx.setTransform(1,0,0,1,0,0);
    this.ctx.translate(this.canvas.width / 2.0, this.canvas.height / 2.0)

    var px = this.x.at(this.lastPoint / this.precision);
    var py = this.y.at(this.lastPoint / this.precision);

    this.ctx.beginPath();
    this.ctx.moveTo(px + 0.5, -this.canvas.height / 2.0);
    this.ctx.lineTo(px + 0.5,  this.canvas.height / 2.0);

    this.ctx.moveTo(-this.canvas.width / 2.0, py);
    this.ctx.lineTo( this.canvas.width / 2.0, py);
    this.ctx.stroke();

    if (this.running) {
        this.animationFrameRequest = window.requestAnimationFrame(this.draw.bind(this));
    }

    this.updateObservable.notifyObservers('endUpdate');
};

CanvasHarmonograph.prototype.restart = function() {
    this.lastPoint = 0;
    this.lastUpdateTime = performance.now();
};

CanvasHarmonograph.prototype.redraw = function() {
    if (!this.animate) {
        this.draw(performance.now());
    } else if (!this.running) {
        this.running = true;
        this.draw(performance.now());
    }
};

CanvasHarmonograph.prototype.handleAnimate = function(value) {
    this.animate = value;
    if (value) {
        this.restart();
        this.redraw();
    } else {
        this.running = false;
    }
};<|endoftext|>


<commit_before>import ContainerPlugin from '../../src/base/container_plugin'

describe('Container Plugin', function() {
  it('should be enabled when created', function() {
    var plugin = new ContainerPlugin({})
    expect(plugin.enabled).to.be.true
  })

  it('should be disable when call disable()', function() {
    var plugin = new ContainerPlugin({})
    plugin.disable()
    expect(plugin.enabled).to.be.false
  })

  it('should call stopListening() when disabled', function() {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'stopListening')
    plugin.disable()
    expect(spy).called.once
  })

  it('should call stopListening() when destroyed', function() {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'stopListening')
    plugin.destroy()
    expect(spy).called.once
  })

  it('should not call bindEvents() twice', function() {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'bindEvents')
    plugin.disable()
    plugin.enable()
    plugin.enable()
    expect(spy).called.once
  })

})
<commit_msg>tests: add test for plugin<commit_after>import ContainerPlugin from 'base/container_plugin'

describe('Container Plugin', function() {
  describe('#constructor', () => {
    it('enables', function() {
      var plugin = new ContainerPlugin({})

      expect(plugin.enabled).to.be.true
    })

    it('binds all events', () => {
      var bind = false
      var Plugin = class MyPlugin extends ContainerPlugin{
        bindEvents() {
          bind = true
        }
      }

      new Plugin({})

      expect(bind).to.be.true
    })
  })

  it('disables', () => {
    var plugin = new ContainerPlugin({})

    plugin.disable()

    expect(plugin.enabled).to.be.false
  })

  it('stops listening when disable an enabled plugin', () => {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'stopListening')

    plugin.disable()

    expect(spy).called.once
  })

  it('doesnt stops listening when disable a disabled plugin', () => {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'stopListening')

    plugin.enabled = false
    plugin.disable()

    expect(spy).not.called
  })

  it('stops listening when destroyed', () => {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'stopListening')

    plugin.destroy()

    expect(spy).called.once
  })

  it('binds events once', () => {
    var plugin = new ContainerPlugin({})
    var spy = sinon.spy(plugin, 'bindEvents')

    plugin.enable()
    plugin.enable()
    plugin.enable()

    expect(spy).not.called
  })
})
<|endoftext|>


<commit_before>const paramCase = require('param-case')
    , bootswatch = require('bootswatch/api/3.json').themes
    , bareReact = require.resolve('generator-bare-react')

const { Base } = require('yeoman-generator')

const PERMISSIONS =
  [ 'tabs'
  , 'bookmarks'
  , 'cookies'
  , 'history'
  , 'management' ]

const DEV_DEPENDENCIES =
  { 'babelify': '~6.4.0'
  , 'browserify': null // use latest
  , 'envify': null
  , 'trash': '~2.0.0' // promisified as of 3.0
  , 'gulp-zip': null
  , 'run-sequence': null
  , 'through2': null
  , 'uglifyify': null
  , 'watchify': null
  , 'concat-stream': null
  , 'path-exists': null
  , 'gulp-imagemin': null
  , 'config-prompt': '~1.0.0'
  , 'in-production': null
  , 'vinyl-imitate': '~1.0.0'
  , 'add-transforms': '~1.0.0' }

const REACT_DEV_DEPENDENCIES =
  { 'babel-plugin-react-transform': '~1.1.1'
  , 'livereactload': '~2.1.0'
  , 'react-proxy': '~1.1.1'
  , 'livereactload-chrome': '~1.0.2' }

function bootswatchNames() {
  return bootswatch.map(theme => theme.name)
}

const self = module.exports = class ChromeGenerator extends Base {
  // TODO: rename all methods, use priorities
  askFor() {
    // In case a module was already generated or created
    let { name, description } = this.fs.readJSON('package.json', {})

    // Use existing manifest values as defaults
    let { permissions = [], browser_action, page_action, options_page, omnibox
        , content_scripts }
        = this.fs.readJSON('app/manifest.json', {})

    let questions =
      [ { name: 'name'
        , message: 'What would you like to call this extension?'
        , default: paramCase(name || this.appname || 'my-chrome-extension')
        , validate: (v) => paramCase(v).length ? true : 'You have to provide a name'
        , filter: paramCase
      },{ name: 'description'
        , message: 'How would you like to describe this extension?'
        , default: description || 'My Chrome Extension'
        , validate: (v) => v.trim().length ? true : 'You have to provide a description'
        , filter: (s) => s.trim()
      },{ type: 'list'
        , name: 'action'
        , message: 'Would you like to use UI Action?'
        , default: browser_action ? 'Browser' : page_action ? 'Page' : 'No'
        , choices: [ 'No', 'Browser', 'Page' ]
      },{ type: 'checkbox'
        , name: 'uifeatures'
        , message: 'Would you like more UI Features?'
        , choices: [ { value: 'options', name: 'Options Page'
                     , checked: !!options_page }
                   , { value: 'contentscript', name: 'Content Scripts'
                     , checked: !!content_scripts }
                   , { value: 'omnibox', name: 'Omnibox'
                     , checked: !!omnibox ]
      },{ type: 'confirm'
        , name: 'bootstrap'
        , store: true
        , message: 'Would you like to use Bootstrap?'
        , default: false
      },{ type: 'list'
        , name: 'bootstrapTheme'
        , message: 'Which Bootstrap theme would you like?'
        , when: (answers) => answers.bootstrap === true
        , default: 'none'
        , store: true
        , choices: [ 'none', 'default' ].concat(bootswatchNames())
      },{ type: 'checkbox'
        , name: 'permissions'
        , message: 'Which permissions do you need?'
        , choices: PERMISSIONS.map(name => {
            let checked = permissions.indexOf(name) >= 0
            return { value: name, name, checked }
          })
      }
    ]

    let done = this.async()

    this.prompt(questions, (answers) => {
      let isChecked = (choices, value) => choices.indexOf(value) >= 0
        , escape = (s) => s.replace(/\"/g, '\\"')
        , name = this.appname = escape(answers.name)
        , description = escape(answers.description)

      // TODO: set display value for question
      if (answers.bootstrapTheme === 'none') answers.bootstrapTheme = null

      this.ctx = answers
      this.manifest = { name, description, permissions: {} }

      this.manifest.action = answers.action === 'No' ? 0 : answers.action === 'Browser' ? 1 : 2
      this.manifest.options = isChecked(answers.uifeatures, 'options')
      this.manifest.omnibox = isChecked(answers.uifeatures, 'omnibox')
      this.manifest.contentscript = isChecked(answers.uifeatures, 'contentscript')
      this.manifest.permissions.tabs = isChecked(answers.permissions, 'tabs')
      this.manifest.permissions.bookmarks = isChecked(answers.permissions, 'bookmarks')
      this.manifest.permissions.cookies = isChecked(answers.permissions, 'cookies')
      this.manifest.permissions.history = isChecked(answers.permissions, 'history')
      this.manifest.permissions.management = isChecked(answers.permissions, 'management')

      let dependencies = {}
        , devDependencies = { ...DEV_DEPENDENCIES }

      if (this.manifest.action > 0 || this.manifest.options || this.manifest.contentscript) {
        // Put additional babelrc in app dir, with livereactload config
        this._copy('react.babelrc', 'app/.babelrc')

        // Add livereactload dependencies
        devDependencies = { ...devDependencies, ...REACT_DEV_DEPENDENCIES }
      }

      this.composeWith('nom'
        , { options:  { name
                      , description
                      , skipInstall: this.options.skipInstall
                      , skipCache: this.options.skipCache
                      , enable: [ 'gulp'
                                , 'npm'  ]
                      , gulp: { tasks: this.templatePath('tasks') + '/**/*'
                              , ctx: this.ctx }
                      , npm:  { cli: false
                              , dependencies
                              , devDependencies }}}
        , { local: require.resolve('generator-nom')
          , link: 'strong' })

      done()
    })
  }

  _addReact(options) {
    let link = { local: bareReact, link: 'strong' }
    this.composeWith('bare-react', { options }, link)
  }

  // TODO: remove legacy code and just stringify manifest as a whole
  manifest() {
    let manifest = {}
      , permissions = []
      , items = []

    // add browser / page action field
    if (this.manifest.action > 0) {
      let action = { default_icon: { 19: 'images/icon-19.png'
                                   , 38: 'images/icon-38.png' }
                   , default_title: this.manifest.name
                   , default_popup: 'lib/popup/popup.html' }

      let type = this.manifest.action === 1 ? 'browser_action' : 'page_action'

      manifest[type] = JSON.stringify(action, null, 2).replace(/\n/g, '\n  ')

      this._addReact
       ({ type: 'app'
        , dest: 'app/lib/popup'
        , name: 'Popup'
        , router: false
        , bootstrap: this.ctx.bootstrap
        , skipInstall: this.options.skipInstall
        , skipCache: this.options.skipCache
        , children: [ 'PopupNinja' ] })
    }

    // add options page field.
    if (this.manifest.options) {
      let options_ui = { page: 'lib/options/options.html', chrome_style: true }

      manifest.options_page = '"lib/options/options.html"'
      manifest.options_ui = JSON.stringify(options_ui, null, 2).replace(/\n/g, '\n  ')

      this._addReact
       ({ type: 'app'
        , skipInstall: this.options.skipInstall
        , skipCache: this.options.skipCache
        , bootstrap: this.ctx.bootstrap
        , dest: 'app/lib/options'
        , name: 'Options' })
    }

    // add omnibox keyword field.
    if (this.manifest.omnibox) {
      let omnibox = { keyword: this.manifest.name }
      manifest.omnibox = JSON.stringify(omnibox, null, 2).replace(/\n/g, '\n  ')
    }

    // add contentscript field.
    if (this.manifest.contentscript) {
      let contentscript = { matches: ['http://*/*', 'https://*/*']
                          , js: ['lib/content-script/index.js']
                          , run_at: 'document_end'
                          , all_frames: false }

      manifest.content_scripts =
        JSON.stringify([contentscript], null, 2).replace(/\n/g, '\n  ')

      this._addReact
       ({ type: 'app'
        , skipInstall: this.options.skipInstall
        , skipCache: this.options.skipCache
        , dest: 'app/lib/content-script'
        , name: 'ContentScript'
        , append: true
        , router: false
        , bootstrap: false })
    }

    // add generate permission field.
    for (let p in this.manifest.permissions) {
      if (this.manifest.permissions[p]) {
        permissions.push(p)
      }
    }

    // add generic match pattern field.
    if (this.manifest.permissions.tabs) {
      permissions.push('http://*/*')
      permissions.push('https://*/*')
    }

    if (permissions.length > 0) {
      manifest.permissions =
        JSON.stringify(permissions, null, 2).replace(/\n/g, '\n  ')
    }

    for (let k in manifest) {
      items.push(['  "', k, '": ', manifest[k]].join(''))
    }

    this.manifest.items = items.length > 0 ? ',\n' + items.join(',\n') : ''

    this._copyTpl('manifest.json', 'app/manifest.json', this.manifest)
  }

  actions() {
    if (this.manifest.action !== 0) {
      this._copy('popup.html', 'app/lib/popup/popup.html')
      this._copy('images/icon-19.png', 'app/images/icon-19.png')
      this._copy('images/icon-38.png', 'app/images/icon-38.png')
      this._copyCSS('app/lib/popup')
    }
  }

  eventpage() {
    let pageName = 'background page'
      , withAction = false
      , action = this.manifest.action

    if (action === 2) {
      pageName = 'event page for Page Action'
      withAction = true
    } else if (action === 1) {
      pageName = 'event page for Browser Action'
      withAction = true
    }

    let ctx = { pageName, withAction }
    this._copyJS('background.js', 'app/lib/background/index.js', ctx)
  }

  optionsHtml() {
    if (this.manifest.options) {
      this._copy('options.html', 'app/lib/options/options.html')
      this._copyCSS('app/lib/options')
    }
  }

  contentscript() {
    if (this.manifest.contentscript) {
      this._copyCSS('app/lib/content-script')
    }
  }

  assets() {
    let messages = '_locales/en/messages.json'

    this._copyTpl(messages, `app/${messages}`, this.manifest)
    this._copy('images/icon-16.png', 'app/images/icon-16.png')
    this._copy('images/icon-128.png', 'app/images/icon-128.png')
  }

  _copy(src, dest = src) {
    this.fs.copy(this.templatePath(src), this.destinationPath(dest))
  }

  _copyTpl(src, dest = src, ctx = {}) {
    this.fs.copyTpl(this.templatePath(src), this.destinationPath(dest), ctx)
  }

  _copyJS(src, dest = src, ctx = {}) {
    this._copyTpl('scripts/' + src, dest, ctx)
  }

  _copyCSS(dest) {
    this._copyTpl('styles/index.css', dest + '/index.css', this.ctx)
  }
}
<commit_msg>Missing brace<commit_after>const paramCase = require('param-case')
    , bootswatch = require('bootswatch/api/3.json').themes
    , bareReact = require.resolve('generator-bare-react')

const { Base } = require('yeoman-generator')

const PERMISSIONS =
  [ 'tabs'
  , 'bookmarks'
  , 'cookies'
  , 'history'
  , 'management' ]

const DEV_DEPENDENCIES =
  { 'babelify': '~6.4.0'
  , 'browserify': null // use latest
  , 'envify': null
  , 'trash': '~2.0.0' // promisified as of 3.0
  , 'gulp-zip': null
  , 'run-sequence': null
  , 'through2': null
  , 'uglifyify': null
  , 'watchify': null
  , 'concat-stream': null
  , 'path-exists': null
  , 'gulp-imagemin': null
  , 'config-prompt': '~1.0.0'
  , 'in-production': null
  , 'vinyl-imitate': '~1.0.0'
  , 'add-transforms': '~1.0.0' }

const REACT_DEV_DEPENDENCIES =
  { 'babel-plugin-react-transform': '~1.1.1'
  , 'livereactload': '~2.1.0'
  , 'react-proxy': '~1.1.1'
  , 'livereactload-chrome': '~1.0.2' }

function bootswatchNames() {
  return bootswatch.map(theme => theme.name)
}

const self = module.exports = class ChromeGenerator extends Base {
  // TODO: rename all methods, use priorities
  askFor() {
    // In case a module was already generated or created
    let { name, description } = this.fs.readJSON('package.json', {})

    // Use existing manifest values as defaults
    let { permissions = [], browser_action, page_action, options_page, omnibox
        , content_scripts }
        = this.fs.readJSON('app/manifest.json', {})

    let questions =
      [ { name: 'name'
        , message: 'What would you like to call this extension?'
        , default: paramCase(name || this.appname || 'my-chrome-extension')
        , validate: (v) => paramCase(v).length ? true : 'You have to provide a name'
        , filter: paramCase
      },{ name: 'description'
        , message: 'How would you like to describe this extension?'
        , default: description || 'My Chrome Extension'
        , validate: (v) => v.trim().length ? true : 'You have to provide a description'
        , filter: (s) => s.trim()
      },{ type: 'list'
        , name: 'action'
        , message: 'Would you like to use UI Action?'
        , default: browser_action ? 'Browser' : page_action ? 'Page' : 'No'
        , choices: [ 'No', 'Browser', 'Page' ]
      },{ type: 'checkbox'
        , name: 'uifeatures'
        , message: 'Would you like more UI Features?'
        , choices: [ { value: 'options', name: 'Options Page'
                     , checked: !!options_page }
                   , { value: 'contentscript', name: 'Content Scripts'
                     , checked: !!content_scripts }
                   , { value: 'omnibox', name: 'Omnibox'
                     , checked: !!omnibox }]
      },{ type: 'confirm'
        , name: 'bootstrap'
        , store: true
        , message: 'Would you like to use Bootstrap?'
        , default: false
      },{ type: 'list'
        , name: 'bootstrapTheme'
        , message: 'Which Bootstrap theme would you like?'
        , when: (answers) => answers.bootstrap === true
        , default: 'none'
        , store: true
        , choices: [ 'none', 'default' ].concat(bootswatchNames())
      },{ type: 'checkbox'
        , name: 'permissions'
        , message: 'Which permissions do you need?'
        , choices: PERMISSIONS.map(name => {
            let checked = permissions.indexOf(name) >= 0
            return { value: name, name, checked }
          })
      }
    ]

    let done = this.async()

    this.prompt(questions, (answers) => {
      let isChecked = (choices, value) => choices.indexOf(value) >= 0
        , escape = (s) => s.replace(/\"/g, '\\"')
        , name = this.appname = escape(answers.name)
        , description = escape(answers.description)

      // TODO: set display value for question
      if (answers.bootstrapTheme === 'none') answers.bootstrapTheme = null

      this.ctx = answers
      this.manifest = { name, description, permissions: {} }

      this.manifest.action = answers.action === 'No' ? 0 : answers.action === 'Browser' ? 1 : 2
      this.manifest.options = isChecked(answers.uifeatures, 'options')
      this.manifest.omnibox = isChecked(answers.uifeatures, 'omnibox')
      this.manifest.contentscript = isChecked(answers.uifeatures, 'contentscript')
      this.manifest.permissions.tabs = isChecked(answers.permissions, 'tabs')
      this.manifest.permissions.bookmarks = isChecked(answers.permissions, 'bookmarks')
      this.manifest.permissions.cookies = isChecked(answers.permissions, 'cookies')
      this.manifest.permissions.history = isChecked(answers.permissions, 'history')
      this.manifest.permissions.management = isChecked(answers.permissions, 'management')

      let dependencies = {}
        , devDependencies = { ...DEV_DEPENDENCIES }

      if (this.manifest.action > 0 || this.manifest.options || this.manifest.contentscript) {
        // Put additional babelrc in app dir, with livereactload config
        this._copy('react.babelrc', 'app/.babelrc')

        // Add livereactload dependencies
        devDependencies = { ...devDependencies, ...REACT_DEV_DEPENDENCIES }
      }

      this.composeWith('nom'
        , { options:  { name
                      , description
                      , skipInstall: this.options.skipInstall
                      , skipCache: this.options.skipCache
                      , enable: [ 'gulp'
                                , 'npm'  ]
                      , gulp: { tasks: this.templatePath('tasks') + '/**/*'
                              , ctx: this.ctx }
                      , npm:  { cli: false
                              , dependencies
                              , devDependencies }}}
        , { local: require.resolve('generator-nom')
          , link: 'strong' })

      done()
    })
  }

  _addReact(options) {
    let link = { local: bareReact, link: 'strong' }
    this.composeWith('bare-react', { options }, link)
  }

  // TODO: remove legacy code and just stringify manifest as a whole
  manifest() {
    let manifest = {}
      , permissions = []
      , items = []

    // add browser / page action field
    if (this.manifest.action > 0) {
      let action = { default_icon: { 19: 'images/icon-19.png'
                                   , 38: 'images/icon-38.png' }
                   , default_title: this.manifest.name
                   , default_popup: 'lib/popup/popup.html' }

      let type = this.manifest.action === 1 ? 'browser_action' : 'page_action'

      manifest[type] = JSON.stringify(action, null, 2).replace(/\n/g, '\n  ')

      this._addReact
       ({ type: 'app'
        , dest: 'app/lib/popup'
        , name: 'Popup'
        , router: false
        , bootstrap: this.ctx.bootstrap
        , skipInstall: this.options.skipInstall
        , skipCache: this.options.skipCache
        , children: [ 'PopupNinja' ] })
    }

    // add options page field.
    if (this.manifest.options) {
      let options_ui = { page: 'lib/options/options.html', chrome_style: true }

      manifest.options_page = '"lib/options/options.html"'
      manifest.options_ui = JSON.stringify(options_ui, null, 2).replace(/\n/g, '\n  ')

      this._addReact
       ({ type: 'app'
        , skipInstall: this.options.skipInstall
        , skipCache: this.options.skipCache
        , bootstrap: this.ctx.bootstrap
        , dest: 'app/lib/options'
        , name: 'Options' })
    }

    // add omnibox keyword field.
    if (this.manifest.omnibox) {
      let omnibox = { keyword: this.manifest.name }
      manifest.omnibox = JSON.stringify(omnibox, null, 2).replace(/\n/g, '\n  ')
    }

    // add contentscript field.
    if (this.manifest.contentscript) {
      let contentscript = { matches: ['http://*/*', 'https://*/*']
                          , js: ['lib/content-script/index.js']
                          , run_at: 'document_end'
                          , all_frames: false }

      manifest.content_scripts =
        JSON.stringify([contentscript], null, 2).replace(/\n/g, '\n  ')

      this._addReact
       ({ type: 'app'
        , skipInstall: this.options.skipInstall
        , skipCache: this.options.skipCache
        , dest: 'app/lib/content-script'
        , name: 'ContentScript'
        , append: true
        , router: false
        , bootstrap: false })
    }

    // add generate permission field.
    for (let p in this.manifest.permissions) {
      if (this.manifest.permissions[p]) {
        permissions.push(p)
      }
    }

    // add generic match pattern field.
    if (this.manifest.permissions.tabs) {
      permissions.push('http://*/*')
      permissions.push('https://*/*')
    }

    if (permissions.length > 0) {
      manifest.permissions =
        JSON.stringify(permissions, null, 2).replace(/\n/g, '\n  ')
    }

    for (let k in manifest) {
      items.push(['  "', k, '": ', manifest[k]].join(''))
    }

    this.manifest.items = items.length > 0 ? ',\n' + items.join(',\n') : ''

    this._copyTpl('manifest.json', 'app/manifest.json', this.manifest)
  }

  actions() {
    if (this.manifest.action !== 0) {
      this._copy('popup.html', 'app/lib/popup/popup.html')
      this._copy('images/icon-19.png', 'app/images/icon-19.png')
      this._copy('images/icon-38.png', 'app/images/icon-38.png')
      this._copyCSS('app/lib/popup')
    }
  }

  eventpage() {
    let pageName = 'background page'
      , withAction = false
      , action = this.manifest.action

    if (action === 2) {
      pageName = 'event page for Page Action'
      withAction = true
    } else if (action === 1) {
      pageName = 'event page for Browser Action'
      withAction = true
    }

    let ctx = { pageName, withAction }
    this._copyJS('background.js', 'app/lib/background/index.js', ctx)
  }

  optionsHtml() {
    if (this.manifest.options) {
      this._copy('options.html', 'app/lib/options/options.html')
      this._copyCSS('app/lib/options')
    }
  }

  contentscript() {
    if (this.manifest.contentscript) {
      this._copyCSS('app/lib/content-script')
    }
  }

  assets() {
    let messages = '_locales/en/messages.json'

    this._copyTpl(messages, `app/${messages}`, this.manifest)
    this._copy('images/icon-16.png', 'app/images/icon-16.png')
    this._copy('images/icon-128.png', 'app/images/icon-128.png')
  }

  _copy(src, dest = src) {
    this.fs.copy(this.templatePath(src), this.destinationPath(dest))
  }

  _copyTpl(src, dest = src, ctx = {}) {
    this.fs.copyTpl(this.templatePath(src), this.destinationPath(dest), ctx)
  }

  _copyJS(src, dest = src, ctx = {}) {
    this._copyTpl('scripts/' + src, dest, ctx)
  }

  _copyCSS(dest) {
    this._copyTpl('styles/index.css', dest + '/index.css', this.ctx)
  }
}
<|endoftext|>


<commit_before>const redis = require('redis')
const { promisify } = require('util')
const Log = require('gk-log')
const { flatten } = require('lodash')

const dbs = require('../lib/dbs')
const { createDocs } = require('../lib/repository-docs')
const GitHub = require('../lib/github')
const getToken = require('../lib/get-token')

module.exports = async function ({ accountId }) {
  const { installations, repositories } = await dbs()
  const logs = dbs.getLogsDb()
  const log = Log({ logsDb: logs, accountId: accountId, repoSlug: null, context: 'sync-repos' })
  log.info(`started`)

  const installation = await installations.get(String(accountId))
  const installationId = installation.installation

  let dbRepos
  let gitHubRepos
  let accountName
  try {
    const repoDoc = await installations.get(String(accountId))
    accountName = repoDoc.login
    const { rows: currentRepos } = await repositories.query('repo-by-org/repo-by-org', {
      key: accountName,
      reduce: false,
      include_docs: true
    })
    dbRepos = currentRepos.map(repo => repo.doc.fullName.split('/')[1])
  } catch (error) {
    log.error('Could not get repos from database', { error: error.message })
  }

  try {
    const { token } = await getToken(Number(installationId))
    const github = GitHub({ auth: `token ${token}` })
    const options = github.apps.listRepos.endpoint.merge({
      headers: {
        accept: 'application/vnd.github.machine-man-preview+json'
      },
      per_page: 100
    })

    // Paginate does not actually flatten results into a single result array
    // as it should, according to the docs, possibly due to these:
    // https://github.com/octokit/rest.js/issues/1161
    // https://github.com/octokit/routes/issues/329
    const results = await github.paginate(options)
    // So we flatten them ourselves
    gitHubRepos = flatten(results.map((result) => result.repositories))
  } catch (error) {
    log.error('Could not get repos from Github', { error: error.message })
  }
  log.info('There are more active repos on GitHub than in our database.')

  // create missing repositories
  let reposToCreate = []
  gitHubRepos.map(ghRepo => {
    if (!dbRepos.includes(ghRepo.name)) {
      reposToCreate.push(ghRepo)
    }
  })

  log.info('Starting to create missing repoDocs')
  try {
    const repoDocs = await createDocs({
      repositories: reposToCreate,
      accountId: String(accountId)
    })
    await repositories.bulkDocs(repoDocs)
  } catch (error) {
    log.warn('Could not create repository', { error: error.message })
  }

  const client = redis.createClient(process.env.REDIS_URL)
  const del = promisify(client.del)
  await del(`sync_${accountName}`)
}
<commit_msg>fix: promisify<commit_after>const redis = require('redis')
const { promisify } = require('util')
const Log = require('gk-log')
const { flatten } = require('lodash')

const dbs = require('../lib/dbs')
const { createDocs } = require('../lib/repository-docs')
const GitHub = require('../lib/github')
const getToken = require('../lib/get-token')

module.exports = async function ({ accountId }) {
  const { installations, repositories } = await dbs()
  const logs = dbs.getLogsDb()
  const log = Log({ logsDb: logs, accountId: accountId, repoSlug: null, context: 'sync-repos' })
  log.info(`started`)

  const installation = await installations.get(String(accountId))
  const installationId = installation.installation

  let dbRepos
  let gitHubRepos
  let accountName
  try {
    const repoDoc = await installations.get(String(accountId))
    accountName = repoDoc.login
    const { rows: currentRepos } = await repositories.query('repo-by-org/repo-by-org', {
      key: accountName,
      reduce: false,
      include_docs: true
    })
    dbRepos = currentRepos.map(repo => repo.doc.fullName.split('/')[1])
  } catch (error) {
    log.error('Could not get repos from database', { error: error.message })
  }

  try {
    const { token } = await getToken(Number(installationId))
    const github = GitHub({ auth: `token ${token}` })
    const options = github.apps.listRepos.endpoint.merge({
      headers: {
        accept: 'application/vnd.github.machine-man-preview+json'
      },
      per_page: 100
    })

    // Paginate does not actually flatten results into a single result array
    // as it should, according to the docs, possibly due to these:
    // https://github.com/octokit/rest.js/issues/1161
    // https://github.com/octokit/routes/issues/329
    const results = await github.paginate(options)
    // So we flatten them ourselves
    gitHubRepos = flatten(results.map((result) => result.repositories))
  } catch (error) {
    log.error('Could not get repos from Github', { error: error.message })
  }
  log.info('There are more active repos on GitHub than in our database.')

  // create missing repositories
  let reposToCreate = []
  gitHubRepos.map(ghRepo => {
    if (!dbRepos.includes(ghRepo.name)) {
      reposToCreate.push(ghRepo)
    }
  })

  log.info('Starting to create missing repoDocs')
  try {
    const repoDocs = await createDocs({
      repositories: reposToCreate,
      accountId: String(accountId)
    })
    await repositories.bulkDocs(repoDocs)
  } catch (error) {
    log.warn('Could not create repository', { error: error.message })
  }

  const client = redis.createClient(process.env.REDIS_URL)
  const del = promisify(client.del.bind(client))
  await del(`sync_${accountName}`)
}
<|endoftext|>


<commit_before>window.parent.postMessage(["setHeight", 50], "*");

function resize() {
    var height = document.body.scrollHeight;
    console.log(height);
    window.parent.postMessage(["setHeight", height], "*");
}

// requires jquery
$(function () {
    var placeholderElement = $("#comments-placeholder");
    placeholderElement.addClass("commentr");

    var handleLoadedComments = function (html) {
        placeholderElement.empty();
        placeholderElement.append(html);
        $("[data-commentr-datetime]").each(function (i, el) {
            var dt = new Date($(el).data("commentr-datetime"));
            $(el).html(dt.toLocaleString());
        });
        resize();
    };

    var loadComments = function () {
        $.ajax({
            dataType: "html",
            url: "/comments",
            data: { permalink: window.commentr.config.permalink },
            success: handleLoadedComments
        }).fail(function () {
            placeholderElement.empty();
            placeholderElement.find('.error').remove();
            placeholderElement.html('Error Loading Comments');
            resize();
        });
    };

    // handle new comment
    $(placeholderElement).on('submit', '#commentr-submit-form', function (e) {
        e.preventDefault();
        var formData = $(this).serialize();
        $.ajax({
            type: "POST",
            dataType: "html",
            url: "/comment",
            data: formData,
            success: handleLoadedComments
        }).fail(function () {
            placeholderElement.find('.error').remove();
            placeholderElement.append('Error Submitting Comment ' + new Date().getTime() + '');
            resize();
        });
    });

    loadComments();
});<commit_msg>fixing an IE layout bug where scrollHeight is not big enough to render, also wait for repaint before calling resize<commit_after>window.parent.postMessage(["setHeight", 50], "*");

function resize() {
    var height = document.body.offsetHeight + 30;
    console.log(height);
    window.parent.postMessage(["setHeight", height], "*");
}

// requires jquery
$(function () {
    var placeholderElement = $("#comments-placeholder");
    placeholderElement.addClass("commentr");

    var handleLoadedComments = function (html) {
        placeholderElement.empty();
        placeholderElement.append(html);
        $("[data-commentr-datetime]").each(function (i, el) {
            var dt = new Date($(el).data("commentr-datetime"));
            $(el).html(dt.toLocaleString());
        });
        setTimeout(resize, 100);
    };

    var loadComments = function () {
        $.ajax({
            dataType: "html",
            url: "/comments",
            data: { permalink: window.commentr.config.permalink },
            success: handleLoadedComments
        }).fail(function () {
            placeholderElement.empty();
            placeholderElement.find('.error').remove();
            placeholderElement.html('Error Loading Comments');
            setTimeout(resize, 100);
        });
    };

    // handle new comment
    $(placeholderElement).on('submit', '#commentr-submit-form', function (e) {
        e.preventDefault();
        var formData = $(this).serialize();
        $.ajax({
            type: "POST",
            dataType: "html",
            url: "/comment",
            data: formData,
            success: handleLoadedComments
        }).fail(function () {
            placeholderElement.find('.error').remove();
            placeholderElement.append('Error Submitting Comment ' + new Date().getTime() + '');
            setTimeout(resize, 100);
        });
    });

    loadComments();
});<|endoftext|>


<commit_before>import * as Aerial from './../aerialjs';
import { Collection } from './../collections/Collection';

Meteor.startup(function () {

    let authReq = new Aerial.AuthRequest(Config);

    authRequest = new Aerial.AuthRequest(Config);
});

Meteor.methods({

    setAuthRequestConfig: function (config) {
        //this.config = config;
    },

    getAuthRequestConfig: function () {

        var defaultConfig = {};

        return Config || this.config || defaultConfig;
    },

    createAuthRequest: function () {

        var x = new Aerial.AuthRequest(Config);
        return x;
    },

    authClient: function () {
        // Aerial.AuthRequest.authenticateClient();
    },

    authUser: function () {

        // return Aerial.AuthRequest.authenticateUser(getAuthRequestConfig());
    },

    refreshUserToken: function () {
    },

    logoutUser: function () {
        // AuthRequest.logoutUser();

        authRequest.logoutUser();
    },

    createCollection: function(name) {
       // let col =  new Aerial.Collection(name);

    }
});
<commit_msg>Refactor composr-connect methods<commit_after>import * as Aerial from './../aerialjs';
import { Collection } from './../collections/Collection';

Meteor.startup(function () {

    let authReq = new Aerial.AuthRequest(Config);

    authRequest = new Aerial.AuthRequest(Config);
});

let methods = {};

methods.setAuthRequestConfig = function (config) {

};

methods.getAuthRequestConfig = function () {

    var defaultConfig = {};
    return Config || this.config || defaultConfig;
}

methods.authValidation = function () {

};

methods.createAuthRequest = function () {
    return new Aerial.AuthRequest(Config);
};

methods.authClient = function () {

};

methods.authUser = function () {

};

methods.refreshUserToken = function () {

};

methods.logoutUser = function () {

};

methods.createCollection = function () {

};

Meteor.methods(methods);
<|endoftext|>