<commit_before>Astro.utils.storage.getModified = function(currDoc, old, options) {
  old = old || false;

  // Prepare options.
  options = _.extend({
    transient: false,
    immutable: false
  }, options);

  let Class = currDoc.constructor;
  let Collection = Class.getCollection();
  let prevDoc = currDoc._id ? Class.findOne(currDoc._id) : new Class();

  // Get raw data from the docs.
  let rawCurrDoc = Astro.utils.fields.rawAll(currDoc, options);
  let rawPrevDoc = Astro.utils.fields.rawAll(prevDoc, options);

  let diff = function(currDoc, prevDoc) {
    let modified = {};

    if (currDoc === null || currDoc === undefined || !_.isObject(currDoc)) {
      return modified;
    }

    // Loop through all fields.
    _.each(currDoc, function(currFieldValue, fieldName) {
      let prevFieldValue = prevDoc[fieldName];

      if (
        _.isArray(currFieldValue) && _.isArray(prevFieldValue) &&
        currFieldValue.length >= prevFieldValue.length
      ) {

        // The number of elements after modifications is greater or equal to the
        // number of elements before modifications. We have to compare each
        // elements and add any new element.
        for (let i = 0; i < currFieldValue.length; i++) {
          // Get the current value of the element.
          let currElement = currFieldValue[i];
          // Get the previous (original) value of the element.
          let prevElement = prevFieldValue[i];
          // If it's an element that existed before modifications.
          if (i < prevFieldValue.length) {
            // Get a difference between elements.
            let subModified = diff(currElement, prevElement);
            // If there are any differences between elements.
            if (_.size(subModified) > 0) {
              _.each(subModified, function(subFieldValue, subFieldName) {
                modified[fieldName + '.' + i + '.' + subFieldName] =
                  subFieldValue;
              });
            }
          } else {
            // If it's a new element.
            modified[fieldName + '.' + i] = old ? prevElement : currElement;
          }
        }

      }
      else if (
        // FIXME: We should use the _.isPlainObject method from lodash when it
        // will be merged into underscore.
        _.isObject(currFieldValue) && _.isObject(prevFieldValue) &&
        currFieldValue.constructor === Object
      ) {

        let subModified = diff(currFieldValue, prevFieldValue);
        if (_.size(subModified) > 0) {
          _.each(subModified, function(nestedFieldValue, nestedFieldName) {
            modified[fieldName + '.' + nestedFieldName] = nestedFieldValue;
          });
        }

      }
      else {

        let currPlainValue = currFieldValue ?
          currFieldValue.valueOf() : currFieldValue;
        let prevPlainValue = prevFieldValue ?
          prevFieldValue.valueOf() : prevFieldValue;

        if (currPlainValue !== prevPlainValue) {
          // Take a value before or after modification.
          modified[fieldName] = old ? prevFieldValue : currFieldValue;
        }

      }
    });

    return modified;
  };

  return diff(
    rawCurrDoc,
    rawPrevDoc
  );
};<commit_msg>Fix the "getModified" method for array of plain elements<commit_after>Astro.utils.storage.getModified = function(currDoc, old, options) {
  old = old || false;

  // Prepare options.
  options = _.extend({
    transient: false,
    immutable: false
  }, options);

  let Class = currDoc.constructor;
  let Collection = Class.getCollection();
  let prevDoc = currDoc._id ? Class.findOne(currDoc._id) : new Class();

  // Get raw data from the docs.
  let rawCurrDoc = Astro.utils.fields.rawAll(currDoc, options);
  let rawPrevDoc = Astro.utils.fields.rawAll(prevDoc, options);

  let diff = function(currDoc, prevDoc) {
    let modified = {};

    if (currDoc === null || currDoc === undefined || !_.isObject(currDoc)) {
      return modified;
    }

    // Loop through all fields.
    _.each(currDoc, function(currFieldValue, fieldName) {
      let prevFieldValue = prevDoc[fieldName];

      if (
        _.isArray(currFieldValue) && _.isArray(prevFieldValue) &&
        currFieldValue.length >= prevFieldValue.length
      ) {

        // The number of elements after modifications is greater or equal to the
        // number of elements before modifications. We have to compare each
        // elements and add any new element.
        for (let i = 0; i < currFieldValue.length; i++) {
          // Get the current value of the element.
          let currElement = currFieldValue[i];
          // Get the previous (original) value of the element.
          let prevElement = prevFieldValue[i];
          // If it's an element that existed before modifications.
          if (i < prevFieldValue.length) {
            // If array elements are object, then we perform diff between them.
            if (_.isObject(currElement) && _.isObject(prevElement)) {
              // Get a difference between elements.
              let subModified = diff(currElement, prevElement);
              // If there are any differences between elements.
              if (_.size(subModified) > 0) {
                _.each(subModified, function(subFieldValue, subFieldName) {
                  modified[fieldName + '.' + i + '.' + subFieldName] =
                    subFieldValue;
                });
              }
            }
            // If array elements are plain types, then we compare them.
            else if (prevElement !== currElement) {
              modified[fieldName + '.' + i] = old ? prevElement : currElement;
            }
          } else {
            // If it's a new element.
            modified[fieldName + '.' + i] = old ? prevElement : currElement;
          }
        }

      }
      else if (
        // FIXME: We should use the _.isPlainObject method from lodash when it
        // will be merged into underscore.
        _.isObject(currFieldValue) && _.isObject(prevFieldValue) &&
        currFieldValue.constructor === Object
      ) {

        let subModified = diff(currFieldValue, prevFieldValue);
        if (_.size(subModified) > 0) {
          _.each(subModified, function(nestedFieldValue, nestedFieldName) {
            modified[fieldName + '.' + nestedFieldName] = nestedFieldValue;
          });
        }

      }
      else {

        let currPlainValue = currFieldValue ?
          currFieldValue.valueOf() : currFieldValue;
        let prevPlainValue = prevFieldValue ?
          prevFieldValue.valueOf() : prevFieldValue;
        if (currPlainValue !== prevPlainValue) {
          // Take a value before or after modification.
          modified[fieldName] = old ? prevFieldValue : currFieldValue;
        }

      }
    });

    return modified;
  };

  return diff(
    rawCurrDoc,
    rawPrevDoc
  );
};<|endoftext|>


<commit_before>import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

import auth from 'common/reducers/auth';
import me from 'common/reducers/me';
import request from 'common/reducers/request';
import deviceTypes from 'common/reducers/deviceTypes';
import navigation from 'common/reducers/navigation';
import users from 'common/reducers/users';
import flash from 'common/reducers/flash';
import devices from 'common/reducers/devices';
import fieldSearchModal from 'common/reducers/field-search-modal';
import rules from 'common/reducers/rules';
import data from 'common/reducers/data';
import dashboard from 'common/reducers/dashboard';

export const reducers = combineReducers({
  auth,
  me,
  request,
  deviceTypes,
  navigation,
  users,
  flash,
  devices,
  fieldSearchModal,
  rules,
  data,
  dashboard,
  form: formReducer,
});
<commit_msg>reducers: adding realtime reducer<commit_after>import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

import auth from 'common/reducers/auth';
import me from 'common/reducers/me';
import request from 'common/reducers/request';
import deviceTypes from 'common/reducers/deviceTypes';
import navigation from 'common/reducers/navigation';
import users from 'common/reducers/users';
import flash from 'common/reducers/flash';
import devices from 'common/reducers/devices';
import fieldSearchModal from 'common/reducers/field-search-modal';
import rules from 'common/reducers/rules';
import data from 'common/reducers/data';
import dashboard from 'common/reducers/dashboard';
import realtime from 'common/reducers/realtime';

export const reducers = combineReducers({
  auth,
  me,
  request,
  deviceTypes,
  navigation,
  users,
  flash,
  devices,
  fieldSearchModal,
  rules,
  data,
  dashboard,
  realtime,
  form: formReducer,
});
<|endoftext|>


<commit_before>// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';

foam.CLASS({
  package: 'org.chromium.webidl',
  name: 'PipelineRunner',
  extends: 'foam.box.Runnable',

  requires: ['org.chromium.webidl.PipelineMessage'],

  constants: {
    INVALID_ARG: `run() expects a PipelineMessage object!`,
    MISSING_PROPS: 'Missing required properties in PipelineMessage!',
  },

  methods: [
    function fmtErrorMsg(msg) {
      return new Error(`${this.cls_.name}: ${msg}`);
    },
    function run(message) {
      if (!this.PipelineMessage.isInstance(message)) {
        var errorMsg = this.fmtErrorMsg(this.INVALID_ARG);
        this.error(errorMsg);
        return errorMsg;
      }
    },
  ],
});
<commit_msg>Add default errorBox to PipelineRunner<commit_after>// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';

foam.CLASS({
  package: 'org.chromium.webidl',
  name: 'PipelineRunner',
  extends: 'foam.box.Runnable',

  requires: [
    'foam.box.LogBox',
    'foam.log.LogLevel',
    'org.chromium.webidl.PipelineMessage',
  ],

  constants: {
    INVALID_ARG: `run() expects a PipelineMessage object!`,
    MISSING_PROPS: 'Missing required properties in PipelineMessage!',
  },

  classes: [
    {
      name: 'ErrorBox',
      extends: 'foam.box.LogBox',

      requires: ['foam.log.LogLevel'],

      properties: [
        {
          name: 'logLevel',
          factory: function() { return this.LogLevel.ERROR; },
        }
      ],

      methods: [
        function send(message) {
          var output = message.object;
          this[this.logLevel.consoleMethodName].apply(this, [
            this.name,
            output instanceof Error ? output.toString() :
                foam.json.Pretty.stringify(message)
          ]);
          this.delegate && this.delegate.send(message);
        }
      ],
    },
  ],

  properties: [
    {
      name: 'errorBox',
      factory: function() {
        return this.ErrorBox.create({
          name: `LogBox:${this.cls_.id}:${this.$UID}`,
        });
      },
    },
  ],

  methods: [
    function fmtErrorMsg(msg) {
      return new Error(`${this.cls_.name}: ${msg}`);
    },
    function run(message) {
      if (!this.PipelineMessage.isInstance(message)) {
        var errorMsg = this.fmtErrorMsg(this.INVALID_ARG);
        this.error(errorMsg);
        return errorMsg;
      }
    },
  ],
});
<|endoftext|>


<commit_before>/**
 * Interact with local and remote project repos using Git.
 */

'use strict';

