<commit_before>/* @flow */
import path from 'path';
import React from 'react';
import ReactDOM from 'react-dom/server';
import evaluate from 'eval';
import { match, RouterContext } from 'react-router';
import async from 'async';

import { name as packageName } from '../package.json';
import { getAllPaths, log } from './utils.js';
import { render } from './Html.js';

/**
 * All source will be compiled with babel so ES6 goes
 *
 * Usage:
 *
 *   new StaticSitePlugin({ src: 'client/routes.js', ...options }),
 *
 */

type Options = {
  src: string,
  bundle?: string,
  stylesheet?: string,
  favicon?: string,
  template?: string,
}

function StaticSitePlugin(options: Options) {
  this.options = options;
  this.render = this.options.template
    ? require(path.resolve(this.options.template))
    : render;
}

/**
 * compiler seems to be an instance of the Compiler
 * https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L143
 *
 * NOTE: renderProps.routes is always passed as an array of route elements. For
 * deeply nested routes the last element in the array is the route we want to
 * get the props of, so we grab it out of the array and use it. This lets us do
 * things like:
 *
 *   <Route title='Blah blah blah...' {...moreProps} />
 *
 * Then set the document title to the title defined on that route
 *
 * NOTE: Sometimes when matching routes we do not get an error but nore do we
 * get renderProps. In my experience this usually means we hit an IndexRedirect
 * or some form of Route that doesn't actually have a component to render. In
 * these cases we simply keep on moving and don't render anything.
 *
 * TODO:
 * - Allow passing a function for title?
 *
 */
StaticSitePlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', (compilation, cb) => {
    const asset = findAsset(this.options.src, compilation);

    if (!asset) {
      throw new Error(`Output file not found: ${this.options.src}`);
    }

    const source = evaluate(asset.source(), true);
    const Component = source.routes || source;
    log('src evaluated to Component:', Component);

    // NOTE: If Symbol(react.element) was removed this would no longer work
    if (!isValidComponent(Component)) {
      log('Component was invalid. Throwing error.');
      throw new Error(`${packageName} -- options.src entry point must export a valid React Component.`);
    }

    if (!isRoute(Component)) {
      log('Entrypoint or chunk name did not return a Route component. Rendering as individual component instead.');
      compilation.assets['index.html'] = renderSingleComponent(Component, this.options, this.render);
      return cb();
    }

    const paths = getAllPaths(Component);
    log('Parsed routes:', paths);

    async.forEach(paths,
      (location, callback) => {
        match({ routes: Component, location }, (err, redirectLocation, renderProps) => {
          // Skip if something goes wrong. See NOTE above.
          if (err || !renderProps) {
            log('Error matching route', err, renderProps);
            return callback();
          }

          const route = renderProps.routes[renderProps.routes.length - 1]; // See NOTE
          const body = ReactDOM.renderToString(<RouterContext {...renderProps} />);
          const { stylesheet, favicon, bundle } = this.options;
          const assetKey = getAssetKey(location);
          const doc = this.render({
            title: route.title,
            body,
            stylesheet,
            favicon,
            bundle,
          });

          compilation.assets[assetKey] = {
            source() { return doc; },
            size() { return doc.length; },
          };

          callback();
        });
      },
      err => {
        if (err) throw err;
        cb();
      }
    );
  });
};

/**
 * @param {string} src
 * @param {Compilation} compilation
 */
function findAsset(src, compilation) {
  const asset = compilation.assets[src];

  // Found it. It was a key within assets
  if (asset) return asset;

  // Didn't find it in assets, it must be a chunk

  const webpackStatsJson = compilation.getStats().toJson();
  let chunkValue = webpackStatsJson.assetsByChunkName[src];

  // Uh oh, couldn't find it as a chunk value either. This indicates a failure
  // to find the asset. The caller should handle a falsey value as it sees fit.
  if (!chunkValue) return null;

  // Webpack outputs an array for each chunk when using sourcemaps
  if (chunkValue instanceof Array) {
    chunkValue = chunkValue[0]; // Is the main bundle always the first element?
  }

  return compilation.assets[chunkValue];
}

/**
 * Given a string location (i.e. path) return a relevant HTML filename.
 * Ex: '/' -> 'index.html'
 * Ex: '/about' -> 'about.html'
 * Ex: '/about/' -> 'about/index.html'
 * Ex: '/about/team' -> 'about/team.html'
 *
 * NOTE: Don't want leading slash
 * i.e. 'path/to/file.html' instead of '/path/to/file.html'
 *
 * NOTE: There is a lone case where the users specifices a not found route that
 * results in a '/*' location. In this case we output 404.html, since it's
 * assumed that this is a 404 route. See the RR changelong for details:
 * https://github.com/rackt/react-router/blob/1.0.x/CHANGES.md#notfound-route
 */
function getAssetKey(location: string): string {
  const basename = path.basename(location);
  const dirname = path.dirname(location).slice(1); // See NOTE above
  let filename;

  if (!basename || location.slice(-1) === '/') {
    filename = 'index.html';
  } else if (basename === '*') {
    filename = '404.html';
  } else {
    filename = basename + '.html';
  }

  return dirname ? (dirname + path.sep + filename) : filename;
}

/**
 * Test if a React Element is a React Router Route or not. Note that this tests
 * the actual object (i.e. React Element), not a constructor. As such we
 * immediately deconstruct out the type property as that is what we want to
 * test.
 *
 * NOTE: Testing whether Component.type was an instanceof Route did not work.
 *
 * NOTE: This is a fragile test. The React Router API is notorious for
 * introducing breaking changes, so of the RR team changed the manditory path
 * and component props this would fail.
 */
function isRoute({ type: component }): boolean {
  return component && component.propTypes.path && component.propTypes.component;
}

/**
 * Test if a component is a valid React component.
 *
 * NOTE: This is a pretty wonky test. React.createElement wasn't doing it for
 * me. It seemed to be giving false positives.
 *
 * @param {any} component
 * @return {boolean}
 */
function isValidComponent(Component): boolean {
  const { type } = React.createElement(Component);
  return typeof type === 'object' || typeof type === 'function';
}

/**
 * If not provided with any React Router Routes we try to render whatever asset
 * was passed to the plugin as a React component. The use case would be anyone
 * who doesn't need/want to add RR as a dependency to their app and instead only
 * wants a single HTML file output.
 *
 * NOTE: In the case of a single component we use the static prop 'title' to get
 * the page title. If this is not provided then the title will default to
 * whatever is provided in the template.
 */
function renderSingleComponent(Component, options, render) { // eslint-disable-line no-shadow
  const body = ReactDOM.renderToString(<Component />);
  const { stylesheet, favicon, bundle } = options;
  const doc = render({
    title: Component.title, // See NOTE
    body,
    stylesheet,
    favicon,
    bundle,
  });

  return {
    source() { return doc; },
    size() { return doc.length; },
  };
}

module.exports = StaticSitePlugin;
<commit_msg>Added support for hashed file names, hash is added to all files that includes '[hash]'. Example: bundle.[hash].js<commit_after>/* @flow */
import path from 'path';
import React from 'react';
import ReactDOM from 'react-dom/server';
import evaluate from 'eval';
import { match, RouterContext } from 'react-router';
import async from 'async';

import { name as packageName } from '../package.json';
import { getAllPaths, log } from './utils.js';
import { render } from './Html.js';

/**
 * All source will be compiled with babel so ES6 goes
 *
 * Usage:
 *
 *   new StaticSitePlugin({ src: 'client/routes.js', ...options }),
 *
 */

type Options = {
  src: string,
  bundle?: string,
  stylesheet?: string,
  favicon?: string,
  template?: string,
}

function StaticSitePlugin(options: Options) {
  this.options = options;
  this.render = this.options.template
    ? require(path.resolve(this.options.template))
    : render;
}

/**
 * compiler seems to be an instance of the Compiler
 * https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L143
 *
 * NOTE: renderProps.routes is always passed as an array of route elements. For
 * deeply nested routes the last element in the array is the route we want to
 * get the props of, so we grab it out of the array and use it. This lets us do
 * things like:
 *
 *   <Route title='Blah blah blah...' {...moreProps} />
 *
 * Then set the document title to the title defined on that route
 *
 * NOTE: Sometimes when matching routes we do not get an error but nore do we
 * get renderProps. In my experience this usually means we hit an IndexRedirect
 * or some form of Route that doesn't actually have a component to render. In
 * these cases we simply keep on moving and don't render anything.
 *
 * TODO:
 * - Allow passing a function for title?
 *
 */
StaticSitePlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', (compilation, cb) => {
    const asset = findAsset(this.options.src, compilation);

    if (!asset) {
      throw new Error(`Output file not found: ${this.options.src}`);
    }

    const source = evaluate(asset.source(), true);
    const Component = source.routes || source;
    log('src evaluated to Component:', Component);

    // NOTE: If Symbol(react.element) was removed this would no longer work
    if (!isValidComponent(Component)) {
      log('Component was invalid. Throwing error.');
      throw new Error(`${packageName} -- options.src entry point must export a valid React Component.`);
    }

    if (!isRoute(Component)) {
      log('Entrypoint or chunk name did not return a Route component. Rendering as individual component instead.');
      compilation.assets['index.html'] = renderSingleComponent(Component, this.options, this.render);
      return cb();
    }

    const paths = getAllPaths(Component);
    log('Parsed routes:', paths);

    async.forEach(paths,
      (location, callback) => {
        match({ routes: Component, location }, (err, redirectLocation, renderProps) => {
          // Skip if something goes wrong. See NOTE above.
          if (err || !renderProps) {
            log('Error matching route', err, renderProps);
            return callback();
          }

          const route = renderProps.routes[renderProps.routes.length - 1]; // See NOTE
          const body = ReactDOM.renderToString(<RouterContext {...renderProps} />);
          const { stylesheet, favicon, bundle } = addHash(this.options, compilation.hash);
          const assetKey = getAssetKey(location);
          const doc = this.render({
            title: route.title,
            body,
            stylesheet,
            favicon,
            bundle,
          });

          compilation.assets[assetKey] = {
            source() { return doc; },
            size() { return doc.length; },
          };

          callback();
        });
      },
      err => {
        if (err) throw err;
        cb();
      }
    );
  });
};

/**
 * @param {string} src
 * @param {Compilation} compilation
 */
function findAsset(src, compilation) {
  const asset = compilation.assets[src];

  // Found it. It was a key within assets
  if (asset) return asset;

  // Didn't find it in assets, it must be a chunk

  const webpackStatsJson = compilation.getStats().toJson();
  let chunkValue = webpackStatsJson.assetsByChunkName[src];

  // Uh oh, couldn't find it as a chunk value either. This indicates a failure
  // to find the asset. The caller should handle a falsey value as it sees fit.
  if (!chunkValue) return null;

  // Webpack outputs an array for each chunk when using sourcemaps
  if (chunkValue instanceof Array) {
    chunkValue = chunkValue[0]; // Is the main bundle always the first element?
  }

  return compilation.assets[chunkValue];
}

/**
 * Add hash to all options that includes '[hash]' ex: bundle.[hash].js
 * NOTE: Only one hash for all files. So even if the css did not change it will get a new hash if the js changed.
 *
 * @param {Options} options
 * @param {string}  hash
 */
function addHash(options, hash) {

  return Object.keys(options).reduce((previous, current) => {
    previous[current] = (options[current] && hash ? options[current].replace('[hash]', hash) : options[current]);
    return previous;
  }, {});

}

/**
 * Given a string location (i.e. path) return a relevant HTML filename.
 * Ex: '/' -> 'index.html'
 * Ex: '/about' -> 'about.html'
 * Ex: '/about/' -> 'about/index.html'
 * Ex: '/about/team' -> 'about/team.html'
 *
 * NOTE: Don't want leading slash
 * i.e. 'path/to/file.html' instead of '/path/to/file.html'
 *
 * NOTE: There is a lone case where the users specifices a not found route that
 * results in a '/*' location. In this case we output 404.html, since it's
 * assumed that this is a 404 route. See the RR changelong for details:
 * https://github.com/rackt/react-router/blob/1.0.x/CHANGES.md#notfound-route
 */
function getAssetKey(location: string): string {
  const basename = path.basename(location);
  const dirname = path.dirname(location).slice(1); // See NOTE above
  let filename;

  if (!basename || location.slice(-1) === '/') {
    filename = 'index.html';
  } else if (basename === '*') {
    filename = '404.html';
  } else {
    filename = basename + '.html';
  }

  return dirname ? (dirname + path.sep + filename) : filename;
}

/**
 * Test if a React Element is a React Router Route or not. Note that this tests
 * the actual object (i.e. React Element), not a constructor. As such we
 * immediately deconstruct out the type property as that is what we want to
 * test.
 *
 * NOTE: Testing whether Component.type was an instanceof Route did not work.
 *
 * NOTE: This is a fragile test. The React Router API is notorious for
 * introducing breaking changes, so of the RR team changed the manditory path
 * and component props this would fail.
 */
function isRoute({ type: component }): boolean {
  return component && component.propTypes.path && component.propTypes.component;
}

/**
 * Test if a component is a valid React component.
 *
 * NOTE: This is a pretty wonky test. React.createElement wasn't doing it for
 * me. It seemed to be giving false positives.
 *
 * @param {any} component
 * @return {boolean}
 */
function isValidComponent(Component): boolean {
  const { type } = React.createElement(Component);
  return typeof type === 'object' || typeof type === 'function';
}

/**
 * If not provided with any React Router Routes we try to render whatever asset
 * was passed to the plugin as a React component. The use case would be anyone
 * who doesn't need/want to add RR as a dependency to their app and instead only
 * wants a single HTML file output.
 *
 * NOTE: In the case of a single component we use the static prop 'title' to get
 * the page title. If this is not provided then the title will default to
 * whatever is provided in the template.
 */
function renderSingleComponent(Component, options, render) { // eslint-disable-line no-shadow
  const body = ReactDOM.renderToString(<Component />);
  const { stylesheet, favicon, bundle } = options;
  const doc = render({
    title: Component.title, // See NOTE
    body,
    stylesheet,
    favicon,
    bundle,
  });

  return {
    source() { return doc; },
    size() { return doc.length; },
  };
}

module.exports = StaticSitePlugin;
<|endoftext|>


<commit_before>'use strict';

class BetDealer {
  addListener () {
    console.log('Listen incoming messages.');
  }

  sendAnswer (message, id, sendResponse) {
    sendResponse({
      err: false,
      value: 'ok'
    });
  }
}

export default BetDealer;
<commit_msg>rename<commit_after>'use strict';

class BetDealer {
  addListener () {
    console.log('Listen incoming messages.');
  }

  processMessage (message, id, sendResponse) {
    sendResponse({
      err: false,
      value: 'ok'
    });
  }
}

export default BetDealer;
<|endoftext|>


<commit_before>
// inspired by https://github.com/NYTimes/Emphasis

import Sha1 from 'sha.js/sha1';
import sbd from 'sbd';
import shortid from 'shortid';
import levenshtein from 'fast-levenshtein';
import escapeRegex from 'escape-regex-string';

const TEXT_NODE = 3;
const SHOW_TEXT = 4;
export let citeable = ['P', 'LI', 'DD', 'DT', 'BLOCKQUOTE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
                       'FIGCAPTION', 'CAPTION', 'ASIDE']
;

/**
 * From a block element, generate a Key
 * - Break block element text content into Sentences
 * - Take first and last Sentences
 * - Sometimes the same. That's ok.
 * - First character from the first three words of each sentence
 * - Each 6 char key refers to specific block element
 */
export function createKey ($el) {
  let key = '';
  let len = 6;
  let txt = ($el.textContent || '').replace(/[^\w\. ]+/gui, '').trim();

  if (txt && txt.length > 1) {
    let lines = sbd.sentences(txt).map(x => x.trim()).filter(x => x);
    if (lines.length) {
      let first = lines[0].match(/\S+/gu).slice(0, (len/2));
      let last = lines[lines.length-1].match(/\S+/gu).slice(0, (len/2));
      let k = first.concat(last);

      let max = (k.length > len) ? len : k.length;

      for (var i=0; i < max; i++) {
        key += k[i].substring(0, 1);
      }
    }
  }

  return key;
}

// create a sha1 hash for the trimmed content of the given element
export function createHash ($el) {
  let sha1 = new Sha1();
  // TODO: textContent.replace(/\s+/g, ' ') ??
  return sha1.update($el.textContent.trim(), 'utf8').digest('hex');
}

// given a range, find the enclosing block element that is part of our whitelist
export function getScope (range) {
  var $scope = range.commonAncestorContainer;
  if ($scope.nodeType === TEXT_NODE) {
    $scope = $scope.parentElement; //get closest Element
  };

  // get closest citeable element
  while (!~citeable.indexOf($scope.tagName)) {
    $scope = $scope.parentElement;
    if ($scope.tagName === 'HTML') {
      return;
    }
  }
  return $scope;
}

// given a range and a scope, returns a data structure with all the details needed to reconstruct it
export function serializeRange (range, $scope = getScope(range)) {
  if (!$scope) return;

  let offsets = getOffsets(range, $scope);

  return {
    $scope: $scope,
    sha1: createHash($scope),
    key: createKey($scope),
    startOffset: offsets.startOffset,
    endOffset: offsets.endOffset,
    text: range.toString().trim()
  };
}

// serialise the selection as a range
export function serializeSelection () {
  let selection = window.getSelection();
  if (!selection.isCollapsed) return serializeRange(selection.getRangeAt(0));
}

// given a scope and start/end offsets that ignore white space, returns a range that takes the
// white space into account and captures that content
export function rangeFromOffsets ($scope, startOffset, endOffset) {
  let node
  ,   it = document.createNodeIterator($scope, SHOW_TEXT)
  ,   acc = 0
  ,   startNode, endNode, relStartOffset, relEndOffset
  ;

  while (node = it.nextNode()) {
    let textContent = node.textContent.trim();
    if (relStartOffset === undefined && ((acc + textContent.length) >= startOffset)) {
      startNode = node;
      // we add back the effect of having trimmed the textContent
      relStartOffset = Math.max(0, startOffset - acc + (startNode.textContent.length -
                                                        startNode.textContent.replace(/^\s+/, '').length));
    }
    if (relEndOffset === undefined && ((acc + textContent.length) >= endOffset)) {
      endNode = node;
      // ditto
      relEndOffset = Math.max(0, endOffset - acc + (endNode.textContent.length -
                                                    endNode.textContent.replace(/^\s+/, '').length));
      break;
    }
    acc += textContent.length;
  }

  var range = document.createRange();
  range.setStart(startNode, relStartOffset);
  range.setEnd(endNode, relEndOffset);

  return range;
};

// given a target key and a list of candidates, find either an exact match, or one with the smallest
// Levenshtein edit distance. If no candidate has an edit distance less than three, the match is
// undefined.
export function findKey (target, candidates) {
  let x = { index: undefined, value: undefined, lev: undefined };

  for (let i = 0; i < candidates.length; i++) {
    if (target === candidates[i]) {
      return { index: i, value: candidates[i], lev: 0 };
    }
    // look for 1st closest Match
    else {
      let ls = levenshtein.get(target.slice(0, 3), candidates[i].slice(0, 3));
      let le = levenshtein.get(target.slice(-3), candidates[i].slice(-3));
      let lev = ls + le;
      if (lev < 3 && ((x.lev === undefined) || (lev < x.lev)) ) {
        x.index = i;
        x.value = candidates[i];
        x.lev = lev;
      }
    }
  }

  return x;
};

// Given a document object, add data-id, data-hash, and data-key to all citeables.
// Only call this if you have control over the document's lifecycle.
export function addIdentifiers ($doc) {
  Array.prototype.forEach.call($doc.body.getElementsByTagName('*'), $el => {
    if (~citeable.indexOf($el.tagName)) {
      $el.setAttribute('data-id', shortid.generate());
      $el.setAttribute('data-hash', createHash($el));
      $el.setAttribute('data-key', createKey($el));
    }
  });
  return $doc;
};

function textNodeFromNode (container) {
    if (container.nodeType === TEXT_NODE) return container;
    for (let i = 0; i < container.childNodes.length; i++) {
      if (container.childNodes[i].nodeType === TEXT_NODE) return container.childNodes[i];
  }
}

function trimmedLength (nodes, index) {
  return nodes.slice(0, index).reduce(function (a, b) {
    return a + b.textContent.trim().length;
  }, 0);
}

// Given a range and an element scope, return the start and end offsets into the text that ignore
// white space.
export function getOffsets (range, $scope) {
  let startTextNode = textNodeFromNode(range.startContainer)
  ,   endTextNode = textNodeFromNode(range.endContainer)
  ;

  let node, indStartTextNode, indEndTextNode, ind = 0, textNodes = []
  ,   it = document.createNodeIterator($scope, SHOW_TEXT);
  while (node = it.nextNode()) {
    textNodes.push(node);
    if (node === startTextNode) {
      indStartTextNode = ind;
    }
    if (node === endTextNode) {
      indEndTextNode = ind;
      break;
    }
    ind++;
  }

  // get the offset without taking white space into account
  let trimmedOffset = (nodes, index, rangeOffset, anchorNode) => {
      let baseOffset = trimmedLength(nodes, index);
      // we subtract the effect of having trimmed the textContent
      if (rangeOffset !== undefined) {
        baseOffset += rangeOffset - (anchorNode.textContent.length -
                                     anchorNode.textContent.replace(/^\s+/, '').length);
      }
      return Math.max(0, baseOffset);
    }
  , startOffset = trimmedOffset(textNodes, indStartTextNode, range.startOffset, startTextNode)
  , endOffset = trimmedOffset(textNodes, indEndTextNode, range.endOffset, endTextNode)
  ;

  return { startOffset: startOffset, endOffset: endOffset };
}

export function getChildOffsets ($parent, $child) {
  let startTextNode = textNodeFromNode($child);

  let node, indStartTextNode, ind = 0, textNodes = [];
  let it = document.createNodeIterator($parent, SHOW_TEXT);
  while (node = it.nextNode()) {
    textNodes.push(node);
    if (node === startTextNode) {
      indStartTextNode = ind;
      break;
    }
    ind++;
  }

  let startOffset = trimmedLength(textNodes, indStartTextNode);
  let endOffset = trimmedLength(textNodes, indStartTextNode + 1);
  return { startOffset: startOffset, endOffset: endOffset };
};

// given a scope and a string, it will find all instances of that string within the
// scope, and use that to create white-space-independent ranges
export function getRangesFromText ($scope, text) {
  text = text.trim();
  let re = new RegExp(escapeRegex(text), 'gi')
    , tc = $scope.textContent
  ;
  //  We need to get an offset that ignores whitespace, BUT the regex needs to match
  //  if it contains whitespace.
  // So we:
  //  get all match indices while trimming *nothing*;
  //  then, for each index, remove the amount of WS that is *before* it (but only before).

  let result, matchIndexes = [];
  while ((result = re.exec(tc)) !== null) matchIndexes.push(result.index);

  let noWSTextLength = text.replace(/\s+/g, '').length;
  return matchIndexes.map(function (index) {
    index -= (tc.substr(0, index).match(/\s/) || []).length;
    return rangeFromOffsets($scope, index, index + noWSTextLength);
  });
};

if (typeof window === 'object') window.WebVerse = exports;
<commit_msg>add a few more elements<commit_after>
// inspired by https://github.com/NYTimes/Emphasis

import Sha1 from 'sha.js/sha1';
import sbd from 'sbd';
import shortid from 'shortid';
import levenshtein from 'fast-levenshtein';
import escapeRegex from 'escape-regex-string';