var git = require('nodegit');
var _ = require('underscore');
var fs = require('q-io/fs'); // https://github.com/kriskowal/q-io

var appConfig = require('../../configuration').get();

var ignoreSSLErrors = {
  certificateCheck: function () {
    return 1;
  }
};

/**
 * Parse the tag and branch names out of the refs list
 * and generate an object of data to return.
 */
function parseTagsAndBranches(refs, projectRef) {
  return refs
    .map(function (ref) {
      var shortName = ref.shorthand();
      var safeName = encodeURIComponent(ref.name());
      return {
        name: shortName,
        tag: ref.isTag(),
        branch: ref.isBranch(),
        urlEncodedName: safeName,
        current: ref.name() === projectRef.name()
      };
    });
}

// Wrap up the supplied repository metadata in an object.
// TODO: this model should not be generated here.
function generateRepoMetaData(projectData, sha, tagsAndBranches, currentBranchName, files, remoteUrl) {
  var updatedData = {
    // Copied
    repoName: projectData.repoName,
    localPath: projectData.localPath,
    projectLink: projectData.projectLink,
    // New
    commit: sha,
    shortCommit: sha.substring(0, 7),
    tagsAndBranches: tagsAndBranches,
    currentBranchName: currentBranchName,
    repoUrl: remoteUrl,
    files: files
  };

  // Filter falsey values and empty arrays.
  function findFalsey(data) {
    return function (key) {
      var value = data[key];
      return !data[key] || (value.length !== undefined ? value.length === 0 : false);
    };
  }

  var missingValues = Object.keys(updatedData).filter(findFalsey(updatedData));
  if (missingValues.length) {
    process.stdout.write('DEBUG: Missing values:' + '\n');
    process.stdout.write(missingValues.toString() + '\n');
  }

  return updatedData;
}

/**
 * Clone the project.
 *
 * @return a promise for the repo metadata.
 */
function clone(projectData) {
  var repository;

  // Need to define this here as the Clone function modifies the passed object (face palm).
  var cloneOptions = {};
  cloneOptions.bare = 1;

  // Mac OSX certificate issue http://www.nodegit.org/guides/cloning/
  // This is now an undocumented Mac only part of the API.
  cloneOptions.remoteCallbacks = ignoreSSLErrors;

  // General option to ingore SSL errors.
  if(appConfig.allowInsecureSSL) {
    cloneOptions.fetchOpts = {
      callbacks: ignoreSSLErrors
    };
  }

  if (!projectData.repoUrl) {
    throw new TypeError('Clone called without a repo URL. Possible wrong project name?');
  }

  return git.Clone(projectData.repoUrl, projectData.localPath, cloneOptions)
    .then(function (repo) {
      repository = repo;
      return repository.getCurrentBranch();
    })
    .then(function (branch) {
      return getProjectData(projectData, branch.shorthand());
    });
}

/**
 * Perform a fetch on the project.
 *
 * @return a promise for the repo metadata.
 */
function update(projectData) {
  return git.Repository.openBare(projectData.localPath)
    .then(function (repository) {

      var fetchOptions = {};
      fetchOptions.downloadTags = 1;
      fetchOptions.prune = 1;
      fetchOptions.bare = 1;
      fetchOptions.updateFetchhead = 1;
      fetchOptions.remoteCallbacks = ignoreSSLErrors;
      fetchOptions.fetchOpts = {
        callbacks: ignoreSSLErrors
      };

      // BUG: This isn't working at all, no new tags or branch are pulled down,
      // current heads aren't updated.
      // https://github.com/nodegit/nodegit/issues/742
      return repository.fetch('origin', fetchOptions);
    })
    .then(function () {
      return getProjectData(projectData, projectData.currentBranchName);
    })
    .catch(function (error) {
      console.log("Error fetching from remote: ", error);
    });
}

/**
 * Get or update the project repo using Git.
 *
 * @return a promise for the repo metadata.
 */
function getProject(projectData) {
  return fs.exists(projectData.localPath)
    .then(function (pathExists) {

      /**
       * If there is no matching directory then clone the repo
       * else perform a pull on it.
       */
      if (!pathExists) {
        return clone(projectData);
      } else {
        return update(projectData);
      }
    });
}

function getBranch(projectData, branchName) {
  return git.Repository.openBare(projectData.localPath)
    .then(function (repo) {
      if (branchName) {
        return repo.getBranch(branchName);
      }
      return repo.getCurrentBranch();
    });
}

// NodeGit has no sensible way to detect an annotated tag, which is
// handled differently from a lightweight tag. So try the first
// approach which will work for branches and lightweights tags
// if that fails fall bacl to try the annotated tag approach.
function getCommitByName(repository, refName) {
  return repository.getReferenceCommit(refName)
    .catch(function() {
      return repository.getTagByName(refName).then(function(tag) {
        return repository.getCommit(tag.targetId().tostrS());
      });
    });
}

function getProjectData(projectData, targetBranchName) {
  var currentRef;
  var repository;
  var remoteUrl;
  var headCommit;
  var listOfFiles;

  return getBranch(projectData, targetBranchName)
    .then(function (ref) {
      currentRef = ref;
      return git.Repository.openBare(projectData.localPath);
    })
    .then(function (repo) {
      repository = repo;
      return repository.getRemote('origin');
    }).then(function (remote) {
      remoteUrl = remote.url();

      return getCommitByName(repository, currentRef.name());
    })
    .then(function (commit) {
      headCommit = commit.sha();
      return commit.getTree();
    })
    .then(getAllFiles)
    .then(function (files) {
      listOfFiles = files;

      return repository.getReferences(git.Reference.TYPE.OID);
    })
    .then(function (refs) {
      var tagsAndBranches = parseTagsAndBranches(refs, currentRef);
      return generateRepoMetaData(projectData, headCommit, tagsAndBranches, currentRef.name(), listOfFiles, remoteUrl);
    });
}

function getFileContents(projectData, filePath) {
  var repository;

  if (!filePath) {
    throw new TypeError('Please supply a file path to read file contents.');
  }

  return git.Repository.openBare(projectData.localPath)
    .then(function (repo) {
      repository = repo;
      return getCommitByName(repository, projectData.currentBranchName);
    })
    .then(function (commit) {
      return commit.getTree();
    })
    .then(function (tree) {
      return tree.getEntry(filePath);
    })
    .then(function (entry) {
      return entry.getBlob();
    })
    .then(function (blob) {
      return blob.toString();
    });
}

function _getAllFiles(tree) {
  var files = [];

  tree.entries().forEach(function (e) {
    if (e.isFile())
      files.push(e.path());

    if (e.isTree())
      files.push(e.getTree().then(function (t) {
        return _getAllFiles(t);
      }));
  });

  return Promise.all(files)
    .then(function (files) {
      return _.flatten(files);
    });
}

function getAllFiles(tree) {
  return _getAllFiles(tree);
}

module.exports = {
  getProject: getProject,
  getProjectData: getProjectData,
  getFileContents: getFileContents
};
<commit_msg>Removing catch from fetch<commit_after>/**
 * Interact with local and remote project repos using Git.
 */

'use strict';

var git = require('nodegit');
var _ = require('underscore');
var fs = require('q-io/fs'); // https://github.com/kriskowal/q-io

var appConfig = require('../../configuration').get();

var ignoreSSLErrors = {
  certificateCheck: function () {
    return 1;
  }
};

/**
 * Parse the tag and branch names out of the refs list
 * and generate an object of data to return.
 */
function parseTagsAndBranches(refs, projectRef) {
  return refs
    .map(function (ref) {
      var shortName = ref.shorthand();
      var safeName = encodeURIComponent(ref.name());
      return {
        name: shortName,
        tag: ref.isTag(),
        branch: ref.isBranch(),
        urlEncodedName: safeName,
        current: ref.name() === projectRef.name()
      };
    });
}

// Wrap up the supplied repository metadata in an object.
// TODO: this model should not be generated here.
function generateRepoMetaData(projectData, sha, tagsAndBranches, currentBranchName, files, remoteUrl) {
  var updatedData = {
    // Copied
    repoName: projectData.repoName,
    localPath: projectData.localPath,
    projectLink: projectData.projectLink,
    // New
    commit: sha,
    shortCommit: sha.substring(0, 7),
    tagsAndBranches: tagsAndBranches,
    currentBranchName: currentBranchName,
    repoUrl: remoteUrl,
    files: files
  };

  // Filter falsey values and empty arrays.
  function findFalsey(data) {
    return function (key) {
      var value = data[key];
      return !data[key] || (value.length !== undefined ? value.length === 0 : false);
    };
  }

  var missingValues = Object.keys(updatedData).filter(findFalsey(updatedData));
  if (missingValues.length) {
    process.stdout.write('DEBUG: Missing values:' + '\n');
    process.stdout.write(missingValues.toString() + '\n');
  }

  return updatedData;
}

/**
 * Clone the project.
 *
 * @return a promise for the repo metadata.
 */
function clone(projectData) {
  var repository;

  // Need to define this here as the Clone function modifies the passed object (face palm).
  var cloneOptions = {};
  cloneOptions.bare = 1;

  // Mac OSX certificate issue http://www.nodegit.org/guides/cloning/
  // This is now an undocumented Mac only part of the API.
  cloneOptions.remoteCallbacks = ignoreSSLErrors;

  // General option to ingore SSL errors.
  if(appConfig.allowInsecureSSL) {
    cloneOptions.fetchOpts = {
      callbacks: ignoreSSLErrors
    };
  }

  if (!projectData.repoUrl) {
    throw new TypeError('Clone called without a repo URL. Possible wrong project name?');
  }

  return git.Clone(projectData.repoUrl, projectData.localPath, cloneOptions)
    .then(function (repo) {
      repository = repo;
      return repository.getCurrentBranch();
    })
    .then(function (branch) {
      return getProjectData(projectData, branch.shorthand());
    });
}

/**
 * Perform a fetch on the project.
 *
 * @return a promise for the repo metadata.
 */
function update(projectData) {
  return git.Repository.openBare(projectData.localPath)
    .then(function (repository) {

      var fetchOptions = {};
      fetchOptions.downloadTags = 1;
      fetchOptions.prune = 1;
      fetchOptions.bare = 1;
      fetchOptions.updateFetchhead = 1;
      fetchOptions.remoteCallbacks = ignoreSSLErrors;
      fetchOptions.fetchOpts = {
        callbacks: ignoreSSLErrors
      };

      // BUG: This isn't working at all, no new tags or branch are pulled down,
      // current heads aren't updated.
      // https://github.com/nodegit/nodegit/issues/742
      return repository.fetch('origin', fetchOptions);
    })
    .then(function () {
      return getProjectData(projectData, projectData.currentBranchName);
    });
}