const TEXT_NODE = 3;
const SHOW_TEXT = 4;
export let citeable = ['P', 'LI', 'DD', 'DT', 'BLOCKQUOTE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
                       'FIGCAPTION', 'CAPTION', 'ASIDE', 'SECTION', 'ARTICLE', 'BODY', 'DIV', 'MAIN']
;

/**
 * From a block element, generate a Key
 * - Break block element text content into Sentences
 * - Take first and last Sentences
 * - Sometimes the same. That's ok.
 * - First character from the first three words of each sentence
 * - Each 6 char key refers to specific block element
 */
export function createKey ($el) {
  let key = '';
  let len = 6;
  let txt = ($el.textContent || '').replace(/[^\w\. ]+/gui, '').trim();

  if (txt && txt.length > 1) {
    let lines = sbd.sentences(txt).map(x => x.trim()).filter(x => x);
    if (lines.length) {
      let first = lines[0].match(/\S+/gu).slice(0, (len/2));
      let last = lines[lines.length-1].match(/\S+/gu).slice(0, (len/2));
      let k = first.concat(last);

      let max = (k.length > len) ? len : k.length;

      for (var i=0; i < max; i++) {
        key += k[i].substring(0, 1);
      }
    }
  }

  return key;
}

// create a sha1 hash for the trimmed content of the given element
export function createHash ($el) {
  let sha1 = new Sha1();
  // TODO: textContent.replace(/\s+/g, ' ') ??
  return sha1.update($el.textContent.trim(), 'utf8').digest('hex');
}

// given a range, find the enclosing block element that is part of our whitelist
export function getScope (range) {
  var $scope = range.commonAncestorContainer;
  if ($scope.nodeType === TEXT_NODE) {
    $scope = $scope.parentElement; //get closest Element
  };

  // get closest citeable element
  while (!~citeable.indexOf($scope.tagName)) {
    $scope = $scope.parentElement;
    if ($scope.tagName === 'HTML') {
      return;
    }
  }
  return $scope;
}

// given a range and a scope, returns a data structure with all the details needed to reconstruct it
export function serializeRange (range, $scope = getScope(range)) {
  if (!$scope) return;

  let offsets = getOffsets(range, $scope);

  return {
    $scope: $scope,
    sha1: createHash($scope),
    key: createKey($scope),
    startOffset: offsets.startOffset,
    endOffset: offsets.endOffset,
    text: range.toString().trim()
  };
}

// serialise the selection as a range
export function serializeSelection () {
  let selection = window.getSelection();
  if (!selection.isCollapsed) return serializeRange(selection.getRangeAt(0));
}

// given a scope and start/end offsets that ignore white space, returns a range that takes the
// white space into account and captures that content
export function rangeFromOffsets ($scope, startOffset, endOffset) {
  let node
  ,   it = document.createNodeIterator($scope, SHOW_TEXT)
  ,   acc = 0
  ,   startNode, endNode, relStartOffset, relEndOffset
  ;

  while (node = it.nextNode()) {
    let textContent = node.textContent.trim();
    if (relStartOffset === undefined && ((acc + textContent.length) >= startOffset)) {
      startNode = node;
      // we add back the effect of having trimmed the textContent
      relStartOffset = Math.max(0, startOffset - acc + (startNode.textContent.length -
                                                        startNode.textContent.replace(/^\s+/, '').length));
    }
    if (relEndOffset === undefined && ((acc + textContent.length) >= endOffset)) {
      endNode = node;
      // ditto
      relEndOffset = Math.max(0, endOffset - acc + (endNode.textContent.length -
                                                    endNode.textContent.replace(/^\s+/, '').length));
      break;
    }
    acc += textContent.length;
  }

  var range = document.createRange();
  range.setStart(startNode, relStartOffset);
  range.setEnd(endNode, relEndOffset);

  return range;
};

// given a target key and a list of candidates, find either an exact match, or one with the smallest
// Levenshtein edit distance. If no candidate has an edit distance less than three, the match is
// undefined.
export function findKey (target, candidates) {
  let x = { index: undefined, value: undefined, lev: undefined };

  for (let i = 0; i < candidates.length; i++) {
    if (target === candidates[i]) {
      return { index: i, value: candidates[i], lev: 0 };
    }
    // look for 1st closest Match
    else {
      let ls = levenshtein.get(target.slice(0, 3), candidates[i].slice(0, 3));
      let le = levenshtein.get(target.slice(-3), candidates[i].slice(-3));
      let lev = ls + le;
      if (lev < 3 && ((x.lev === undefined) || (lev < x.lev)) ) {
        x.index = i;
        x.value = candidates[i];
        x.lev = lev;
      }
    }
  }

  return x;
};

// Given a document object, add data-id, data-hash, and data-key to all citeables.
// Only call this if you have control over the document's lifecycle.
export function addIdentifiers ($doc) {
  Array.prototype.forEach.call($doc.body.getElementsByTagName('*'), $el => {
    if (~citeable.indexOf($el.tagName)) {
      $el.setAttribute('data-id', shortid.generate());
      $el.setAttribute('data-hash', createHash($el));
      $el.setAttribute('data-key', createKey($el));
    }
  });
  return $doc;
};

function textNodeFromNode (container) {
    if (container.nodeType === TEXT_NODE) return container;
    for (let i = 0; i < container.childNodes.length; i++) {
      if (container.childNodes[i].nodeType === TEXT_NODE) return container.childNodes[i];
  }
}

function trimmedLength (nodes, index) {
  return nodes.slice(0, index).reduce(function (a, b) {
    return a + b.textContent.trim().length;
  }, 0);
}

// Given a range and an element scope, return the start and end offsets into the text that ignore
// white space.
export function getOffsets (range, $scope) {
  let startTextNode = textNodeFromNode(range.startContainer)
  ,   endTextNode = textNodeFromNode(range.endContainer)
  ;

  let node, indStartTextNode, indEndTextNode, ind = 0, textNodes = []
  ,   it = document.createNodeIterator($scope, SHOW_TEXT);
  while (node = it.nextNode()) {
    textNodes.push(node);
    if (node === startTextNode) {
      indStartTextNode = ind;
    }
    if (node === endTextNode) {
      indEndTextNode = ind;
      break;
    }
    ind++;
  }

  // get the offset without taking white space into account
  let trimmedOffset = (nodes, index, rangeOffset, anchorNode) => {
      let baseOffset = trimmedLength(nodes, index);
      // we subtract the effect of having trimmed the textContent
      if (rangeOffset !== undefined) {
        baseOffset += rangeOffset - (anchorNode.textContent.length -
                                     anchorNode.textContent.replace(/^\s+/, '').length);
      }
      return Math.max(0, baseOffset);
    }
  , startOffset = trimmedOffset(textNodes, indStartTextNode, range.startOffset, startTextNode)
  , endOffset = trimmedOffset(textNodes, indEndTextNode, range.endOffset, endTextNode)
  ;

  return { startOffset: startOffset, endOffset: endOffset };
}

export function getChildOffsets ($parent, $child) {
  let startTextNode = textNodeFromNode($child);

  let node, indStartTextNode, ind = 0, textNodes = [];
  let it = document.createNodeIterator($parent, SHOW_TEXT);
  while (node = it.nextNode()) {
    textNodes.push(node);
    if (node === startTextNode) {
      indStartTextNode = ind;
      break;
    }
    ind++;
  }

  let startOffset = trimmedLength(textNodes, indStartTextNode);
  let endOffset = trimmedLength(textNodes, indStartTextNode + 1);
  return { startOffset: startOffset, endOffset: endOffset };
};

// given a scope and a string, it will find all instances of that string within the
// scope, and use that to create white-space-independent ranges
export function getRangesFromText ($scope, text) {
  text = text.trim();
  let re = new RegExp(escapeRegex(text), 'gi')
    , tc = $scope.textContent
  ;
  //  We need to get an offset that ignores whitespace, BUT the regex needs to match
  //  if it contains whitespace.
  // So we:
  //  get all match indices while trimming *nothing*;
  //  then, for each index, remove the amount of WS that is *before* it (but only before).

  let result, matchIndexes = [];
  while ((result = re.exec(tc)) !== null) matchIndexes.push(result.index);

  let noWSTextLength = text.replace(/\s+/g, '').length;
  return matchIndexes.map(function (index) {
    index -= (tc.substr(0, index).match(/\s/) || []).length;
    return rangeFromOffsets($scope, index, index + noWSTextLength);
  });
};

if (typeof window === 'object') window.WebVerse = exports;
<|endoftext|>


<commit_before>import {pick,omit,clone,assign} from "lodash";
import PouchDB from "pouchdb";
import * as methods from "./methods";
import {get as getMode} from "./modes/index.js";
import * as utils from "./utils/index.js";
import extend from "backbone-extend-standalone";

// fixes http adapter's need to use the prefix value
PouchDB.adapters.http.use_prefix = false;

export default function(baseUrl, defaultOpts, callback) {
	if (typeof baseUrl === "object") {
		[callback,defaultOpts,baseUrl] = [defaultOpts,baseUrl,void 0];
	} else if (typeof defaultOpts === "function") {
		[callback,defaultOpts] = [defaultOpts,void 0];
	}

	defaultOpts = clone(defaultOpts || {});
	if (baseUrl) defaultOpts.baseUrl = baseUrl;
	defaultOpts = utils.parseOptions(defaultOpts, {
		authmode: "basic",
		ajax: { headers: {} }
	});

	let auth = defaultOpts.auth;
	delete defaultOpts.auth;

	let PouchAlt = PouchDB.defaults();

	function CouchDB(name, opts, callback) {
		if (!(this instanceof CouchDB)) {
			return new CouchDB(name, opts, callback);
		}

		if (typeof opts === "function" || typeof opts === "undefined") {
			callback = opts;
			opts = {};
		}

		if (name && typeof name === 'object') {
			opts = name;
			name = undefined;
		}

		opts = assign({}, defaultOpts, opts);
		opts.adapter = "http";
		if (!opts.getHost) opts.getHost = utils.prefixHost(opts.baseUrl);

		if (typeof callback !== "function") callback = (e)=>{ if (e) throw e; };
		this._auth_mode = CouchDB._auth_mode;
		let p = [];

		if (opts.authmode !== defaultOpts.authmode || opts.auth) {
			this._auth_mode = getMode(opts.authmode || defaultOpts.authmode, CouchDB);
		}

		PouchDB.call(this, name, omit(opts, "authmode", "auth"), function(err, res) {
			if (err) return callback(err);
			utils.callbackify(Promise.all(p), (e) => callback(e, res));
		});

		// PouchDB uses an excessive number of clones on the options value
		// this makes it impossible to hack before we pass it the PouchDB constructor
		// so instead we hack the headers value right after we set up, but
		// still synchronously with the constructor
		if (this._auth_mode !== CouchDB._auth_mode) {
			assign(this, pick(methods, "_applyModeMethod", "signIn", "signOut"));
			p.push(this._applyModeMethod("setup", [ opts.auth, this.getHeaders() ]));
		}
	}

	CouchDB.prototype = Object.create(PouchAlt.prototype);
	CouchDB.prototype.constructor = CouchDB;
	assign(CouchDB, PouchAlt, methods);

	CouchDB.baseUrl = defaultOpts.baseUrl;
	CouchDB._auth_mode = getMode(defaultOpts.authmode, CouchDB);
	CouchDB.request = utils.makeRequest(defaultOpts.ajax);
	CouchDB.users = new CouchDB("_users");

	CouchDB.extend = extend;
	CouchDB.defaults = defaults;

	let p = [];
	p.push(CouchDB._applyModeMethod("setup", [ auth, defaultOpts.ajax.headers ]));

	Promise.all(p).then(() => {
		if (callback) callback(null, CouchDB);
	}, (e) => {
		if (callback) callback(e);
		throw e;
	}).catch((e) => process.nextTick(() => {
		// we don't want to auto-throw errors if something is going to catch it
		// but we still definitely want the error event to run
		if (!callback || CouchDB.listenerCount("error")) CouchDB.emit("error", e);
	}));

	return CouchDB;
}