/**
 * Get or update the project repo using Git.
 *
 * @return a promise for the repo metadata.
 */
function getProject(projectData) {
  return fs.exists(projectData.localPath)
    .then(function (pathExists) {

      /**
       * If there is no matching directory then clone the repo
       * else perform a pull on it.
       */
      if (!pathExists) {
        return clone(projectData);
      } else {
        return update(projectData);
      }
    });
}

function getBranch(projectData, branchName) {
  return git.Repository.openBare(projectData.localPath)
    .then(function (repo) {
      if (branchName) {
        return repo.getBranch(branchName);
      }
      return repo.getCurrentBranch();
    });
}

// NodeGit has no sensible way to detect an annotated tag, which is
// handled differently from a lightweight tag. So try the first
// approach which will work for branches and lightweights tags
// if that fails fall bacl to try the annotated tag approach.
function getCommitByName(repository, refName) {
  return repository.getReferenceCommit(refName)
    .catch(function() {
      return repository.getTagByName(refName).then(function(tag) {
        return repository.getCommit(tag.targetId().tostrS());
      });
    });
}

function getProjectData(projectData, targetBranchName) {
  var currentRef;
  var repository;
  var remoteUrl;
  var headCommit;
  var listOfFiles;

  return getBranch(projectData, targetBranchName)
    .then(function (ref) {
      currentRef = ref;
      return git.Repository.openBare(projectData.localPath);
    })
    .then(function (repo) {
      repository = repo;
      return repository.getRemote('origin');
    }).then(function (remote) {
      remoteUrl = remote.url();

      return getCommitByName(repository, currentRef.name());
    })
    .then(function (commit) {
      headCommit = commit.sha();
      return commit.getTree();
    })
    .then(getAllFiles)
    .then(function (files) {
      listOfFiles = files;

      return repository.getReferences(git.Reference.TYPE.OID);
    })
    .then(function (refs) {
      var tagsAndBranches = parseTagsAndBranches(refs, currentRef);
      return generateRepoMetaData(projectData, headCommit, tagsAndBranches, currentRef.name(), listOfFiles, remoteUrl);
    });
}

function getFileContents(projectData, filePath) {
  var repository;

  if (!filePath) {
    throw new TypeError('Please supply a file path to read file contents.');
  }

  return git.Repository.openBare(projectData.localPath)
    .then(function (repo) {
      repository = repo;
      return getCommitByName(repository, projectData.currentBranchName);
    })
    .then(function (commit) {
      return commit.getTree();
    })
    .then(function (tree) {
      return tree.getEntry(filePath);
    })
    .then(function (entry) {
      return entry.getBlob();
    })
    .then(function (blob) {
      return blob.toString();
    });
}

function _getAllFiles(tree) {
  var files = [];

  tree.entries().forEach(function (e) {
    if (e.isFile())
      files.push(e.path());

    if (e.isTree())
      files.push(e.getTree().then(function (t) {
        return _getAllFiles(t);
      }));
  });

  return Promise.all(files)
    .then(function (files) {
      return _.flatten(files);
    });
}

function getAllFiles(tree) {
  return _getAllFiles(tree);
}

module.exports = {
  getProject: getProject,
  getProjectData: getProjectData,
  getFileContents: getFileContents
};
<|endoftext|>


<commit_before>var _ = require('lodash');
var defaultStylesheet = require('./defaultStylesheet');
var stylePropObjectComparator = require('./stylePropObjectComparator');
var unquote = require('./unquote');
var memoizeSync = require('memoizesync');
var cssFontWeightNames = require('css-font-weight-names');
var cssPseudoElementRegExp = require('../cssPseudoElementRegExp');
var getPseudoElements = require('./getPseudoElements');
var stripPseudoClassesFromSelector = require('./stripPseudoClassesFromSelector');
var gatherStylesheetsWithIncomingMedia = require('./gatherStylesheetsWithIncomingMedia');
var getCssRulesByProperty = require('./getCssRulesByProperty');

var FONT_PROPS = [
    'font-family',
    'font-weight',
    'font-style'
];

var INITIAL_VALUES = {
    // 'font-family': 'serif'
    'font-weight': 400,
    'font-style': 'normal'
};

var NAME_CONVERSIONS = {
    'font-weight': cssFontWeightNames
};

function createPredicatePermutations(predicatesToVary, i) {
    if (typeof i !== 'number') {
        i = 0;
    }
    if (i < predicatesToVary.length) {
        var permutations = [];
        createPredicatePermutations(predicatesToVary, i + 1).forEach(function (permutation) {
            var permutationWithPredicateOff = Object.assign({}, permutation);
            permutationWithPredicateOff[predicatesToVary[i]] = true;
            permutations.push(permutation, permutationWithPredicateOff);
        });
        return permutations;
    } else {
        return [ {} ];
    }
}

var excludedNodes = ['HEAD', 'STYLE', 'SCRIPT'];

function getNodeIdArray(node) {
    var idArray = [];
    var currentNode = node;
    var childNodes;

    while (currentNode.parentNode) {
        childNodes = Array.prototype.slice.call(currentNode.parentNode.childNodes)
            .filter(function (node) {
                return node.nodeType === 1  && excludedNodes.indexOf(node.tagName) === -1;
            });

        idArray.push(childNodes.indexOf(currentNode));

        currentNode = currentNode.parentNode;
    }

    return idArray.reverse();
}

function getFontRulesWithDefaultStylesheetApplied(htmlAsset, memoizedGetCssRulesByProperty) {
    var fontPropRules = [{ text: defaultStylesheet, incomingMedia: [] }].concat(gatherStylesheetsWithIncomingMedia(htmlAsset.assetGraph, htmlAsset))
        .map(function (stylesheetAndIncomingMedia) {
            return memoizedGetCssRulesByProperty(FONT_PROPS, stylesheetAndIncomingMedia.text, stylesheetAndIncomingMedia.incomingMedia);
        })
        .reduce(function (rules, current) {
            // Input:
            // [
            //     {
            //         'font-style': [],
            //         'font-weight': [],
            //         'font-family': []
            //     },
            //     {
            //         'font-style': [],
            //         'font-weight': [],
            //         'font-family': []
            //     },
            //     {
            //         'font-style': [],
            //         'font-weight': [],
            //         'font-family': []
            //     }
            // ]

            // Output:
            // {
            //     'font-style': [].concat([], [], []),
            //     'font-weight': [].concat([], [], []),
            //     'font-family': [].concat([], [], [])
            // }
            Object.keys(current).forEach(function (prop) {
                if (!rules[prop]) {
                    rules[prop] = [];
                }

                rules[prop] = rules[prop].concat(current[prop]);
            });

            return rules;
        }, {});

    Object.keys(fontPropRules).forEach(function (prop) {
        fontPropRules[prop].sort(stylePropObjectComparator(fontPropRules[prop]));
    });

    return fontPropRules;
}

function getMemoizedElementStyleResolver(fontPropRules, availableWeights, memoizedGetCssRulesByProperty) {
    var nonInheritingTags = ['BUTTON', 'INPUT', 'OPTION', 'TEXTAREA'];

    var getComputedStyle = memoizeSync(function (node, idArray, truePredicates, falsePredicates) {
        truePredicates = truePredicates || {};
        falsePredicates = falsePredicates || {};
        var localFontPropRules = Object.assign({}, fontPropRules);
        var props = Object.keys(fontPropRules);
        var result = {};

        // Stop condition. We moved above <HTML>
        if (!node.tagName) {
            props.forEach(function (prop) {
                result[prop] = [ { value: INITIAL_VALUES[prop], truePredicates: truePredicates, falsePredicates: falsePredicates }];
            });
            return result;
        }

        if (node.getAttribute('style')) {
            var attributeStyles = memoizedGetCssRulesByProperty(FONT_PROPS, 'bogusselector { ' + node.getAttribute('style') + ' }', []);

            Object.keys(attributeStyles).forEach(function (prop) {
                if (attributeStyles[prop].length > 0) {
                    var concatRules = attributeStyles[prop].concat(localFontPropRules[prop]);
                    localFontPropRules[prop] = concatRules.sort(stylePropObjectComparator(concatRules));
                }
            });
        }

        function traceProp(prop, startIndex, truePredicates, falsePredicates) {
            startIndex = startIndex || 0;

            for (var i = startIndex; i < localFontPropRules[prop].length; i += 1) {
                var declaration = localFontPropRules[prop][i];
                // Skip to the next rule if we are doing a trace where one of the incoming media attributes are already assumed false:
                if (declaration.incomingMedia.some(function (incomingMedia) { return falsePredicates['incomingMedia:' + incomingMedia]; })) {
                    continue;
                }

                // Style attributes always have a specificity array of [1, 0, 0, 0]
                var isStyleAttribute = declaration.specificityArray[0] === 1;
                var selectorWithoutPseudoClasses = !isStyleAttribute && stripPseudoClassesFromSelector(declaration.selector);

                // Pseudo elements are handled elsewhere, and Jsdom crashes if we execute a node.matches on this selector
                if (!isStyleAttribute && declaration.selector.match(cssPseudoElementRegExp)) {
                    continue;
                }

                if (isStyleAttribute || node.matches(selectorWithoutPseudoClasses)) {
                    var hypotheticalValues;
                    if (declaration.value === 'inherit' || declaration.value === 'unset') {
                        hypotheticalValues = getComputedStyle(node.parentNode, idArray.slice(0, -1), truePredicates, falsePredicates)[prop];
                    } else {
                        var value;
                        if (declaration.value === 'initial') {
                            value = INITIAL_VALUES[prop];
                        } else if (NAME_CONVERSIONS[prop] && NAME_CONVERSIONS[prop][declaration.value]) {
                            value = NAME_CONVERSIONS[prop][declaration.value];
                        } else if (prop === 'font-weight') {
                            if (declaration.value === 'lighter' || declaration.value === 'bolder') {
                                var inheritedWeight = getComputedStyle(node.parentNode, idArray.slice(0, -1))[prop];
                                if (declaration.value === 'lighter') {
                                    value = availableWeights[availableWeights.indexOf(inheritedWeight[0]) - 1] || inheritedWeight;
                                }
                                if (declaration.value === 'bolder') {
                                    value = availableWeights[availableWeights.indexOf(inheritedWeight[0]) + 1] || inheritedWeight;
                                }
                            } else {
                                value = Number(declaration.value);
                            }
                        } else if (prop === 'font-family') {
                            value = unquote(declaration.value);
                        } else {
                            value = declaration.value;
                        }

                        hypotheticalValues = [ { value: value, truePredicates: truePredicates, falsePredicates: falsePredicates } ];
                    }

                    var predicatesToVary = [];
                    if (!isStyleAttribute && selectorWithoutPseudoClasses !== declaration.selector) {
                        predicatesToVary.push('selectorWithPseudoClasses:' + declaration.selector);
                    }
                    if (declaration.mediaQuery) {
                        predicatesToVary.push('mediaQuery:' + declaration.mediaQuery);
                    }
                    Array.prototype.push.apply(predicatesToVary, declaration.incomingMedia.map(function (incomingMedia) {
                        return 'incomingMedia:' + incomingMedia;
                    }));
                    if (predicatesToVary.length > 0) {
                        var multipliedHypotheticalValues = [];
                        createPredicatePermutations(predicatesToVary).forEach(function (predicatePermutation) {
                            if (Object.keys(predicatePermutation).length === 0) {
                                return;
                            }
                            var truePredicatesForThisPermutation = Object.assign({}, truePredicates, predicatePermutation);
                            if (declaration.incomingMedia.every(function (incomingMedia) { return truePredicatesForThisPermutation['incomingMedia:' + incomingMedia]; })) {
                                Array.prototype.push.apply(
                                    multipliedHypotheticalValues,
                                    hypotheticalValues.map(function (hypotheticalValue) {
                                        return {
                                            value: hypotheticalValue.value,
                                            truePredicates: truePredicatesForThisPermutation,
                                            falsePredicates: falsePredicates
                                        };
                                    })
                                );
                            }
                            Array.prototype.push.apply(
                                multipliedHypotheticalValues,
                                traceProp(prop, i + 1, truePredicates, Object.assign({}, falsePredicates, predicatePermutation))
                            );
                        });
                        return multipliedHypotheticalValues;
                    } else {
                        return hypotheticalValues;
                    }
                }
            }

            if (nonInheritingTags.indexOf(node.tagName) === -1) {
                return getComputedStyle(node.parentNode, idArray.slice(0, -1), truePredicates, falsePredicates)[prop];
            } else {
                return [ { value: INITIAL_VALUES[prop], truePredicates: truePredicates, falsePredicates: falsePredicates }];
            }
        }

        props.forEach(function (prop) {
            result[prop] = traceProp(prop, 0, truePredicates, falsePredicates);
        });

        return result;
    }, {
        argumentsStringifier: function (args) {
            return (
                args[1].join(',') + '\x1e' +
                (args[2] ? Object.keys(args[2]).join('\x1d') : '') + '\x1e' +
                (args[3] ? Object.keys(args[3]).join('\x1d') : '')
            );
        }
    });

    return getComputedStyle;
}

// memoizedGetCssRulesByProperty is optional
function getTextByFontProperties(htmlAsset, availableWeights, memoizedGetCssRulesByProperty) {
    if (!htmlAsset || htmlAsset.type !== 'Html'  || !htmlAsset.assetGraph) {
        throw new Error('htmlAsset must be a Html-asset and be in an assetGraph');
    }

    if (availableWeights && !Array.isArray(availableWeights)) {
        throw new Error('availableWeights must be an array of css font weights');
    }

    availableWeights = availableWeights || [];

    memoizedGetCssRulesByProperty = memoizedGetCssRulesByProperty || getCssRulesByProperty;

    var fontPropRules = getFontRulesWithDefaultStylesheetApplied(htmlAsset, memoizedGetCssRulesByProperty);
    var getComputedStyle = getMemoizedElementStyleResolver(fontPropRules, availableWeights, memoizedGetCssRulesByProperty);

    var document = htmlAsset.parseTree;

    var textNodes = [];
    var nonTextnodes = [];
    var visualValueInputTypes = [
        'date',
        'datetime-local',
        'email',
        'month',
        'number',
        'reset',
        'search',
        'submit',
        'tel',
        'text',
        'time',
        'url',
        'week'
    ];

    var pseudoElements = getPseudoElements(htmlAsset);

    (function traversePreOrder(node, idArray) {
        if (node.nodeType === 1) {
            if (!idArray) {
                idArray = [0];
            }

            var currentIndex = 0;
            var child = node.firstChild;

            // Inputs might have visual text, but don't have childNodes
            if (node.tagName === 'INPUT' && visualValueInputTypes.indexOf(node.type || 'text') !== -1) {
                var inputValue = (node.value || '').trim();
                var inputPlaceholder = (node.placeholder || '').trim();

                if (inputValue) {
                    nonTextnodes.push({
                        text: inputValue,
                        node: node,
                        id: idArray
                    });
                }

                if (inputPlaceholder) {
                    nonTextnodes.push({
                        text: inputPlaceholder,
                        node: node,
                        id: idArray
                    });
                }
            }

            while (child) {
                if (child.nodeType === 3 && child.textContent.trim()) {
                    textNodes.push({
                        node: child,
                        parentId: idArray
                    });
                }

                if (child.nodeType === 1 && excludedNodes.indexOf(child.tagName) === -1) {
                    traversePreOrder(child, idArray.concat(currentIndex));
                    currentIndex += 1;
                }

                child = child.nextSibling;
            }
        }
    }(document.body.parentNode));

    var styledTexts = textNodes
        .map(function (textNodeObj) {
            return {
                text: textNodeObj.node.textContent.trim(),
                props: getComputedStyle(textNodeObj.node.parentNode, textNodeObj.parentId)
            };
        })
        .concat(pseudoElements.map(function (pseudoElement) {
            return {
                text: pseudoElement.textContent.trim(),
                props: getComputedStyle(pseudoElement, getNodeIdArray(pseudoElement.parentNode).concat([0]))
            };
        }))
        .concat(nonTextnodes.map(function (nodeObj) {
            return {
                text: nodeObj.text,
                props: getComputedStyle(nodeObj.node, nodeObj.id)
            };
        }));

    // propsByText Before:
    // [
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': [ { value: 'a', truePredicates: Set, falsePredicates: Set }, { value: 'b', truePredicates: Set, falsePredicates: Set }],
    //             'font-style': [ { value: 'normal', truePredicates: Set, falsePredicates: Set } ],
    //             'font-weight': [ { value: 400, truePredicates: Set, falsePredicates: Set }, { value: 700, truePredicates: Set, falsePredicates: Set }]
    //         }
    //     },
    //     ...
    // ]

    // Expand into all permutations in case of multiple hypothetical values:
    function expandPermutations(styledText, propertyNames) {
        propertyNames = propertyNames || Object.keys(styledText.props);
        var permutations = [];
        var firstPropertyName = propertyNames[0];
        var firstPropertyValues = styledText.props[propertyNames[0]];

        for (var i = 0 ; i < Math.max(1, firstPropertyValues.length) ; i += 1) {
            if (propertyNames.length > 1) {
                expandPermutations(styledText, propertyNames.slice(1)).forEach(function (permutation) {
                    permutation[firstPropertyName] = firstPropertyValues[i];
                    permutations.push(permutation);
                });
            } else {
                var permutation = {};
                permutation[firstPropertyName] = firstPropertyValues[i];
                permutations.push(permutation);
            }
        }

        return permutations;
    }

    var multipliedStyledTexts = _.flatten(styledTexts.map(function (styledText) {
        var deduplicatedExpandedPermutations = [];
        var seenPermutationByKey = {};
        expandPermutations(styledText)
            .filter(function (hypotheticalValuesByProp) {
                // Check that none of the predicates assumed true are assumed false, too:
                return FONT_PROPS.every(function (prop) {
                    return Object.keys(hypotheticalValuesByProp[prop].truePredicates).every(function (truePredicate) {
                        return FONT_PROPS.every(function (otherProp) {
                            return !hypotheticalValuesByProp[otherProp].falsePredicates[truePredicate];
                        });
                    });
                });
            })
            .forEach(function (hypotheticalValuesByProp) {
                // Unwrap the "hypothetical value" objects:
                var permutationKey = '';
                var props = {};
                FONT_PROPS.forEach(function (prop) {
                    permutationKey += prop + '\x1d' + hypotheticalValuesByProp[prop].value + '\x1d';
                    props[prop] = hypotheticalValuesByProp[prop].value;
                });
                // Deduplicate:
                if (!seenPermutationByKey[permutationKey]) {
                    seenPermutationByKey[permutationKey] = true;
                    deduplicatedExpandedPermutations.push({
                        text: styledText.text,
                        props: props
                    });
                }
            });
        return deduplicatedExpandedPermutations;
    }));

    // multipliedStyledTexts After:
    // [
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'a',
    //             'font-style': 'normal',
    //             'font-weight': 400
    //         }
    //     },
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'b',
    //             'font-style': 'normal',
    //             'font-weight': 400
    //         }
    //     },
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'a',
    //             'font-style': 'normal',
    //             'font-weight': 700
    //         }
    //     },
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'b',
    //             'font-style': 'normal',
    //             'font-weight': 700
    //         }
    //     },
    //     ...
    // ]

    return multipliedStyledTexts;
}

module.exports = getTextByFontProperties;
<commit_msg>getComputedStyle in a spacific font property order: family, style, weight. For future implementation of weight calculations<commit_after>var _ = require('lodash');
var defaultStylesheet = require('./defaultStylesheet');
var stylePropObjectComparator = require('./stylePropObjectComparator');
var unquote = require('./unquote');
var memoizeSync = require('memoizesync');
var cssFontWeightNames = require('css-font-weight-names');
var cssPseudoElementRegExp = require('../cssPseudoElementRegExp');
var getPseudoElements = require('./getPseudoElements');
var stripPseudoClassesFromSelector = require('./stripPseudoClassesFromSelector');
var gatherStylesheetsWithIncomingMedia = require('./gatherStylesheetsWithIncomingMedia');
var getCssRulesByProperty = require('./getCssRulesByProperty');

var FONT_PROPS = [
    'font-family',
    'font-style',
    'font-weight'
];

var INITIAL_VALUES = {
    // 'font-family': 'serif'
    'font-weight': 400,
    'font-style': 'normal'
};