function defaults(defaultOpts) {
	let CouchDB = this;
	let CouchAlt = this.extend({
		constructor: function(name, opts, callback) {
			if (!(this instanceof CouchAlt)) {
				return new CouchAlt(name, opts, callback);
			}

			if (typeof opts === 'function' || typeof opts === 'undefined') {
				callback = opts;
				opts = {};
			}
			if (name && typeof name === 'object') {
				opts = name;
				name = undefined;
			}

			opts = assign({}, defaultOpts, opts);
			CouchDB.call(this, name, opts, callback);
		}
	});

	return CouchAlt;
}
<commit_msg>feat: added a setup method<commit_after>import {pick,omit,clone,assign} from "lodash";
import PouchDB from "pouchdb";
import * as methods from "./methods";
import {get as getMode} from "./modes/index.js";
import * as utils from "./utils/index.js";
import extend from "backbone-extend-standalone";

// fixes http adapter's need to use the prefix value
PouchDB.adapters.http.use_prefix = false;

export default function(baseUrl, defaultOpts, callback) {
	if (typeof baseUrl === "object") {
		[callback,defaultOpts,baseUrl] = [defaultOpts,baseUrl,void 0];
	} else if (typeof defaultOpts === "function") {
		[callback,defaultOpts] = [defaultOpts,void 0];
	}

	defaultOpts = clone(defaultOpts || {});
	if (baseUrl) defaultOpts.baseUrl = baseUrl;
	defaultOpts = utils.parseOptions(defaultOpts, {
		authmode: "basic",
		ajax: { headers: {} }
	});

	let auth = defaultOpts.auth;
	delete defaultOpts.auth;

	let PouchAlt = PouchDB.defaults();

	function CouchDB(name, opts, callback) {
		if (!(this instanceof CouchDB)) {
			return new CouchDB(name, opts, callback);
		}

		if (typeof opts === "function" || typeof opts === "undefined") {
			callback = opts;
			opts = {};
		}

		if (name && typeof name === 'object') {
			opts = name;
			name = undefined;
		}

		opts = assign({}, defaultOpts, opts);
		opts.adapter = "http";
		if (!opts.getHost) opts.getHost = utils.prefixHost(opts.baseUrl);

		if (typeof callback !== "function") callback = (e)=>{ if (e) throw e; };
		this._auth_mode = CouchDB._auth_mode;
		let p = [];

		if (opts.authmode !== defaultOpts.authmode || opts.auth) {
			this._auth_mode = getMode(opts.authmode || defaultOpts.authmode, CouchDB);
		}

		PouchDB.call(this, name, omit(opts, "authmode", "auth"), function(err, res) {
			if (err) return callback(err);
			utils.callbackify(Promise.all(p), (e) => callback(e, res));
		});

		// PouchDB uses an excessive number of clones on the options value
		// this makes it impossible to hack before we pass it the PouchDB constructor
		// so instead we hack the headers value right after we set up, but
		// still synchronously with the constructor
		if (this._auth_mode !== CouchDB._auth_mode) {
			assign(this, pick(methods, "_applyModeMethod", "signIn", "signOut"));
			p.push(this._applyModeMethod("setup", [ opts.auth, this.getHeaders() ]));
		}
	}

	CouchDB.prototype = Object.create(PouchAlt.prototype);
	CouchDB.prototype.constructor = CouchDB;
	assign(CouchDB, PouchAlt, methods);

	CouchDB.baseUrl = defaultOpts.baseUrl;
	CouchDB._auth_mode = getMode(defaultOpts.authmode, CouchDB);
	CouchDB.request = utils.makeRequest(defaultOpts.ajax);
	CouchDB.users = new CouchDB("_users");

	CouchDB.extend = extend;
	CouchDB.defaults = defaults;

	let p = [];
	p.push(CouchDB._applyModeMethod("setup", [ auth, defaultOpts.ajax.headers ]));

	let setupPromise = Promise.all(p).then(() => {
		if (callback) callback(null, CouchDB);
	}, (e) => {
		if (callback) callback(e);
		throw e;
	}).catch((e) => process.nextTick(() => {
		// we don't want to auto-throw errors if something is going to catch it
		// but we still definitely want the error event to run
		if (!callback || CouchDB.listenerCount("error")) CouchDB.emit("error", e);
	}));

	CouchDB.setup = () => setupPromise;
	return CouchDB;
}

function defaults(defaultOpts) {
	let CouchDB = this;
	let CouchAlt = this.extend({
		constructor: function(name, opts, callback) {
			if (!(this instanceof CouchAlt)) {
				return new CouchAlt(name, opts, callback);
			}

			if (typeof opts === 'function' || typeof opts === 'undefined') {
				callback = opts;
				opts = {};
			}
			if (name && typeof name === 'object') {
				opts = name;
				name = undefined;
			}

			opts = assign({}, defaultOpts, opts);
			CouchDB.call(this, name, opts, callback);
		}
	});

	return CouchAlt;
}
<|endoftext|>


<commit_before>'use strict';

var url = require('url');
var zlib = require('zlib');

var _ = require('./helpers');

module.exports = function(options, callback){

  var callbackDone = false,
      httpProtocol = options.url.indexOf('https') === 0 ? 'https' : 'http',
      requestData = url.parse(options.url),
      method = options.method || 'get',
      isJson = options.json || false,
      headers = options.headers || {},
      isPost = method === 'post',
      postBody = isPost ? JSON.stringify(options.body) : null,
      contentLength = !!postBody ? Buffer.byteLength(postBody) : null,
      timeout = options.timeout || 5,
      setHeader = function(v, k){ requestData.headers[k] = v; };

  var respond = function(statusCode, body){
    body = body.toString('utf-8');

    var error = statusCode !== 200 ? statusCode : null,
        response;

    if(isJson){
      try {
        callback(error, JSON.parse(body));
      } catch(e){
        return callback('json parsing error');
      }
    } else {
      callback(error, body);
    }
  };

  requestData.headers = {};
  requestData.method = method;

  _.each(headers, setHeader);
  setHeader('gzip', 'accept-encoding');

  if(isPost){
    setHeader(contentLength, 'content-length');
    setHeader('application/json', 'content-type');
  }

  var timer = setTimeout(function() {
    if(!callbackDone){
      callbackDone = true;
      return callback('timeout');
    }
  }, 1000 * timeout);

  var req = require(httpProtocol).request(requestData).on('response', function(response) {

    var body = [];

    response.on('data', function(chunk){
      body.push(chunk);
    }).on('end', function(){
      body = Buffer.concat(body);
      if(!callbackDone){
        clearTimeout(timer);
        callbackDone = true;

        if(response.headers['content-encoding'] === 'gzip'){
          zlib.gunzip(body, function(err, dezipped) {
            if(!!err){ return callback(err); }
            respond(response.statusCode, dezipped);
          });
        } else {
          respond(response.statusCode, body);
        }
      }
    });
  }).on('error', function(e){
    if(!callbackDone){
      clearTimeout(timer);
      callbackDone = true;
      callback(e);
    }
  });

  if(isPost){
    req.write(postBody);
  }

  req.end();
};<commit_msg>Using native request timeout handler<commit_after>'use strict';

var url = require('url');
var zlib = require('zlib');

var _ = require('./helpers');

module.exports = function(options, callback){

  var callbackDone = false,
      httpProtocol = options.url.indexOf('https') === 0 ? 'https' : 'http',
      requestData = url.parse(options.url),
      method = options.method || 'get',
      isJson = options.json || false,
      headers = options.headers || {},
      isPost = method === 'post',
      postBody = isPost ? JSON.stringify(options.body) : null,
      contentLength = !!postBody ? Buffer.byteLength(postBody) : null,
      timeout = options.timeout || 5,
      setHeader = function(v, k){ requestData.headers[k] = v; };

  var respond = function(statusCode, body){
    body = body.toString('utf-8');

    var error = statusCode !== 200 ? statusCode : null,
        response;

    if(isJson){
      try {
        callback(error, JSON.parse(body));
      } catch(e){
        return callback('json parsing error');
      }
    } else {
      callback(error, body);
    }
  };

  requestData.headers = {};
  requestData.method = method;

  _.each(headers, setHeader);
  setHeader('gzip', 'accept-encoding');

  if(isPost){
    setHeader(contentLength, 'content-length');
    setHeader('application/json', 'content-type');
  }

  var req = require(httpProtocol).request(requestData).on('response', function(response) {

    var body = [];

    response.on('data', function(chunk){
      body.push(chunk);
    }).on('end', function(){
      body = Buffer.concat(body);
      if(!callbackDone){
        callbackDone = true;

        if(response.headers['content-encoding'] === 'gzip'){
          zlib.gunzip(body, function(err, dezipped) {
            if(!!err){ return callback(err); }
            respond(response.statusCode, dezipped);
          });
        } else {
          respond(response.statusCode, body);
        }
      }
    });
  }).on('error', function(e){
    if(!callbackDone){
      callbackDone = true;
      callback(e);
    }
  }).setTimeout(1000 * timeout, function(){
    if(!callbackDone){
      callbackDone = true;
      callback('timeout');
    }
  });

  if(isPost){
    req.write(postBody);
  }

  req.end();
};
<|endoftext|>


<commit_before>import _ from 'lodash';
import del from 'del';
import babel from 'gulp-babel';
import sourcemaps from 'gulp-sourcemaps';
import mocha from 'gulp-spawn-mocha';
import glob from 'globby';
import plumber from 'gulp-plumber';
import chalk from 'chalk';
import {
    lintFiles,
    getFormatter
} from 'canonical';

import logEvents from './logEvents';
import gutil from 'gulp-util';
import prettyjson from 'prettyjson';
// import stackTrace from 'stack-trace';

import createTaskCraetor from './createTaskCreator';

/**
 * @typedef {Object} options
 * @property {string} prefix Used to prefix all pragmatist tasks.
 * @property {boolean} forceLogging Forces gulp logs for task start time and completion time.
 */

/**
 * @param {Object} gulp
 * @param {options} options
 * @returns {undefined}
 */