var NAME_CONVERSIONS = {
    'font-weight': cssFontWeightNames
};

function createPredicatePermutations(predicatesToVary, i) {
    if (typeof i !== 'number') {
        i = 0;
    }
    if (i < predicatesToVary.length) {
        var permutations = [];
        createPredicatePermutations(predicatesToVary, i + 1).forEach(function (permutation) {
            var permutationWithPredicateOff = Object.assign({}, permutation);
            permutationWithPredicateOff[predicatesToVary[i]] = true;
            permutations.push(permutation, permutationWithPredicateOff);
        });
        return permutations;
    } else {
        return [ {} ];
    }
}

var excludedNodes = ['HEAD', 'STYLE', 'SCRIPT'];

function getNodeIdArray(node) {
    var idArray = [];
    var currentNode = node;
    var childNodes;

    while (currentNode.parentNode) {
        childNodes = Array.prototype.slice.call(currentNode.parentNode.childNodes)
            .filter(function (node) {
                return node.nodeType === 1  && excludedNodes.indexOf(node.tagName) === -1;
            });

        idArray.push(childNodes.indexOf(currentNode));

        currentNode = currentNode.parentNode;
    }

    return idArray.reverse();
}

function getFontRulesWithDefaultStylesheetApplied(htmlAsset, memoizedGetCssRulesByProperty) {
    var fontPropRules = [{ text: defaultStylesheet, incomingMedia: [] }].concat(gatherStylesheetsWithIncomingMedia(htmlAsset.assetGraph, htmlAsset))
        .map(function (stylesheetAndIncomingMedia) {
            return memoizedGetCssRulesByProperty(FONT_PROPS, stylesheetAndIncomingMedia.text, stylesheetAndIncomingMedia.incomingMedia);
        })
        .reduce(function (rules, current) {
            // Input:
            // [
            //     {
            //         'font-style': [],
            //         'font-weight': [],
            //         'font-family': []
            //     },
            //     {
            //         'font-style': [],
            //         'font-weight': [],
            //         'font-family': []
            //     },
            //     {
            //         'font-style': [],
            //         'font-weight': [],
            //         'font-family': []
            //     }
            // ]

            // Output:
            // {
            //     'font-style': [].concat([], [], []),
            //     'font-weight': [].concat([], [], []),
            //     'font-family': [].concat([], [], [])
            // }
            Object.keys(current).forEach(function (prop) {
                if (!rules[prop]) {
                    rules[prop] = [];
                }

                rules[prop] = rules[prop].concat(current[prop]);
            });

            return rules;
        }, {});

    Object.keys(fontPropRules).forEach(function (prop) {
        fontPropRules[prop].sort(stylePropObjectComparator(fontPropRules[prop]));
    });

    return fontPropRules;
}

function getMemoizedElementStyleResolver(fontPropRules, availableWeights, memoizedGetCssRulesByProperty) {
    var nonInheritingTags = ['BUTTON', 'INPUT', 'OPTION', 'TEXTAREA'];

    var getComputedStyle = memoizeSync(function (node, idArray, truePredicates, falsePredicates) {
        truePredicates = truePredicates || {};
        falsePredicates = falsePredicates || {};
        var localFontPropRules = Object.assign({}, fontPropRules);
        var result = {};

        // Stop condition. We moved above <HTML>
        if (!node.tagName) {
            FONT_PROPS.forEach(function (prop) {
                result[prop] = [ { value: INITIAL_VALUES[prop], truePredicates: truePredicates, falsePredicates: falsePredicates }];
            });
            return result;
        }

        if (node.getAttribute('style')) {
            var attributeStyles = memoizedGetCssRulesByProperty(FONT_PROPS, 'bogusselector { ' + node.getAttribute('style') + ' }', []);

            Object.keys(attributeStyles).forEach(function (prop) {
                if (attributeStyles[prop].length > 0) {
                    var concatRules = attributeStyles[prop].concat(localFontPropRules[prop]);
                    localFontPropRules[prop] = concatRules.sort(stylePropObjectComparator(concatRules));
                }
            });
        }

        function traceProp(prop, startIndex, truePredicates, falsePredicates) {
            startIndex = startIndex || 0;

            for (var i = startIndex; i < localFontPropRules[prop].length; i += 1) {
                var declaration = localFontPropRules[prop][i];
                // Skip to the next rule if we are doing a trace where one of the incoming media attributes are already assumed false:
                if (declaration.incomingMedia.some(function (incomingMedia) { return falsePredicates['incomingMedia:' + incomingMedia]; })) {
                    continue;
                }

                // Style attributes always have a specificity array of [1, 0, 0, 0]
                var isStyleAttribute = declaration.specificityArray[0] === 1;
                var selectorWithoutPseudoClasses = !isStyleAttribute && stripPseudoClassesFromSelector(declaration.selector);

                // Pseudo elements are handled elsewhere, and Jsdom crashes if we execute a node.matches on this selector
                if (!isStyleAttribute && declaration.selector.match(cssPseudoElementRegExp)) {
                    continue;
                }

                if (isStyleAttribute || node.matches(selectorWithoutPseudoClasses)) {
                    var hypotheticalValues;
                    if (declaration.value === 'inherit' || declaration.value === 'unset') {
                        hypotheticalValues = getComputedStyle(node.parentNode, idArray.slice(0, -1), truePredicates, falsePredicates)[prop];
                    } else {
                        var value;
                        if (declaration.value === 'initial') {
                            value = INITIAL_VALUES[prop];
                        } else if (NAME_CONVERSIONS[prop] && NAME_CONVERSIONS[prop][declaration.value]) {
                            value = NAME_CONVERSIONS[prop][declaration.value];
                        } else if (prop === 'font-weight') {
                            if (declaration.value === 'lighter' || declaration.value === 'bolder') {
                                var inheritedWeight = getComputedStyle(node.parentNode, idArray.slice(0, -1))[prop];
                                if (declaration.value === 'lighter') {
                                    value = availableWeights[availableWeights.indexOf(inheritedWeight[0]) - 1] || inheritedWeight;
                                }
                                if (declaration.value === 'bolder') {
                                    value = availableWeights[availableWeights.indexOf(inheritedWeight[0]) + 1] || inheritedWeight;
                                }
                            } else {
                                value = Number(declaration.value);
                            }
                        } else if (prop === 'font-family') {
                            value = unquote(declaration.value);
                        } else {
                            value = declaration.value;
                        }

                        hypotheticalValues = [ { value: value, truePredicates: truePredicates, falsePredicates: falsePredicates } ];
                    }

                    var predicatesToVary = [];
                    if (!isStyleAttribute && selectorWithoutPseudoClasses !== declaration.selector) {
                        predicatesToVary.push('selectorWithPseudoClasses:' + declaration.selector);
                    }
                    if (declaration.mediaQuery) {
                        predicatesToVary.push('mediaQuery:' + declaration.mediaQuery);
                    }
                    Array.prototype.push.apply(predicatesToVary, declaration.incomingMedia.map(function (incomingMedia) {
                        return 'incomingMedia:' + incomingMedia;
                    }));
                    if (predicatesToVary.length > 0) {
                        var multipliedHypotheticalValues = [];
                        createPredicatePermutations(predicatesToVary).forEach(function (predicatePermutation) {
                            if (Object.keys(predicatePermutation).length === 0) {
                                return;
                            }
                            var truePredicatesForThisPermutation = Object.assign({}, truePredicates, predicatePermutation);
                            if (declaration.incomingMedia.every(function (incomingMedia) { return truePredicatesForThisPermutation['incomingMedia:' + incomingMedia]; })) {
                                Array.prototype.push.apply(
                                    multipliedHypotheticalValues,
                                    hypotheticalValues.map(function (hypotheticalValue) {
                                        return {
                                            value: hypotheticalValue.value,
                                            truePredicates: truePredicatesForThisPermutation,
                                            falsePredicates: falsePredicates
                                        };
                                    })
                                );
                            }
                            Array.prototype.push.apply(
                                multipliedHypotheticalValues,
                                traceProp(prop, i + 1, truePredicates, Object.assign({}, falsePredicates, predicatePermutation))
                            );
                        });
                        return multipliedHypotheticalValues;
                    } else {
                        return hypotheticalValues;
                    }
                }
            }

            if (nonInheritingTags.indexOf(node.tagName) === -1) {
                return getComputedStyle(node.parentNode, idArray.slice(0, -1), truePredicates, falsePredicates)[prop];
            } else {
                return [ { value: INITIAL_VALUES[prop], truePredicates: truePredicates, falsePredicates: falsePredicates }];
            }
        }

        FONT_PROPS.forEach(function (prop) {
            result[prop] = traceProp(prop, 0, truePredicates, falsePredicates);
        });

        return result;
    }, {
        argumentsStringifier: function (args) {
            return (
                args[1].join(',') + '\x1e' +
                (args[2] ? Object.keys(args[2]).join('\x1d') : '') + '\x1e' +
                (args[3] ? Object.keys(args[3]).join('\x1d') : '')
            );
        }
    });

    return getComputedStyle;
}

// memoizedGetCssRulesByProperty is optional
function getTextByFontProperties(htmlAsset, availableWeights, memoizedGetCssRulesByProperty) {
    if (!htmlAsset || htmlAsset.type !== 'Html'  || !htmlAsset.assetGraph) {
        throw new Error('htmlAsset must be a Html-asset and be in an assetGraph');
    }

    if (availableWeights && !Array.isArray(availableWeights)) {
        throw new Error('availableWeights must be an array of css font weights');
    }

    availableWeights = availableWeights || [];

    memoizedGetCssRulesByProperty = memoizedGetCssRulesByProperty || getCssRulesByProperty;

    var fontPropRules = getFontRulesWithDefaultStylesheetApplied(htmlAsset, memoizedGetCssRulesByProperty);
    var getComputedStyle = getMemoizedElementStyleResolver(fontPropRules, availableWeights, memoizedGetCssRulesByProperty);

    var document = htmlAsset.parseTree;

    var textNodes = [];
    var nonTextnodes = [];
    var visualValueInputTypes = [
        'date',
        'datetime-local',
        'email',
        'month',
        'number',
        'reset',
        'search',
        'submit',
        'tel',
        'text',
        'time',
        'url',
        'week'
    ];

    var pseudoElements = getPseudoElements(htmlAsset);

    (function traversePreOrder(node, idArray) {
        if (node.nodeType === 1) {
            if (!idArray) {
                idArray = [0];
            }

            var currentIndex = 0;
            var child = node.firstChild;

            // Inputs might have visual text, but don't have childNodes
            if (node.tagName === 'INPUT' && visualValueInputTypes.indexOf(node.type || 'text') !== -1) {
                var inputValue = (node.value || '').trim();
                var inputPlaceholder = (node.placeholder || '').trim();

                if (inputValue) {
                    nonTextnodes.push({
                        text: inputValue,
                        node: node,
                        id: idArray
                    });
                }

                if (inputPlaceholder) {
                    nonTextnodes.push({
                        text: inputPlaceholder,
                        node: node,
                        id: idArray
                    });
                }
            }

            while (child) {
                if (child.nodeType === 3 && child.textContent.trim()) {
                    textNodes.push({
                        node: child,
                        parentId: idArray
                    });
                }

                if (child.nodeType === 1 && excludedNodes.indexOf(child.tagName) === -1) {
                    traversePreOrder(child, idArray.concat(currentIndex));
                    currentIndex += 1;
                }

                child = child.nextSibling;
            }
        }
    }(document.body.parentNode));

    var styledTexts = textNodes
        .map(function (textNodeObj) {
            return {
                text: textNodeObj.node.textContent.trim(),
                props: getComputedStyle(textNodeObj.node.parentNode, textNodeObj.parentId)
            };
        })
        .concat(pseudoElements.map(function (pseudoElement) {
            return {
                text: pseudoElement.textContent.trim(),
                props: getComputedStyle(pseudoElement, getNodeIdArray(pseudoElement.parentNode).concat([0]))
            };
        }))
        .concat(nonTextnodes.map(function (nodeObj) {
            return {
                text: nodeObj.text,
                props: getComputedStyle(nodeObj.node, nodeObj.id)
            };
        }));

    // propsByText Before:
    // [
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': [ { value: 'a', truePredicates: Set, falsePredicates: Set }, { value: 'b', truePredicates: Set, falsePredicates: Set }],
    //             'font-style': [ { value: 'normal', truePredicates: Set, falsePredicates: Set } ],
    //             'font-weight': [ { value: 400, truePredicates: Set, falsePredicates: Set }, { value: 700, truePredicates: Set, falsePredicates: Set }]
    //         }
    //     },
    //     ...
    // ]

    // Expand into all permutations in case of multiple hypothetical values:
    function expandPermutations(styledText, propertyNames) {
        propertyNames = propertyNames || Object.keys(styledText.props);
        var permutations = [];
        var firstPropertyName = propertyNames[0];
        var firstPropertyValues = styledText.props[propertyNames[0]];

        for (var i = 0 ; i < Math.max(1, firstPropertyValues.length) ; i += 1) {
            if (propertyNames.length > 1) {
                expandPermutations(styledText, propertyNames.slice(1)).forEach(function (permutation) {
                    permutation[firstPropertyName] = firstPropertyValues[i];
                    permutations.push(permutation);
                });
            } else {
                var permutation = {};
                permutation[firstPropertyName] = firstPropertyValues[i];
                permutations.push(permutation);
            }
        }

        return permutations;
    }

    var multipliedStyledTexts = _.flatten(styledTexts.map(function (styledText) {
        var deduplicatedExpandedPermutations = [];
        var seenPermutationByKey = {};
        expandPermutations(styledText)
            .filter(function (hypotheticalValuesByProp) {
                // Check that none of the predicates assumed true are assumed false, too:
                return FONT_PROPS.every(function (prop) {
                    return Object.keys(hypotheticalValuesByProp[prop].truePredicates).every(function (truePredicate) {
                        return FONT_PROPS.every(function (otherProp) {
                            return !hypotheticalValuesByProp[otherProp].falsePredicates[truePredicate];
                        });
                    });
                });
            })
            .forEach(function (hypotheticalValuesByProp) {
                // Unwrap the "hypothetical value" objects:
                var permutationKey = '';
                var props = {};
                FONT_PROPS.forEach(function (prop) {
                    permutationKey += prop + '\x1d' + hypotheticalValuesByProp[prop].value + '\x1d';
                    props[prop] = hypotheticalValuesByProp[prop].value;
                });
                // Deduplicate:
                if (!seenPermutationByKey[permutationKey]) {
                    seenPermutationByKey[permutationKey] = true;
                    deduplicatedExpandedPermutations.push({
                        text: styledText.text,
                        props: props
                    });
                }
            });
        return deduplicatedExpandedPermutations;
    }));

    // multipliedStyledTexts After:
    // [
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'a',
    //             'font-style': 'normal',
    //             'font-weight': 400
    //         }
    //     },
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'b',
    //             'font-style': 'normal',
    //             'font-weight': 400
    //         }
    //     },
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'a',
    //             'font-style': 'normal',
    //             'font-weight': 700
    //         }
    //     },
    //     {
    //         text: 'foo',
    //         props: {
    //             'font-family': 'b',
    //             'font-style': 'normal',
    //             'font-weight': 700
    //         }
    //     },
    //     ...
    // ]

    return multipliedStyledTexts;
}

module.exports = getTextByFontProperties;
<|endoftext|>


<commit_before>var child_process = require('child_process'); // http://nodejs.org/api/child_process.html
var path = require('path'); // File path processing. http://nodejs.org/api/path.html
var _ = require('underscore'); // Utilities. http://underscorejs.org/
var Backbone = require('backbone'); // Data model utilities. http://backbonejs.org/
var moment = require('moment'); // Date processing. http://momentjs.com/
var request = require('request'); // Simpler HTTP. https://github.com/mikeal/request
var progress = require('request-progress'); // Progress events on downloads. https://github.com/IndigoUnited/node-request-progress
var XRegExp = require('xregexp').XRegExp; // Fancy regular expressions. http://xregexp.com/
var rimraf = require('rimraf'); // Recursive directory delete. https://github.com/isaacs/rimraf
var fs = require('node-fs'); // Recursive directory creation. https://github.com/bpedro/node-fs
var ncp = require('ncp').ncp; // Recursive directory copy. https://npmjs.org/package/ncp
var winston = require('winston'); // Logging. https://github.com/flatiron/winston