export default (gulp, options = {}) => {
    let babelConfig,
        config,
        plumberHandler,
        taskCreator,
        taskError,
        watching;

    watching = false;

    taskCreator = createTaskCraetor(gulp, {
        preTask: (taskName) => {
            if (taskError) {
                gutil.log(chalk.red('Skipping') + ' task ' + chalk.cyan(taskName) + ' due to an earlier error.');

                return false;
            }

            return true;
        },
        prefix: options.prefix
    });

    plumberHandler = () => {
        return plumber({
            errorHandler (error) {
                let errorPrint;

                taskError = true;

                errorPrint = error;

                if (error.message) {
                    // console.log('error', error.stack);

                    errorPrint = {
                        message: error.message,
                        name: error.name,
                        plugin: error.plugin
                    };

                    /* stack: _.map(stackTrace.parse(error), (crumb) => {
                        return crumb.fileName + ':' + crumb.lineNumber + ':' + crumb.columnNumber;
                    }) */

                    /* eslint-disable no-underscore-dangle */
                    if (error._babel && error.codeFrame) {
                    /* eslint-enable no-underscore-dangle */
                        errorPrint.code = {
                            file: error.fileName + ':' + error.loc.line + ':' + error.loc.column,
                            frame: error.codeFrame
                        };

                        if (errorPrint.message.indexOf(error.fileName + ': ') === 0) {
                            errorPrint.message = errorPrint.message.substr(error.fileName.length + 2);
                        }
                    }
                }

                gutil.log('\n\n' + prettyjson.render(errorPrint) + '\n');

                if (watching) {
                    this.emit('end');
                }
            }
        });
    };

    config = _.assign({}, {
        forceLogging: false,
        prefix: 'pragmatist:'
    }, options);

    babelConfig = {
        plugins: [
            require.resolve('babel-plugin-lodash-modularize'),
            require.resolve('babel-plugin-add-module-exports')
        ],
        presets: [
            require.resolve('babel-preset-stage-0'),
            require.resolve('babel-preset-react')
        ],
        babelrc: false,
        extends: __dirname + '/babelrc.json'
    };

    if (config.types) {
        babelConfig.plugins.unshift(require.resolve('babel-plugin-typecheck'));
    }

    if (config.browser) {
        babelConfig.presets.unshift(require.resolve('babel-preset-es2015'));
        // https://phabricator.babeljs.io/T6719#68505
        // babelConfig.plugins.unshift(require.resolve('babel-plugin-transform-class-properties'));
    } else {
        babelConfig.plugins.push(require.resolve('babel-plugin-transform-es2015-modules-commonjs'));
        babelConfig.plugins.push(require.resolve('babel-plugin-transform-es2015-parameters'));
    }

    if (config.forceLogging) {
        logEvents(gulp);
    }

    taskCreator('lint', () => {
        return glob([
            './src/**/*.js',
            './tests/**/*.js',
            './src/**/*.css',
            './src/**/*.scss'
        ])
        .then((paths) => {
            let formatter,
                report;

            formatter = getFormatter();
            report = lintFiles(paths);

            /* eslint-disable no-console */
            console.log(formatter(report));
            /* eslint-enable no-console */
        });
    });

    taskCreator('clean', () => {
        return del('./dist');
    });

    taskCreator('copy', ['clean'], () => {
        return gulp
            .src('./src/**/*')
            .pipe(gulp.dest('./dist'));
    });

    taskCreator('build', ['copy'], () => {
        if (!config.browser) {
            /* eslint-disable no-console */
            console.log('Making browser ' + chalk.red('incompatible build') + '. Build is configured to compile node v5+ ready code. ES2015 Babel preset can be added using "browser" option.');
            /* eslint-enable no-console */
        }

        return gulp
            .src('./src/**/*.js')
            .pipe(plumberHandler())
            .pipe(sourcemaps.init())
            .pipe(babel(babelConfig))
            .pipe(sourcemaps.write('.'))
            .pipe(gulp.dest('./dist'));
    });

    taskCreator('task-pre-copy-clean', () => {
        return del('./.test-build');
    });

    taskCreator('test-copy', ['task-pre-copy-clean'], () => {
        return gulp
            .src(['./tests/**/*', './src/**/*'], {
                base: './'
            }).pipe(gulp.dest('./.test-build'));
    });

    taskCreator('test-build', ['test-copy'], () => {
        return gulp
            .src('./.test-build/**/*.js')
            .pipe(plumberHandler())
            .pipe(sourcemaps.init())
            .pipe(babel(babelConfig))
            .pipe(sourcemaps.write('.', {
                sourceRoot: process.cwd()
            }))
            .pipe(gulp.dest('./.test-build'));
    });

    taskCreator('test-hook-require', ['test-build'], () => {
        return gulp
            .src('./.test-build/src/**/*.js')
            .pipe(plumberHandler());
    });

    taskCreator('test-run', ['test-hook-require'], () => {
        return gulp
            .src(['./.test-build/tests/**/*.js'])
            .pipe(plumberHandler())
            .pipe(mocha({
                istanbul: true,
                /* eslint-disable id-length */
                r: require.resolve('source-map-support/register')
                /* eslint-enable id-length */
            }));
    });

    taskCreator('test-clean', ['test-run'], () => {
        return del('./.test-build');
    });

    taskCreator('test', ['test-clean']);

    gulp.task(config.prefix + 'pre-watch', () => {
        watching = true;
        taskError = false;
    });

    taskCreator('watch', () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'lint', config.prefix + 'test', config.prefix + 'build']);
    });

    taskCreator('watch-lint', () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'lint']);
    });

    taskCreator('watch-test', () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'test']);
    });

    taskCreator('watch-build', () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'build']);
    });
};
<commit_msg>Watch initiates initial task.<commit_after>import _ from 'lodash';
import del from 'del';
import babel from 'gulp-babel';
import sourcemaps from 'gulp-sourcemaps';
import mocha from 'gulp-spawn-mocha';
import glob from 'globby';
import plumber from 'gulp-plumber';
import chalk from 'chalk';
import {
    lintFiles,
    getFormatter
} from 'canonical';

import logEvents from './logEvents';
import gutil from 'gulp-util';
import prettyjson from 'prettyjson';
// import stackTrace from 'stack-trace';

import createTaskCraetor from './createTaskCreator';

/**
 * @typedef {Object} options
 * @property {string} prefix Used to prefix all pragmatist tasks.
 * @property {boolean} forceLogging Forces gulp logs for task start time and completion time.
 */

/**
 * @param {Object} gulp
 * @param {options} options
 * @returns {undefined}
 */
export default (gulp, options = {}) => {
    let babelConfig,
        config,
        plumberHandler,
        taskCreator,
        taskError,
        watching;

    watching = false;

    taskCreator = createTaskCraetor(gulp, {
        preTask: (taskName) => {
            if (taskError) {
                gutil.log(chalk.red('Skipping') + ' task ' + chalk.cyan(taskName) + ' due to an earlier error.');

                return false;
            }

            return true;
        },
        prefix: options.prefix
    });

    plumberHandler = () => {
        return plumber({
            errorHandler (error) {
                let errorPrint;

                taskError = true;

                errorPrint = error;

                if (error.message) {
                    // console.log('error', error.stack);

                    errorPrint = {
                        message: error.message,
                        name: error.name,
                        plugin: error.plugin
                    };

                    /* stack: _.map(stackTrace.parse(error), (crumb) => {
                        return crumb.fileName + ':' + crumb.lineNumber + ':' + crumb.columnNumber;
                    }) */

                    /* eslint-disable no-underscore-dangle */
                    if (error._babel && error.codeFrame) {
                    /* eslint-enable no-underscore-dangle */
                        errorPrint.code = {
                            file: error.fileName + ':' + error.loc.line + ':' + error.loc.column,
                            frame: error.codeFrame
                        };

                        if (errorPrint.message.indexOf(error.fileName + ': ') === 0) {
                            errorPrint.message = errorPrint.message.substr(error.fileName.length + 2);
                        }
                    }
                }

                gutil.log('\n\n' + prettyjson.render(errorPrint) + '\n');

                if (watching) {
                    this.emit('end');
                }
            }
        });
    };

    config = _.assign({}, {
        forceLogging: false,
        prefix: 'pragmatist:'
    }, options);

    babelConfig = {
        plugins: [
            require.resolve('babel-plugin-lodash-modularize'),
            require.resolve('babel-plugin-add-module-exports')
        ],
        presets: [
            require.resolve('babel-preset-stage-0'),
            require.resolve('babel-preset-react')
        ],
        babelrc: false,
        extends: __dirname + '/babelrc.json'
    };

    if (config.types) {
        babelConfig.plugins.unshift(require.resolve('babel-plugin-typecheck'));
    }

    if (config.browser) {
        babelConfig.presets.unshift(require.resolve('babel-preset-es2015'));
        // https://phabricator.babeljs.io/T6719#68505
        // babelConfig.plugins.unshift(require.resolve('babel-plugin-transform-class-properties'));
    } else {
        babelConfig.plugins.push(require.resolve('babel-plugin-transform-es2015-modules-commonjs'));
        babelConfig.plugins.push(require.resolve('babel-plugin-transform-es2015-parameters'));
    }

    if (config.forceLogging) {
        logEvents(gulp);
    }

    taskCreator('lint', () => {
        return glob([
            './src/**/*.js',
            './tests/**/*.js',
            './src/**/*.css',
            './src/**/*.scss'
        ])
        .then((paths) => {
            let formatter,
                report;

            formatter = getFormatter();
            report = lintFiles(paths);

            /* eslint-disable no-console */
            console.log(formatter(report));
            /* eslint-enable no-console */
        });
    });

    taskCreator('clean', () => {
        return del('./dist');
    });

    taskCreator('copy', ['clean'], () => {
        return gulp
            .src('./src/**/*')
            .pipe(gulp.dest('./dist'));
    });

    taskCreator('build', ['copy'], () => {
        if (!config.browser) {
            /* eslint-disable no-console */
            console.log('Making browser ' + chalk.red('incompatible build') + '. Build is configured to compile node v5+ ready code. ES2015 Babel preset can be added using "browser" option.');
            /* eslint-enable no-console */
        }

        return gulp
            .src('./src/**/*.js')
            .pipe(plumberHandler())
            .pipe(sourcemaps.init())
            .pipe(babel(babelConfig))
            .pipe(sourcemaps.write('.'))
            .pipe(gulp.dest('./dist'));
    });

    taskCreator('task-pre-copy-clean', () => {
        return del('./.test-build');
    });

    taskCreator('test-copy', ['task-pre-copy-clean'], () => {
        return gulp
            .src(['./tests/**/*', './src/**/*'], {
                base: './'
            }).pipe(gulp.dest('./.test-build'));
    });

    taskCreator('test-build', ['test-copy'], () => {
        return gulp
            .src('./.test-build/**/*.js')
            .pipe(plumberHandler())
            .pipe(sourcemaps.init())
            .pipe(babel(babelConfig))
            .pipe(sourcemaps.write('.', {
                sourceRoot: process.cwd()
            }))
            .pipe(gulp.dest('./.test-build'));
    });

    taskCreator('test-hook-require', ['test-build'], () => {
        return gulp
            .src('./.test-build/src/**/*.js')
            .pipe(plumberHandler());
    });

    taskCreator('test-run', ['test-hook-require'], () => {
        return gulp
            .src(['./.test-build/tests/**/*.js'])
            .pipe(plumberHandler())
            .pipe(mocha({
                istanbul: true,
                /* eslint-disable id-length */
                r: require.resolve('source-map-support/register')
                /* eslint-enable id-length */
            }));
    });

    taskCreator('test-clean', ['test-run'], () => {
        return del('./.test-build');
    });

    taskCreator('test', ['test-clean']);

    gulp.task(config.prefix + 'pre-watch', () => {
        watching = true;
        taskError = false;
    });

    taskCreator('watch', [config.prefix + 'pre-watch', config.prefix + 'lint', config.prefix + 'test', config.prefix + 'build'], () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'lint', config.prefix + 'test', config.prefix + 'build']);
    });

    taskCreator('watch-lint', [config.prefix + 'pre-watch', config.prefix + 'lint'], () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'lint']);
    });

    taskCreator('watch-test', [config.prefix + 'pre-watch', config.prefix + 'test'], () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'test']);
    });

    taskCreator('watch-build', [config.prefix + 'pre-watch', config.prefix + 'build'], () => {
        gulp.watch(['./src/**/*', './tests/**/*'], [config.prefix + 'pre-watch', config.prefix + 'build']);
    });
};
<|endoftext|>


<commit_before>import Stream from 'stream';
import util from 'util';
import crypto from 'crypto';
import path from 'path';
import fs from 'fs';

/**
 * is stream
 */
export function isStream(stream) {
  return !!stream && stream instanceof Stream;
}

/**
 * is buffer
 */
export const isBuffer = Buffer.isBuffer;

/**
 * is array
 */
export const isArray = Array.isArray;

/**
 * is function
 */
export function isFunction(obj) {
  return typeof obj === 'function';
}
/**
 * is object
 */
export function isObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}
/**
 * is string
 */
export function isString(obj) {
  return Object.prototype.toString.call(obj) === '[object String]';
}
/**
 * is regexp
 */
export const isRegExp = util.isRegExp;

/**
 * check path is exist
 */