// Update content for the application, as well as the application itself.
exports.ContentUpdater = Backbone.Model.extend({

    defaults: {
        // When the content was last updated.
        downloaded: null,
        updated: null,

        // The remote URL to the root XML.
        remote: null,

        // The final local path for content.
        local: 'content/',

        // The temp path for content.
        temp: 'content.tmp/',

        // A collection of file objects that are being processed.
        files: null,
        needsUpdate: false,
    },

    // A callback to call when updating is complete.
    _callback: null,

    // Download new content to the temp folder.
    download: function(callback) {
        this.set('needsUpdate', false);
        this._callback = callback;
        this._initDirectories(_.bind(function() {
            if (this.get('remote').indexOf('http') === 0) {
                // We're going to pull down an XML file from the web and parse it for other files.
                request(this.get('remote'), _.bind(this._processContentRoot, this));
            } else {
                // We're going to just robocopy from another folder instead.
                this._robocopy(this.get('remote'), path.resolve(this.get('temp')), null, _.bind(function(code) {
                    this.set('needsUpdate', code > 0 && code <= 8);
                    this._callback(code > 8 ? code : 0);
                    if (code > 8) {
                        // Something bad happened.
                        winston.error('Robocopy failed with code ' + code);
                    }
                }, this));
            }
        }, this));
    },

    // Set up the temp and output directories.
    _initDirectories: function(callback) {
        // Make the temp directory.
        fs.mkdir(this.get('temp'), 0777, true, _.bind(function(error) {
            this._handleError('Error creating temp directory.', error);

            fs.exists(this.get('local'), _.bind(function(exists) {
                if (exists) {
                    callback();
                    return;
                }

                // Make the ouput directory.
                fs.mkdir(this.get('local'), 0777, true, _.bind(function(error) {
                    this._handleError('Error creating ouput directory.', error);
                    callback();
                }, this));
            }, this));
        }, this));
    },

    // Process the XML file to extract files to load.
    _processContentRoot: function(error, response, body) {
        if (response.statusCode != 200) {
            this._handleError('Error loading root XML -- bad password?');
            return;
        }

        this._handleError('Error loading root XML.', error);

        // Write the root XML file.
        fs.writeFile(this.get('temp') + 'content.xml', body, _.bind(function(error) {
            this._handleError('Error writing root XML.', error);
            this.set('files', new exports.ContentFiles());

            // Collect any URLs in root file.
            var pattern = new XRegExp(/>(http.*?)</gi);
            var matches = [];
            var match;

            while (true) {
                match = pattern.exec(body);
                if (!match) {
                    break;
                }
                matches.push(match);
            }

            // Figure out what each URL begins with -- this is removed to create the relative path.
            var prefix = '';

            function iterator(element, index, list) {
                return element[1].indexOf(prefix) === 0;
            }

            while (true) {
                prefix += matches[0][1][prefix.length];
                if (!_.every(matches, iterator)) {
                    prefix = prefix.substr(0, prefix.length - 1);
                    break;
                }
            }

            // Build file objects.
            _.each(matches, function(element, index, list) {
                var url = element[1];
                var relativePath = url.substr(prefix.length);
                var file = new exports.ContentFile({
                    url: url,
                    filePath: this.get('local') + relativePath,
                    tempPath: this.get('temp') + relativePath
                });

                file.on('loaded', this._onFileLoaded, this);
                this.get('files').add(file);
            }, this);

            _.each(this.get('files').models, _.bind(this._processFile, this));
        }, this));
    },

    // Determine if the file needs to be loaded, or if it's already up to date.
    _processFile: function(contentFile) {
        fs.exists(contentFile.get('tempPath'), _.bind(function(exists) {
            if (!exists) {
                // The file doesn't exist locally, so download it.
                this._downloadFile(contentFile);
                return;
            }

            // The file does exist locally, check if the remote one is newer.
            fs.stat(contentFile.get('tempPath'), _.bind(function(error, stats) {
                var localFileModified = moment(stats.mtime);
                request({
                    url: contentFile.get('url'),
                    method: 'HEAD'
                }, _.bind(function(error, response, body) {
                    var remoteFileModified = moment(response.headers['last-modified']);
                    if (remoteFileModified.isAfter(localFileModified)) {
                        // The remote file is newer, go get it.
                        this._downloadFile(contentFile);
                        return;
                    }

                    // The local file is newer, flag it as loaded.
                    contentFile.set('progress', 1);
                }, this));
            }, this));
        }, this));
    },

    // Load the file into memory and create its temp output directory.
    _downloadFile: function(contentFile) {
        winston.info('Downloading ' + contentFile.get('url'));

        // Request the file from the network.
        progress(request({
            url: contentFile.get('url'),
            encoding: null // Required for binary files.
        }, _.bind(function(error, response, body) {
            if (!error && response.statusCode != 200) {
                error = response.statusCode;
            }

            this.set('needsUpdate', !error);
            this._handleError('Error loading ' + contentFile.get('url'), error);

            // Create the file's output directory if needed.
            var dir = path.dirname(contentFile.get('tempPath'));
            fs.exists(dir, _.bind(function(exists) {
                if (!exists) {
                    fs.mkdir(dir, 0777, true, _.bind(function(error) {
                        this._handleError('Error creating temp directory for file.', error);
                        this._writeFile(contentFile, body);
                    }, this));
                } else {
                    this._writeFile(contentFile, body);
                }
            }, this));

        }, this))).on('progress', function(state) {

            // Set progress properties on the file.
            contentFile.set('receivedBytes', state.received);
            contentFile.set('totalBytes', state.total);
            contentFile.set('progress', state.percent / 100);
        });
    },

    // When the file is done loading, write it to disk.
    _writeFile: function(contentFile, body) {
        fs.writeFile(contentFile.get('tempPath'), body, null, _.bind(function(error) {
            this._handleError('Error writing file.', error);
            contentFile.set('progress', 1);
        }, this));
    },

    // Called when the progress of a file is set to 1.
    _onFileLoaded: function(contentFile) {
        contentFile.off('loaded', this._onFileLoaded, this);

        // Determine if all files are loaded.
        var filesToGo = _.filter(this.get('files').models, function(file) {
            return file.get('progress') < 1;
        }).length;
        var style = contentFile.get('totalBytes') ? 'loaded' : 'cached';
        winston.info(contentFile.get('url') + ' ' + style + ', ' + filesToGo + ' to go');

        if (!filesToGo) {
            this.set('downloaded', moment());
            this._callback();
        }
    },

    // Copy files to their final destination when all files are loaded.
    update: function(callback) {
        this._callback = callback;
        // Recursive copy from temp to target.
        ncp(this.get('temp'), this.get('local'), _.bind(function(error) {
            this._handleError('Error copying from temp folder.', error);
            this._completed();
        }, this));
    },

    // Notify on completion.
    _completed: function() {
        this.set('updated', moment());
        this.trigger('complete');
        if (this._callback) {
            this._callback();
        }
    },

    // Generic error handling function.
    _handleError: function(message, error) {
        if (!error) {
            return;
        }

        winston.error(message, error);
        if (this._callback) {
            this._callback(error);
        }
    },

    // Robocopy files instead of downloading them.
    _robocopy: function(sourceDir, targetDir, file, callback) {
        // http://technet.microsoft.com/en-us/library/cc733145.aspx
        var args = [
            sourceDir,
            targetDir,
            file,
            '/e', // Copies subdirectories. Note that this option includes empty directories.
            '/v', // Produces verbose output, and shows all skipped files.
            '/np', // Specifies that the progress of the copying operation (the number of files or directories copied so far) will not be displayed.
            '/njs', // Specifies that there is no job summary.
            '/njh', // Specifies that there is no job header.
            '/bytes', // Prints sizes, as bytes.
            '/fft', // Assumes FAT file times (two-second precision).
            '/ndl' // Specifies that directory names are not to be logged.
        ];

        if (!file) {
            // We're copying full directories, remove the file argument.
            args.splice(2, 1);
        } else {
            // We're copying one file, remove the copy-subdirectories argument.
            args.splice(3, 1);
        }

        var copy = child_process.spawn('robocopy', args);
        winston.info('robocopy: robocopy ' + args.join(' '));

        copy.stdout.on('data', _.bind(function(data) {
            var lines = data.toString().split('\r\n');
            _.each(lines, function(line) {
                line = line.trim();
                if (line) {
                    winston.info('robocopy: ' + line);
                }
            });
        }, this));

        copy.stderr.on('data', _.bind(function(data) {
            winston.error('robocopy: ' + data.toString());
        }, this));

        copy.on('close', _.bind(function(code) {
            // The return codes are weird. http://support.microsoft.com/kb/954404
            this.set('downloaded', moment());
            if (callback) {
                callback(code);
            }
        }, this));
    }
});

exports.ContentFile = Backbone.Model.extend({
    defaults: {
        url: null,
        filePath: null,
        tempPath: null,
        progress: 0,
        receivedBytes: 0,
        totalBytes: 0
    },

    initialize: function() {
        this.on('change:progress', function(model, value, options) {
            if (value >= 1) {
                this.trigger('loaded', this);
            }
        }, this);
    }
});

exports.ContentFiles = Backbone.Collection.extend({
    model: exports.ContentFile
});<commit_msg>don't try to update if you don't have a URL<commit_after>var child_process = require('child_process'); // http://nodejs.org/api/child_process.html
var path = require('path'); // File path processing. http://nodejs.org/api/path.html
var _ = require('underscore'); // Utilities. http://underscorejs.org/
var Backbone = require('backbone'); // Data model utilities. http://backbonejs.org/
var moment = require('moment'); // Date processing. http://momentjs.com/
var request = require('request'); // Simpler HTTP. https://github.com/mikeal/request
var progress = require('request-progress'); // Progress events on downloads. https://github.com/IndigoUnited/node-request-progress
var XRegExp = require('xregexp').XRegExp; // Fancy regular expressions. http://xregexp.com/
var rimraf = require('rimraf'); // Recursive directory delete. https://github.com/isaacs/rimraf
var fs = require('node-fs'); // Recursive directory creation. https://github.com/bpedro/node-fs
var ncp = require('ncp').ncp; // Recursive directory copy. https://npmjs.org/package/ncp
var winston = require('winston'); // Logging. https://github.com/flatiron/winston

// Update content for the application, as well as the application itself.
exports.ContentUpdater = Backbone.Model.extend({

    defaults: {
        // When the content was last updated.
        downloaded: null,
        updated: null,

        // The remote URL to the root XML.
        remote: null,

        // The final local path for content.
        local: 'content/',

        // The temp path for content.
        temp: 'content.tmp/',

        // A collection of file objects that are being processed.
        files: null,
        needsUpdate: false,
    },

    // A callback to call when updating is complete.
    _callback: null,

    // Download new content to the temp folder.
    download: function(callback) {
        if (!this.get('remote')) {
            return;
        }

        this.set('needsUpdate', false);
        this._callback = callback;
        this._initDirectories(_.bind(function() {
            if (this.get('remote').indexOf('http') === 0) {
                // We're going to pull down an XML file from the web and parse it for other files.
                request(this.get('remote'), _.bind(this._processContentRoot, this));
            } else {
                // We're going to just robocopy from another folder instead.
                this._robocopy(this.get('remote'), path.resolve(this.get('temp')), null, _.bind(function(code) {
                    this.set('needsUpdate', code > 0 && code <= 8);
                    this._callback(code > 8 ? code : 0);
                    if (code > 8) {
                        // Something bad happened.
                        winston.error('Robocopy failed with code ' + code);
                    }
                }, this));
            }
        }, this));
    },

    // Set up the temp and output directories.
    _initDirectories: function(callback) {
        // Make the temp directory.
        fs.mkdir(this.get('temp'), 0777, true, _.bind(function(error) {
            this._handleError('Error creating temp directory.', error);

            fs.exists(this.get('local'), _.bind(function(exists) {
                if (exists) {
                    callback();
                    return;
                }

                // Make the ouput directory.
                fs.mkdir(this.get('local'), 0777, true, _.bind(function(error) {
                    this._handleError('Error creating ouput directory.', error);
                    callback();
                }, this));
            }, this));
        }, this));
    },

    // Process the XML file to extract files to load.
    _processContentRoot: function(error, response, body) {
        if (response.statusCode != 200) {
            this._handleError('Error loading root XML -- bad password?');
            return;
        }

        this._handleError('Error loading root XML.', error);

        // Write the root XML file.
        fs.writeFile(this.get('temp') + 'content.xml', body, _.bind(function(error) {
            this._handleError('Error writing root XML.', error);
            this.set('files', new exports.ContentFiles());

            // Collect any URLs in root file.
            var pattern = new XRegExp(/>(http.*?)</gi);
            var matches = [];
            var match;

            while (true) {
                match = pattern.exec(body);
                if (!match) {
                    break;
                }
                matches.push(match);
            }

            // Figure out what each URL begins with -- this is removed to create the relative path.
            var prefix = '';

            function iterator(element, index, list) {
                return element[1].indexOf(prefix) === 0;
            }

            while (true) {
                prefix += matches[0][1][prefix.length];
                if (!_.every(matches, iterator)) {
                    prefix = prefix.substr(0, prefix.length - 1);
                    break;
                }
            }

            // Build file objects.
            _.each(matches, function(element, index, list) {
                var url = element[1];
                var relativePath = url.substr(prefix.length);
                var file = new exports.ContentFile({
                    url: url,
                    filePath: this.get('local') + relativePath,
                    tempPath: this.get('temp') + relativePath
                });

                file.on('loaded', this._onFileLoaded, this);
                this.get('files').add(file);
            }, this);

            _.each(this.get('files').models, _.bind(this._processFile, this));
        }, this));
    },

    // Determine if the file needs to be loaded, or if it's already up to date.
    _processFile: function(contentFile) {
        fs.exists(contentFile.get('tempPath'), _.bind(function(exists) {
            if (!exists) {
                // The file doesn't exist locally, so download it.
                this._downloadFile(contentFile);
                return;
            }

            // The file does exist locally, check if the remote one is newer.
            fs.stat(contentFile.get('tempPath'), _.bind(function(error, stats) {
                var localFileModified = moment(stats.mtime);
                request({
                    url: contentFile.get('url'),
                    method: 'HEAD'
                }, _.bind(function(error, response, body) {
                    var remoteFileModified = moment(response.headers['last-modified']);
                    if (remoteFileModified.isAfter(localFileModified)) {
                        // The remote file is newer, go get it.
                        this._downloadFile(contentFile);
                        return;
                    }

                    // The local file is newer, flag it as loaded.
                    contentFile.set('progress', 1);
                }, this));
            }, this));
        }, this));
    },

    // Load the file into memory and create its temp output directory.
    _downloadFile: function(contentFile) {
        winston.info('Downloading ' + contentFile.get('url'));

        // Request the file from the network.
        progress(request({
            url: contentFile.get('url'),
            encoding: null // Required for binary files.
        }, _.bind(function(error, response, body) {
            if (!error && response.statusCode != 200) {
                error = response.statusCode;
            }

            this.set('needsUpdate', !error);
            this._handleError('Error loading ' + contentFile.get('url'), error);

            // Create the file's output directory if needed.
            var dir = path.dirname(contentFile.get('tempPath'));
            fs.exists(dir, _.bind(function(exists) {
                if (!exists) {
                    fs.mkdir(dir, 0777, true, _.bind(function(error) {
                        this._handleError('Error creating temp directory for file.', error);
                        this._writeFile(contentFile, body);
                    }, this));
                } else {
                    this._writeFile(contentFile, body);
                }
            }, this));

        }, this))).on('progress', function(state) {

            // Set progress properties on the file.
            contentFile.set('receivedBytes', state.received);
            contentFile.set('totalBytes', state.total);
            contentFile.set('progress', state.percent / 100);
        });
    },

    // When the file is done loading, write it to disk.
    _writeFile: function(contentFile, body) {
        fs.writeFile(contentFile.get('tempPath'), body, null, _.bind(function(error) {
            this._handleError('Error writing file.', error);
            contentFile.set('progress', 1);
        }, this));
    },

    // Called when the progress of a file is set to 1.
    _onFileLoaded: function(contentFile) {
        contentFile.off('loaded', this._onFileLoaded, this);

        // Determine if all files are loaded.
        var filesToGo = _.filter(this.get('files').models, function(file) {
            return file.get('progress') < 1;
        }).length;
        var style = contentFile.get('totalBytes') ? 'loaded' : 'cached';
        winston.info(contentFile.get('url') + ' ' + style + ', ' + filesToGo + ' to go');

        if (!filesToGo) {
            this.set('downloaded', moment());
            this._callback();
        }
    },

    // Copy files to their final destination when all files are loaded.
    update: function(callback) {
        this._callback = callback;
        // Recursive copy from temp to target.
        ncp(this.get('temp'), this.get('local'), _.bind(function(error) {
            this._handleError('Error copying from temp folder.', error);
            this._completed();
        }, this));
    },

    // Notify on completion.
    _completed: function() {
        this.set('updated', moment());
        this.trigger('complete');
        if (this._callback) {
            this._callback();
        }
    },

    // Generic error handling function.
    _handleError: function(message, error) {
        if (!error) {
            return;
        }

        winston.error(message, error);
        if (this._callback) {
            this._callback(error);
        }
    },

    // Robocopy files instead of downloading them.
    _robocopy: function(sourceDir, targetDir, file, callback) {
        // http://technet.microsoft.com/en-us/library/cc733145.aspx
        var args = [
            sourceDir,
            targetDir,
            file,
            '/e', // Copies subdirectories. Note that this option includes empty directories.
            '/v', // Produces verbose output, and shows all skipped files.
            '/np', // Specifies that the progress of the copying operation (the number of files or directories copied so far) will not be displayed.
            '/njs', // Specifies that there is no job summary.
            '/njh', // Specifies that there is no job header.
            '/bytes', // Prints sizes, as bytes.
            '/fft', // Assumes FAT file times (two-second precision).
            '/ndl' // Specifies that directory names are not to be logged.
        ];

        if (!file) {
            // We're copying full directories, remove the file argument.
            args.splice(2, 1);
        } else {
            // We're copying one file, remove the copy-subdirectories argument.
            args.splice(3, 1);
        }

        var copy = child_process.spawn('robocopy', args);
        winston.info('robocopy: robocopy ' + args.join(' '));

        copy.stdout.on('data', _.bind(function(data) {
            var lines = data.toString().split('\r\n');
            _.each(lines, function(line) {
                line = line.trim();
                if (line) {
                    winston.info('robocopy: ' + line);
                }
            });
        }, this));

        copy.stderr.on('data', _.bind(function(data) {
            winston.error('robocopy: ' + data.toString());
        }, this));

        copy.on('close', _.bind(function(code) {
            // The return codes are weird. http://support.microsoft.com/kb/954404
            this.set('downloaded', moment());
            if (callback) {
                callback(code);
            }
        }, this));
    }
});