export function isExist(dir) {
  dir = path.normalize(dir);
  if (fs.accessSync) {
    try {
      fs.accessSync(dir, fs.R_OK);
      return true;
    } catch (e) {
      return false;
    }
  }
  return fs.existsSync(dir);
}

/**
 * check filepath is file
 */
export function isFile(filePath) {
  if (!isExist(filePath)) {
    return false;
  }
  let stat = fs.statSync(filePath);
  return stat.isFile();
}

/**
 * check path is directory
 */
export function isDirectory(filePath) {
  if (!isExist(filePath)) {
    return false;
  }
  let stat = fs.statSync(filePath);
  return stat.isDirectory();
}
/**
 * extend
 */
export function extend(target, source) {
  let src, copy;
  if (!target) {
    target = isArray(source) ? [] : {};
  }
  if (!source) {
    return target;
  }
  for (let key in source) {
    src = target[key];
    copy = source[key];
    if (src && src === copy) {
      continue;
    }
    if (isObject(copy)) {
      target[key] = extend(src && isObject(src) ? src : {}, copy);
    } else if (isArray(copy)) {
      target[key] = extend([], copy);
    } else {
      target[key] = copy;
    }
  }
  return target;
}

/**
 * get path files
 */
export function getFiles(dir, prefix = '') {
  if (!isExist(dir)) {
    return [];
  }
  if (isFile(dir)) {
    return [dir];
  }
  let files = fs.readdirSync(dir);
  let result = [];
  files.forEach(item => {
    let stat = fs.statSync(path.join(dir, item));
    if (stat.isFile()) {
      result.push(path.join(prefix, item));
    } else if (stat.isDirectory()) {
      let cFiles = getFiles(path.join(dir, item), path.join(prefix, item));
      result = result.concat(cFiles);
    }
  });
  return result;
}

/**
 * make callback to promise
 */
export function promisify(fn, receiver) {
  return (...args) => {
    return new Promise((resolve, reject) => {
      fn.apply(receiver, [...args, (err, res) => {
        return err ? reject(err) : resolve(res);
      }]);
    });
  };
}

/**
 * defer
 */
export function defer() {
  let deferred = {};
  deferred.promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });
  return deferred;
}

/**
 * md5
 */
export function md5(str) {
  return crypto.createHash('md5').update(str + '', 'utf8').digest('hex');
}

/**
 * async replace content
 */
export async function asyncReplace(content = '', replace, callback) {
  let match = [];
  content.replace(replace, (...args) => {
    match.push(args);
  });
  if (!match.length) {
    return content;
  }
  let promises = match.map(args => {
    return callback(...args);
  });
  let data = await Promise.all(promises);
  let result = '', prevIndex = 0;
  match.forEach((item, idx) => {
    let index = content.indexOf(item[0], prevIndex);
    result += content.substring(prevIndex, index) + data[idx];
    prevIndex = index + item[0].length;
  });
  result += content.substr(prevIndex);
  return result;
}
/**
 * check if is remote url
 */
export function isRemoteUrl(url) {
  if (!url) {
    return false;
  }
  url = url.toLowerCase();
  return url.indexOf('http://') === 0 || url.indexOf('https://') === 0 || url.indexOf('//') === 0;
}
/**
 * change mode
 */
export function chmod(dir, mode = '0777') {
  if (!isExist(dir)) {
    return false;
  }
  try {
    fs.chmodSync(dir, mode);
    return true;
  } catch (e) {
    return false;
  }
}
/**
 * make dir
 */
export function mkdir(dir, mode = '0777') {
  if (isExist(dir)) {
    return chmod(dir, mode);
  }
  let pp = path.dirname(dir);
  if (isExist(pp)) {
    try {
      fs.mkdirSync(dir, mode);
      return true;
    } catch (e) {
      return false;
    }
  }
  if (mkdir(pp, mode)) {
    return mkdir(dir, mode);
  } else {
    return false;
  }
}

/**
 * resource regexp
 */