exports.ContentFile = Backbone.Model.extend({
    defaults: {
        url: null,
        filePath: null,
        tempPath: null,
        progress: 0,
        receivedBytes: 0,
        totalBytes: 0
    },

    initialize: function() {
        this.on('change:progress', function(model, value, options) {
            if (value >= 1) {
                this.trigger('loaded', this);
            }
        }, this);
    }
});

exports.ContentFiles = Backbone.Collection.extend({
    model: exports.ContentFile
});<|endoftext|>


<commit_before>'use strict';
module.exports = {

    "nordelec":{
        "id":1,
        "coor":{
            "lat":1.1,
            "long":1.1
        },
        "events":[]
    }

};<commit_msg>adding locations<commit_after>'use strict';
module.exports = [
    {
        "name": "Kazoom Cafe",
        "address": {
            "street": "160-2169, boul Marcel-Laurin",
            "city": "Saint-Laurent",
            "prov": "QC",
            "pcode": "H4R 1K4"
        },
        "geoCode": {
            "latitude": "45.5192799",
            "longitude": "-73.7026514"
        },
        "phones": [
            {
                "freecall": false,
                "npa": "514",
                "nxx": "331",
                "num": "4386",
                "dispNum": "514-331-4386",
                "type": "primary"
            }
        ],
        "photos": [
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_5b10a100ce65b72c82bc1dd17f3194df4180b40a_0_36699100_1394218980_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_5b10a100ce65b72c82bc1dd17f3194df4180b40a_0_36699100_1394218980_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_499581a860d67ae9a9018d4b402000a65ccec3af_0_67077000_1394218989_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_499581a860d67ae9a9018d4b402000a65ccec3af_0_67077000_1394218989_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_70f974c3bde1c43540f455638e63d743d37ebeb8_0_70947600_1394218993_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_70f974c3bde1c43540f455638e63d743d37ebeb8_0_70947600_1394218993_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_3434c4ff28b562513dffd23db010865f0cc0d51d_0_47727900_1394218997_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_3434c4ff28b562513dffd23db010865f0cc0d51d_0_47727900_1394218997_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_b6b798bddd4d12fa5010d3ff732a8f5e100230ca_0_09711900_1394219002_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_b6b798bddd4d12fa5010d3ff732a8f5e100230ca_0_09711900_1394219002_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_ea91ffdbd82553bf892de7c328b4c9a12114ea19_0_15119200_1394219007_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_ea91ffdbd82553bf892de7c328b4c9a12114ea19_0_15119200_1394219007_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_911256e1b69569e594e63529dad35dc7803f7f97_0_60096800_1394219012_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_911256e1b69569e594e63529dad35dc7803f7f97_0_60096800_1394219012_t.jpg",
                "description": ""
            },
            {
                "url": "http://cdn.media.yp.ca/6359118/pcc_photos_963c10671b750e3781ad1beaaf36b723b6e58e81_0_22610400_1394219017_r.jpg",
                "thmbUrl": "http://cdn.media.yp.ca/6359118/pcc_photos_963c10671b750e3781ad1beaaf36b723b6e58e81_0_22610400_1394219017_t.jpg",
                "description": ""
            }
        ],
        themes:[1,2]
    },
    {

    }
];<|endoftext|>


<commit_before>Wheel.MouseManager = Wheel.EventManager.subclass({
  listen: function() {
    this._super();
    var self = this;
    this.touch = {};

    var onMove = function(e) {
      self.onMove(e);
    };

    // only listen for touchmove when a mousedown event
    // has occurred
    this.$.on(this.startEvent, function(e) {
      self.onStart(e);
      self.$.on(self.moveEvent, onMove);
    });

    // TODO: on end stuff should be removed to for memory managment
    this.$.on(this.endEvent, function(e) {
      self.onEnd(e);
      self.$.off(self.moveEvent, onMove);
    });
  },

  startEvent: 'mousedown',
  moveEvent:  'mousemove',
  endEvent:   'mouseup'
});
<commit_msg>patch possible memory leak in event listeners in TouchManager<commit_after>Wheel.MouseManager = Wheel.EventManager.subclass({
  listen: function() {
    this._super();
    var self = this;
    this.touch = {};

    function onMove(e) {
      self.onMove(e);
    }

    function onEnd(e) {
      self.onEnd(e);
      self.$.off(self.moveEvent, onMove);
      self.$.off(self.endEvent, onEnd);
    }

    // only listen for touchmove when a mousedown event
    // has occurred
    this.$.on(this.startEvent, function(e) {
      self.onStart(e);
      self.$.on(self.moveEvent, onMove);
    });

    this.$.on(this.endEvent, onEnd);
  },

  startEvent: 'mousedown',
  moveEvent:  'mousemove',
  endEvent:   'mouseup'
});
<|endoftext|>


<commit_before>var child_process = require('child_process');

job_carousel = child_process.spawn('/home/bram/src/ebu-tools/carousel/carousel.sh')
job_dasher = child_process.spawn('/home/bram/src/ebu-tools/dasher/dasher.rb')

job_carousel.on('close', function(code, signal) {
  console.log("Carousel job exited with code " + code + " and signal " + signal + ". Aborting.");
  job_dasher.kill(); // Send SIGTERM to other job
})

job_dasher.on('close', function(code, signal) {
  console.log("Dasher job exited with code " + code + " and signal " + signal + ". Aborting.");
  job_carousel.kill(); // Send SIGTERM to other job
})<commit_msg>Call correct DASH script.<commit_after>var child_process = require('child_process');

job_carousel = child_process.spawn('/home/bram/src/ebu-tools/carousel/carousel.sh')
job_dasher = child_process.spawn('/home/bram/src/ebu-tools/dasher/runner.rb')

job_carousel.on('close', function(code, signal) {
  console.log("Carousel job exited with code " + code + " and signal " + signal + ". Aborting.");
  job_dasher.kill(); // Send SIGTERM to other job
})

job_dasher.on('close', function(code, signal) {
  console.log("Dasher job exited with code " + code + " and signal " + signal + ". Aborting.");
  job_carousel.kill(); // Send SIGTERM to other job
})<|endoftext|>