export const ResourceRegExp = {
  background: /url\s*\(\s*([\'\"]?)([\w\-\/\.\@]+\.(?:png|jpg|gif|jpeg|ico|cur|webp))(?:\?[^\?\'\"\)\s]*)?\1\s*\)/ig,
  font: /url\s*\(\s*([\'\"]?)([^\'\"\?]+\.(?:eot|woff|woff2|ttf|svg))([^\s\)\'\"]*)\1\s*\)/ig,
  filter: /src\s*=\s*([\'\"])?([^\'\"]+\.(?:png|jpg|gif|jpeg|ico|cur|webp))(?:\?[^\?\'\"\)\s]*)?\1\s*/ig,
  cdn: /\{\s*([\'\"]?)cdn\1\s*\:\s*([\'\"])([\w\/\-\.]+)\2\s*\}\.cdn/gi,
  inline: /\{\s*([\'\"]?)inline\1\s*\:\s*([\'\"])([\w\/\-\.]+)\2\s*\}\.inline/gi,
}

/**
 * background url mapper
 * construct from a string
 * can modify url for further changes
 */
export class BackgroundURLMapper {
  constructor(str) {
    this.orginalStr = str;
    ResourceRegExp.background.lastIndex = 0;
    let matches = ResourceRegExp.background.exec(str);
    this.url = matches && matches.length > 2 && matches[2];
    if (!this.url) {
      throw new TypeError("URLMapper: invalid url");
    }
    this.originalUrl = this.url;
    let pivot = str.indexOf(this.url);
    this.prev = str.slice(0, pivot);
    this.next = str.slice(pivot + this.url.length);
    this.type = /(png|jpg|gif|jpeg|ico|cur|webp)$/.exec(this.url)[1];
  }
  reset() {
    this.url = this.originalUrl;
  }
  valueOf() {
    return this.prev + this.url + this.next;
  }
  isRemoteUrl() {
    return isRemoteUrl(this.url);
  }
};<commit_msg>Add tag resource attrs<commit_after>import Stream from 'stream';
import util from 'util';
import crypto from 'crypto';
import path from 'path';
import fs from 'fs';

/**
 * is stream
 */
export function isStream(stream) {
  return !!stream && stream instanceof Stream;
}

/**
 * is buffer
 */
export const isBuffer = Buffer.isBuffer;

/**
 * is array
 */
export const isArray = Array.isArray;

/**
 * is function
 */
export function isFunction(obj) {
  return typeof obj === 'function';
}
/**
 * is object
 */
export function isObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}
/**
 * is string
 */
export function isString(obj) {
  return Object.prototype.toString.call(obj) === '[object String]';
}
/**
 * is regexp
 */
export const isRegExp = util.isRegExp;

/**
 * check path is exist
 */
export function isExist(dir) {
  dir = path.normalize(dir);
  if (fs.accessSync) {
    try {
      fs.accessSync(dir, fs.R_OK);
      return true;
    } catch (e) {
      return false;
    }
  }
  return fs.existsSync(dir);
}

/**
 * check filepath is file
 */
export function isFile(filePath) {
  if (!isExist(filePath)) {
    return false;
  }
  let stat = fs.statSync(filePath);
  return stat.isFile();
}

/**
 * check path is directory
 */
export function isDirectory(filePath) {
  if (!isExist(filePath)) {
    return false;
  }
  let stat = fs.statSync(filePath);
  return stat.isDirectory();
}
/**
 * extend
 */
export function extend(target, source) {
  let src, copy;
  if (!target) {
    target = isArray(source) ? [] : {};
  }
  if (!source) {
    return target;
  }
  for (let key in source) {
    src = target[key];
    copy = source[key];
    if (src && src === copy) {
      continue;
    }
    if (isObject(copy)) {
      target[key] = extend(src && isObject(src) ? src : {}, copy);
    } else if (isArray(copy)) {
      target[key] = extend([], copy);
    } else {
      target[key] = copy;
    }
  }
  return target;
}

/**
 * get path files
 */
export function getFiles(dir, prefix = '') {
  if (!isExist(dir)) {
    return [];
  }
  if (isFile(dir)) {
    return [dir];
  }
  let files = fs.readdirSync(dir);
  let result = [];
  files.forEach(item => {
    let stat = fs.statSync(path.join(dir, item));
    if (stat.isFile()) {
      result.push(path.join(prefix, item));
    } else if (stat.isDirectory()) {
      let cFiles = getFiles(path.join(dir, item), path.join(prefix, item));
      result = result.concat(cFiles);
    }
  });
  return result;
}

/**
 * make callback to promise
 */
export function promisify(fn, receiver) {
  return (...args) => {
    return new Promise((resolve, reject) => {
      fn.apply(receiver, [...args, (err, res) => {
        return err ? reject(err) : resolve(res);
      }]);
    });
  };
}

/**
 * defer
 */
export function defer() {
  let deferred = {};
  deferred.promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });
  return deferred;
}

/**
 * md5
 */
export function md5(str) {
  return crypto.createHash('md5').update(str + '', 'utf8').digest('hex');
}

/**
 * async replace content
 */
export async function asyncReplace(content = '', replace, callback) {
  let match = [];
  content.replace(replace, (...args) => {
    match.push(args);
  });
  if (!match.length) {
    return content;
  }
  let promises = match.map(args => {
    return callback(...args);
  });
  let data = await Promise.all(promises);
  let result = '', prevIndex = 0;
  match.forEach((item, idx) => {
    let index = content.indexOf(item[0], prevIndex);
    result += content.substring(prevIndex, index) + data[idx];
    prevIndex = index + item[0].length;
  });
  result += content.substr(prevIndex);
  return result;
}
/**
 * check if is remote url
 */
export function isRemoteUrl(url) {
  if (!url) {
    return false;
  }
  url = url.toLowerCase();
  return url.indexOf('http://') === 0 || url.indexOf('https://') === 0 || url.indexOf('//') === 0;
}
/**
 * change mode
 */
export function chmod(dir, mode = '0777') {
  if (!isExist(dir)) {
    return false;
  }
  try {
    fs.chmodSync(dir, mode);
    return true;
  } catch (e) {
    return false;
  }
}
/**
 * make dir
 */
export function mkdir(dir, mode = '0777') {
  if (isExist(dir)) {
    return chmod(dir, mode);
  }
  let pp = path.dirname(dir);
  if (isExist(pp)) {
    try {
      fs.mkdirSync(dir, mode);
      return true;
    } catch (e) {
      return false;
    }
  }
  if (mkdir(pp, mode)) {
    return mkdir(dir, mode);
  } else {
    return false;
  }
}

/**
 * resource regexp
 */
export const ResourceRegExp = {
  background: /url\s*\(\s*([\'\"]?)([\w\-\/\.\@]+\.(?:png|jpg|gif|jpeg|ico|cur|webp))(?:\?[^\?\'\"\)\s]*)?\1\s*\)/ig,
  font: /url\s*\(\s*([\'\"]?)([^\'\"\?]+\.(?:eot|woff|woff2|ttf|svg))([^\s\)\'\"]*)\1\s*\)/ig,
  filter: /src\s*=\s*([\'\"])?([^\'\"]+\.(?:png|jpg|gif|jpeg|ico|cur|webp))(?:\?[^\?\'\"\)\s]*)?\1\s*/ig,
  cdn: /\{\s*([\'\"]?)cdn\1\s*\:\s*([\'\"])([\w\/\-\.]+)\2\s*\}\.cdn/ig,
  inline: /\{\s*([\'\"]?)inline\1\s*\:\s*([\'\"])([\w\/\-\.]+)\2\s*\}\.inline/ig
};

/**
 * resource in tag attrs
 */
export const htmlTagResourceAttrs = {
  img: ['src', 'srcset'],
  script: ['src'],
  link: ['href'],
  param: ['value'],
  embed: ['src'],
  object: ['data'],
  source: ['src', 'srcset']
};

/**
 * background url mapper
 * construct from a string
 * can modify url for further changes
 */
export class BackgroundURLMapper {
  constructor(str) {
    this.orginalStr = str;
    ResourceRegExp.background.lastIndex = 0;
    let matches = ResourceRegExp.background.exec(str);
    this.url = matches && matches.length > 2 && matches[2];
    if (!this.url) {
      throw new TypeError('URLMapper: invalid url');
    }
    this.originalUrl = this.url;
    let pivot = str.indexOf(this.url);
    this.prev = str.slice(0, pivot);
    this.next = str.slice(pivot + this.url.length);
    this.type = /(png|jpg|gif|jpeg|ico|cur|webp)$/.exec(this.url)[1];
  }
  reset() {
    this.url = this.originalUrl;
  }
  valueOf() {
    return this.prev + this.url + this.next;
  }
  isRemoteUrl() {
    return isRemoteUrl(this.url);
  }
}<|endoftext|>


<commit_before>var svgicons2svgfont = require('svgicons2svgfont');
var gutil = require('gulp-util');
var Stream = require('readable-stream');
var Fs = require('fs');
var path = require('path');
var plexer = require('plexer');

module.exports = function(options) {
  var files = [];
  var firstFile = null;
  var metadata;

  options = options || {};
  options.ignoreExt = options.ignoreExt || false;
  options.startUnicode = options.startUnicode ||  0xEA01;
  options.appendUnicode = !!options.appendUnicode;

  if(!options.fontName) {
    throw new gutil.PluginError('svgicons2svgfont', 'Missing options.fontName');
  }

  options.log = options.log || function() {
    gutil.log.apply(gutil, ['gulp-svgicons2svgfont:'].concat(
      [].slice.call(arguments, 0).concat()));
  };

  // Emit event containing codepoint mapping
  options.callback = function(glyphs) {
    stream.emit('glyphs', glyphs.map(function(glyph) {
      return {
        name: glyph.name,
        unicode: glyph.unicode
      };
    }));
  };
  var inputStream = new Stream.Transform({objectMode: true});
  var outputStream = new Stream.PassThrough({objectMode: true});
  var stream = plexer({objectMode: true}, inputStream, outputStream);
  var fontStream;

  options.error = options.error || function() {
    stream.emit('error', new PluginError('svgicons2svgfont',
      [].slice.call(arguments, 0).concat()));
  };

  metadata = require('svgicons2svgfont/src/metadata')({
    startUnicode: options.startUnicode,
    appendUnicode: options.appendUnicode
  });

  inputStream._transform  = function _gulpSVGIcons2SVGFontTransform(file, unused, done) {
    // When null just pass through
    if(file.isNull()) {
      outputStream.write(file); done();
      return;
    }

    // If the ext doesn't match, pass it through
    if((!options.ignoreExt) && '.svg' !== path.extname(file.path)) {
      outputStream.write(file); done();
      return;
    }

    // Generating the font
    if(!firstFile) {
      fontStream = svgicons2svgfont(options);
      fontStream.on('error', function (error) {
        outputStream.emit('error', error);
      });
      firstFile = file;
      // Create the font file
      var joinedFile = new gutil.File({
        cwd: firstFile.cwd,
        base: firstFile.base,
        path: path.join(firstFile.base, options.fontName) + '.svg'
      });

      // Giving the font back to the stream
      if(firstFile.isBuffer()) {
        var buf = new Buffer('');
        fontStream.on('data', function(chunk) {
          buf = Buffer.concat([buf, chunk], buf.length + chunk.length);
        });
        fontStream.on('end', function() {
          joinedFile.contents = buf;
          outputStream.push(joinedFile);
          outputStream.end();
        });
      } else {
        joinedFile.contents = fontStream;
        outputStream.push(joinedFile);
        fontStream.on('end', function() {
          outputStream.end();
        });
      }
    }

    // Wrap icons for the underlying lib
    var iconStream;
    if(file.isBuffer()) {
      iconStream = new Stream.PassThrough();
      setImmediate(function(argument) {
        iconStream.write(file.contents);
        iconStream.end();
      });
    } else {
      iconStream = file.contents;
    }
    metadata(file.path, function(err, theMetadata) {
      if(err) {
        fontStream.emit('error', err);
      }
      iconStream.metadata = theMetadata;
      fontStream.write(iconStream);

      done();
    });
  };

  inputStream._flush  = function _gulpSVGIcons2SVGFontFlush(done) {
    if(!firstFile) {
      outputStream.end();
    } else {
      fontStream.end();
    }
    done();
  };

  return stream;
};
<commit_msg>Configurable metadataProvider<commit_after>var svgicons2svgfont = require('svgicons2svgfont');
var gutil = require('gulp-util');
var Stream = require('readable-stream');
var Fs = require('fs');
var path = require('path');
var plexer = require('plexer');

module.exports = function(options) {
  var files = [];
  var firstFile = null;
  var metadataProvider;

  options = options || {};
  options.ignoreExt = options.ignoreExt || false;
  options.startUnicode = options.startUnicode ||  0xEA01;
  options.appendUnicode = !!options.appendUnicode;

  if(!options.fontName) {
    throw new gutil.PluginError('svgicons2svgfont', 'Missing options.fontName');
  }

  options.log = options.log || function() {
    gutil.log.apply(gutil, ['gulp-svgicons2svgfont:'].concat(
      [].slice.call(arguments, 0).concat()));
  };

  // Emit event containing codepoint mapping
  options.callback = function(glyphs) {
    stream.emit('glyphs', glyphs.map(function(glyph) {
      return {
        name: glyph.name,
        unicode: glyph.unicode
      };
    }));
  };
  var inputStream = new Stream.Transform({objectMode: true});
  var outputStream = new Stream.PassThrough({objectMode: true});
  var stream = plexer({objectMode: true}, inputStream, outputStream);
  var fontStream;

  options.error = options.error || function() {
    stream.emit('error', new PluginError('svgicons2svgfont',
      [].slice.call(arguments, 0).concat()));
  };

  metadataProvider = options.metadataProvider || require('svgicons2svgfont/src/metadata')({
    startUnicode: options.startUnicode,
    appendUnicode: options.appendUnicode
  });

  inputStream._transform  = function _gulpSVGIcons2SVGFontTransform(file, unused, done) {
    // When null just pass through
    if(file.isNull()) {
      outputStream.write(file); done();
      return;
    }

    // If the ext doesn't match, pass it through
    if((!options.ignoreExt) && '.svg' !== path.extname(file.path)) {
      outputStream.write(file); done();
      return;
    }

    // Generating the font
    if(!firstFile) {
      fontStream = svgicons2svgfont(options);
      fontStream.on('error', function (error) {
        outputStream.emit('error', error);
      });
      firstFile = file;
      // Create the font file
      var joinedFile = new gutil.File({
        cwd: firstFile.cwd,
        base: firstFile.base,
        path: path.join(firstFile.base, options.fontName) + '.svg'
      });

      // Giving the font back to the stream
      if(firstFile.isBuffer()) {
        var buf = new Buffer('');
        fontStream.on('data', function(chunk) {
          buf = Buffer.concat([buf, chunk], buf.length + chunk.length);
        });
        fontStream.on('end', function() {
          joinedFile.contents = buf;
          outputStream.push(joinedFile);
          outputStream.end();
        });
      } else {
        joinedFile.contents = fontStream;
        outputStream.push(joinedFile);
        fontStream.on('end', function() {
          outputStream.end();
        });
      }
    }

    // Wrap icons for the underlying lib
    var iconStream;
    if(file.isBuffer()) {
      iconStream = new Stream.PassThrough();
      setImmediate(function(argument) {
        iconStream.write(file.contents);
        iconStream.end();
      });
    } else {
      iconStream = file.contents;
    }
    metadataProvider(file.path, function(err, theMetadata) {
      if(err) {
        fontStream.emit('error', err);
      }
      iconStream.metadata = theMetadata;
      fontStream.write(iconStream);

      done();
    });
  };

  inputStream._flush  = function _gulpSVGIcons2SVGFontFlush(done) {
    if(!firstFile) {
      outputStream.end();
    } else {
      fontStream.end();
    }
    done();
  };

  return stream;
};
<|endoftext|>


<commit_before>const Promise     = require('bluebird');
const _           = require('lodash');

const EventQueue = require('./event-queue');

const telegramPlatform  = require('./platforms/telegram');
const ircPlatform       = require('./platforms/irc');

const log = require('./logger')(__filename);

class Core {
  constructor() {
    // The handlers
    this._monitors = [];
    this._commanders = [];
    this._responders = [];

    // The platforms
    this._availablePlatforms = [
      telegramPlatform,
      ircPlatform
    ];

    // Event queue
    this._queue = new EventQueue();

    // The bots (instances of platforms)
    this._bots = {};
  }

  // Use this if you don't want to use "new" keyword
  static create() {
    return new Core();
  }

  createBot(type, name, options) {

    let chosenPlatform = _.find(this._availablePlatforms, platform => platform.type === type);

    // Platform not found
    if (!chosenPlatform) {
      return Promise.reject(`Platform ${type} not available`);
    }

    // Invalid name
    if (!name) {
      return Promise.reject('Missing parameter "name"');
    }

    // Allow only unique names
    if (this._bots[name]) {
      return Promise.reject(`Bot with name "${name}" has already been created.`);
    }

    // All ok, create bot
    let newBot = new chosenPlatform(name, options);
    this._bots[name] = newBot;

    // Subscribe to pipeline
    newBot.onMessage(event => this.processEvent(event));

    log.info(`Bot "${name}" created from ${type} platform succesfully`);

    // Returns the underlying library so developer can interact
    // with it directly if necessary (setting webhooks etc.)
    return Promise.resolve(newBot);

  }

  getAvailablePlatforms() {
    return this._availablePlatforms.map(platform => platform.type);
  }

  addPlatfrom(platform) {
    if (!_.isFunction(platform.onMessage)) {
      return Promise.reject('Missing required function onMessage()');

    } else if (!_.isFunction(platform.handleActions)) {
      return Promise.reject('Missing required function handleActions()');

    } else if (_.isUndefined(platform.type)) {
      return Promise.reject('Platform type is undefined');

    } else if (_.find(this._availablePlatforms.map(oldPlat => oldPlat.type), platform.type)) {
      return Promise.reject(`Platform with type ${platform.type} already exists`);

    // TODO: Check rest of the error cases (if there is)

    } else {
      this._availablePlatforms.push(platform);
      return Promise.resolve(platform);
    }
  }

  addResponder(responderInstance) {
    if (!_.isFunction(responderInstance.handleEvent)) {
      log.error(`No handleEvent defined for ${responderInstance.constructor.name} ignoring!`);
    } else {
      this._responders.push(responderInstance);
    }
  }

  addCommander(commanderInstance) {
    if (!_.isFunction(commanderInstance.getBidForEvent)) {
      log.error(`No getBidForEvent defined for ${commanderInstance.constructor.name}!`);
    } else if (!_.isFunction(commanderInstance.handleEvent)) {
      log.error(`No handleEvent defined for ${commanderInstance.constructor.name} ignoring!`);
    } else {
      this._commanders.push(commanderInstance);
    }
  }

  addMonitor(monitorInstance) {
    if (!_.isFunction(monitorInstance.handleEvent)) {
      log.error(`No handleEvent defined for ${monitorInstance.constructor.name} ignoring!`);
    } else {
      this._monitors.push(monitorInstance);
    }
  }

  // "The start of the Pipeline"
  processEvent(event) {
    if (this._queue.hasEvent(event.eventId, event.fromBot)) {
      // this event is already in progress -> do nothing
      return Promise.resolve();
    }

    // Check for possible existing conflict situattions
    // TODO: implement

    // Send event to all monitors
    this.sentEventToMonitors(event);

    // Send message to "Pipeline"
    return Promise.resolve()
      .then(() => this.getActions(event))
      .then(actions => this.executeActions(actions, event))
      .catch(e => {
        log.error('Error occurred on the pipeline: ', e);
        return Promise.resolve();
      });
  }

  sendEventToMonitors(event) {
    this._monitors.forEach(monitor => monitor.handleEvent(event));
  }

  getActions(event) {
    return Promise.resolve()
      .then(() => this.getActionsFromCommanders(event))
      .then(commanderActions => {
        // If only one action is returned, wrap it into array
        commanderActions = !_.isArray(commanderActions)
         ? [commanderActions] : commanderActions;

        log.debug('commanderActions', commanderActions);
        console.assert(_.isArray(commanderActions)); // Remove from production

        if (commanderActions.length > 0) {
          log.debug('Actions from Commanders!', commanderActions);
          return commanderActions;
        } else {
          // No actions from Commanders - ask from Responders
          return this.getActionsFromResponders(event);
        }
      });
  }

  executeActions(actions, event) {
    // TODO: IMPLEMENT!
    // -> send the actions to whereever they are going etc

    console.assert(_.isArray(actions)); // Remove from production

    log.debug('Got actions', actions);

    // Fill the missing attributes
    actions.forEach(action => {
      action.toBot = (action.toBot) ? action.toBot : event.fromBot;
      action.targetId = (action.targetId) ? action.targetId : event.userId;
    });

    // Group actions by their destination bot name
    let groupedActions = _.groupBy(actions, 'toBot');

    // In here we should have an array of "actions".
    // These actions may have come from Commanders or Responders,
    // it shouldn't matter at this point.
    //
    // (The actions of Responders will have that "priority" thing since all Responders
    //  will handle all the events, but let's figure out that later)
    //
    // In here the actions should be "cleared" by using whatever bot platform we
    // currently are using.

    // Send actions to bots so they can execute the required actions
    _.forEach(groupedActions, (botActions, bot) => this._bots[bot].handleActions(botActions));

    return actions;
  }

  getActionsFromCommanders(event) {
    // # Command & Respond -loop
    const commandHandlerCandidates = _.map(this._commanders, cmdr =>
      cmdr.getBidForEvent(event)
    );

    // Hack to get only solved promises
    // (http://stackoverflow.com/questions/30309273/keep-the-values-only-from-the-promises-that-resolve-and-ignore-the-rejected)
    return Promise.all(commandHandlerCandidates.map(promise => promise.reflect()))
    .filter(promise => promise.isFulfilled())
    .map(promise => promise.value())
    .then(function solveHandlerCommander(handlerBids) {
      let winningBid = null;

      if (handlerBids.length > 1) {
        // More than one commander was interested in the event,
        // tell user about this conflict

        // TODO: Make user to decide which commander will take the control
        //  (this will require the Core to have some internal state which
        //   will listen for the user's response, and block those messages
        //   from going on. How to do that platform agnostically?)

        winningBid = handlerBids[0]; // TODO temp hack

      } else if (handlerBids.length === 1) {
        // Take the only bid we got
        winningBid = handlerBids[0];
      }

      // Do we have a winner or not?
      // TODO: does the "bid conflict case" need some attention/branch in here?
      if (!_.isNull(winningBid)) {
        // Get actions from the "winning Commander"
        return winningBid.commander.handleEvent(event);

      } else {
        // There was no interested from commanders, send empty array to
        // state that there are no actions from commanders
        return [];
      }
    });
  }

  getActionsFromResponders(event) {
    const responderActionPromises = _.map(this._responders, rspndr =>
      rspndr.handleEvent(event)
    );

    return Promise.all(responderActionPromises.map(promise => promise.reflect()))
      .filter(promise => promise.isFulfilled())
      .map(reflectedPromise => reflectedPromise.value())
      .then(responderActionCandidates => {

        // TODO: in here we have 0-n arrays of actions for each responder which resolved
        // our handleEvent call. Implement some fancy logic for selecting which responder
        // to use or combine them or whatever!
        //
        // For now we just select the first one.
        if (_.isArray(responderActionCandidates) && responderActionCandidates.length > 0) {
          return Promise.resolve(responderActionCandidates[0]);
        } else {
          return Promise.resolve([]);
        }
      });
  }
}

module.exports = Core;
<commit_msg>Use build-in assertion library instead of console.assert<commit_after>const Promise     = require('bluebird');
const _           = require('lodash');
const assert      = require('assert');

const EventQueue = require('./event-queue');

const telegramPlatform  = require('./platforms/telegram');
const ircPlatform       = require('./platforms/irc');

const log = require('./logger')(__filename);

class Core {
  constructor() {
    // The handlers
    this._monitors = [];
    this._commanders = [];
    this._responders = [];

    // The platforms
    this._availablePlatforms = [
      telegramPlatform,
      ircPlatform
    ];

    // Event queue
    this._queue = new EventQueue();

    // The bots (instances of platforms)
    this._bots = {};
  }

  // Use this if you don't want to use "new" keyword
  static create() {
    return new Core();
  }

  createBot(type, name, options) {

    let chosenPlatform = _.find(this._availablePlatforms, platform => platform.type === type);

    // Platform not found
    if (!chosenPlatform) {
      return Promise.reject(`Platform ${type} not available`);
    }

    // Invalid name
    if (!name) {
      return Promise.reject('Missing parameter "name"');
    }

    // Allow only unique names
    if (this._bots[name]) {
      return Promise.reject(`Bot with name "${name}" has already been created.`);
    }

    // All ok, create bot
    let newBot = new chosenPlatform(name, options);
    this._bots[name] = newBot;

    // Subscribe to pipeline
    newBot.onMessage(event => this.processEvent(event));

    log.info(`Bot "${name}" created from ${type} platform succesfully`);

    // Returns the underlying library so developer can interact
    // with it directly if necessary (setting webhooks etc.)
    return Promise.resolve(newBot);

  }

  getAvailablePlatforms() {
    return this._availablePlatforms.map(platform => platform.type);
  }

  addPlatfrom(platform) {
    if (!_.isFunction(platform.onMessage)) {
      return Promise.reject('Missing required function onMessage()');

    } else if (!_.isFunction(platform.handleActions)) {
      return Promise.reject('Missing required function handleActions()');

    } else if (_.isUndefined(platform.type)) {
      return Promise.reject('Platform type is undefined');

    } else if (_.find(this._availablePlatforms.map(oldPlat => oldPlat.type), platform.type)) {
      return Promise.reject(`Platform with type ${platform.type} already exists`);

    // TODO: Check rest of the error cases (if there is)

    } else {
      this._availablePlatforms.push(platform);
      return Promise.resolve(platform);
    }
  }

  addResponder(responderInstance) {
    if (!_.isFunction(responderInstance.handleEvent)) {
      log.error(`No handleEvent defined for ${responderInstance.constructor.name} ignoring!`);
    } else {
      this._responders.push(responderInstance);
    }
  }

  addCommander(commanderInstance) {
    if (!_.isFunction(commanderInstance.getBidForEvent)) {
      log.error(`No getBidForEvent defined for ${commanderInstance.constructor.name}!`);
    } else if (!_.isFunction(commanderInstance.handleEvent)) {
      log.error(`No handleEvent defined for ${commanderInstance.constructor.name} ignoring!`);
    } else {
      this._commanders.push(commanderInstance);
    }
  }

  addMonitor(monitorInstance) {
    if (!_.isFunction(monitorInstance.handleEvent)) {
      log.error(`No handleEvent defined for ${monitorInstance.constructor.name} ignoring!`);
    } else {
      this._monitors.push(monitorInstance);
    }
  }

  // "The start of the Pipeline"
  processEvent(event) {
    if (this._queue.hasEvent(event.eventId, event.fromBot)) {
      // this event is already in progress -> do nothing
      return Promise.resolve();
    }

    // Check for possible existing conflict situattions
    // TODO: implement

    // Send event to all monitors
    this.sentEventToMonitors(event);

    // Send message to "Pipeline"
    return Promise.resolve()
      .then(() => this.getActions(event))
      .then(actions => this.executeActions(actions, event))
      .catch(e => {
        log.error('Error occurred on the pipeline: ', e);
        return Promise.resolve();
      });
  }

  sendEventToMonitors(event) {
    this._monitors.forEach(monitor => monitor.handleEvent(event));
  }

  getActions(event) {
    return Promise.resolve()
      .then(() => this.getActionsFromCommanders(event))
      .then(commanderActions => {
        // If only one action is returned, wrap it into array
        commanderActions = !_.isArray(commanderActions)
         ? [commanderActions] : commanderActions;

        log.debug('commanderActions', commanderActions);
        assert(_.isArray(commanderActions)); // Remove from production

        if (commanderActions.length > 0) {
          log.debug('Actions from Commanders!', commanderActions);
          return commanderActions;
        } else {
          // No actions from Commanders - ask from Responders
          return this.getActionsFromResponders(event);
        }
      });
  }

  executeActions(actions, event) {
    // TODO: IMPLEMENT!
    // -> send the actions to whereever they are going etc

    assert(_.isArray(actions)); // Remove from production

    log.debug('Got actions', actions);

    // Fill the missing attributes
    actions.forEach(action => {
      action.toBot = (action.toBot) ? action.toBot : event.fromBot;
      action.targetId = (action.targetId) ? action.targetId : event.userId;
    });

    // Group actions by their destination bot name
    let groupedActions = _.groupBy(actions, 'toBot');

    // In here we should have an array of "actions".
    // These actions may have come from Commanders or Responders,
    // it shouldn't matter at this point.
    //
    // (The actions of Responders will have that "priority" thing since all Responders
    //  will handle all the events, but let's figure out that later)
    //
    // In here the actions should be "cleared" by using whatever bot platform we
    // currently are using.

    // Send actions to bots so they can execute the required actions
    _.forEach(groupedActions, (botActions, bot) => this._bots[bot].handleActions(botActions));

    return actions;
  }

  getActionsFromCommanders(event) {
    // # Command & Respond -loop
    const commandHandlerCandidates = _.map(this._commanders, cmdr =>
      cmdr.getBidForEvent(event)
    );

    // Hack to get only solved promises
    // (http://stackoverflow.com/questions/30309273/keep-the-values-only-from-the-promises-that-resolve-and-ignore-the-rejected)
    return Promise.all(commandHandlerCandidates.map(promise => promise.reflect()))
    .filter(promise => promise.isFulfilled())
    .map(promise => promise.value())
    .then(function solveHandlerCommander(handlerBids) {
      let winningBid = null;

      if (handlerBids.length > 1) {
        // More than one commander was interested in the event,
        // tell user about this conflict

        // TODO: Make user to decide which commander will take the control
        //  (this will require the Core to have some internal state which
        //   will listen for the user's response, and block those messages
        //   from going on. How to do that platform agnostically?)

        winningBid = handlerBids[0]; // TODO temp hack

      } else if (handlerBids.length === 1) {
        // Take the only bid we got
        winningBid = handlerBids[0];
      }

      // Do we have a winner or not?
      // TODO: does the "bid conflict case" need some attention/branch in here?
      if (!_.isNull(winningBid)) {
        // Get actions from the "winning Commander"
        return winningBid.commander.handleEvent(event);

      } else {
        // There was no interested from commanders, send empty array to
        // state that there are no actions from commanders
        return [];
      }
    });
  }

  getActionsFromResponders(event) {
    const responderActionPromises = _.map(this._responders, rspndr =>
      rspndr.handleEvent(event)
    );

    return Promise.all(responderActionPromises.map(promise => promise.reflect()))
      .filter(promise => promise.isFulfilled())
      .map(reflectedPromise => reflectedPromise.value())
      .then(responderActionCandidates => {

        // TODO: in here we have 0-n arrays of actions for each responder which resolved
        // our handleEvent call. Implement some fancy logic for selecting which responder
        // to use or combine them or whatever!
        //
        // For now we just select the first one.
        if (_.isArray(responderActionCandidates) && responderActionCandidates.length > 0) {
          return Promise.resolve(responderActionCandidates[0]);
        } else {
          return Promise.resolve([]);
        }
      });
  }
}

module.exports = Core;
<|endoftext|>