<commit_before>/**
Definition of the FRESHResume class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module fresh-resume.js
*/

(function() {

  var FS = require('fs')
    , extend = require('../utils/extend')
    , validator = require('is-my-json-valid')
    , _ = require('underscore')
    , __ = require('lodash')
    , PATH = require('path')
    , moment = require('moment')
    , XML = require('xml-escape')
    , MD = require('marked')
    , CONVERTER = require('./convert')
    , JRSResume = require('./jrs-resume');

  /**
  A FRESH-style resume in JSON or YAML.
  @class FreshResume
  */
  function FreshResume() {

  }

  /**
  Open and parse the specified FRESH resume sheet. Merge the JSON object model
  onto this Sheet instance with extend() and convert sheet dates to a safe &
  consistent format. Then sort each section by startDate descending.
  */
  FreshResume.prototype.open = function( file, title ) {
    this.imp = { fileName: file };
    this.imp.raw = FS.readFileSync( file, 'utf8' );
    return this.parse( this.imp.raw, title );
  };

  /**
  Save the sheet to disk (for environments that have disk access).
  */
  FreshResume.prototype.save = function( filename ) {
    this.imp.fileName = filename || this.imp.fileName;
    FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
    return this;
  };

  /**
  Save the sheet to disk in a specific format, either FRESH or JSON Resume.
  */
  FreshResume.prototype.saveAs = function( filename, format ) {

    if( format !== 'JRS' ) {
      this.imp.fileName = filename || this.imp.fileName;
      FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
    }
    else {
      var newRep = CONVERTER.toJRS( this );
      FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' );
    }
    return this;
  };

  FreshResume.prototype.dupe = function() {
    var rnew = new FreshResume();
    rnew.parse( this.stringify(), { } );
    return rnew;
  };

  /**
  Convert the supplied object to a JSON string, sanitizing meta-properties along
  the way.
  */
  FreshResume.stringify = function( obj ) {
    function replacer( key,value ) { // Exclude these keys from stringification
      return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
        'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
        function( val ) { return key.trim() === val; }
      ) ? undefined : value;
    }
    return JSON.stringify( obj, replacer, 2 );
  };

  /**
  Create a copy of this resume in which all string fields have been run through
  a transformation function (such as a Markdown filter or XML encoder).
  */
  FreshResume.prototype.transformStrings = function( filt, transformer ) {

    var that = this;
    var ret = this.dupe();

    // TODO: refactor recursion
    function transformStringsInObject( obj, filters ) {

      if( !obj ) return;
      if( moment.isMoment( obj ) ) return;

      if( _.isArray( obj ) ) {
        obj.forEach( function(elem, idx, ar) {
          if( typeof elem === 'string' || elem instanceof String )
            ar[idx] = transformer( null, elem );
          else if (_.isObject(elem))
            transformStringsInObject( elem, filters );
        });
      }
      else if (_.isObject( obj )) {
        Object.keys( obj ).forEach(function(k) {
          if( filters.length && _.contains(filters, k) )
            return;
          var sub = obj[k];
          if( typeof sub === 'string' || sub instanceof String ) {
            obj[k] = transformer( k, sub );
          }
          else if (_.isObject( sub ))
            transformStringsInObject( sub, filters );
        });
      }

    }

    Object.keys( ret ).forEach(function(member){
      if( !filt || !filt.length || !_.contains(filt, member) )
        transformStringsInObject( ret[ member ], filt || [] );
    });

    return ret;
  };

  /**
  Create a copy of this resume in which all fields have been interpreted as
  Markdown.
  */
  FreshResume.prototype.markdownify = function() {

    function MDIN( txt ){
      return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
    }

    function trx(key, val) {
      if( key === 'summary' ) {
        return MD(val);
      }
      return MDIN(val);
    }

    return this.transformStrings( ['skills','url','start','end','date'], trx );
  };

  /**
  Create a copy of this resume in which all fields have been interpreted as
  Markdown.
  */
  FreshResume.prototype.xmlify = function() {

    function trx(key, val) {
      return XML(val);
    }

    return this.transformStrings( [], trx );
  };

  /**
  Convert this object to a JSON string, sanitizing meta-properties along the
  way. Don't override .toString().
  */
  FreshResume.prototype.stringify = function() {
    return FreshResume.stringify( this );
  };

  /**
  Initialize the FreshResume from JSON data.
  Open and parse the specified FRESH resume. Merge the JSON object model onto
  this Sheet instance with extend() and convert sheet dates to a safe &
  consistent format. Then sort each section by startDate descending.
  */
  FreshResume.prototype.parseJSON = function( rep, opts ) {
    // Convert JSON Resume to FRESH if necessary
    if( rep.basics ) {
      rep = CONVERTER.toFRESH( rep );
      rep.imp = rep.imp || { };
      rep.imp.orgFormat = 'JRS';
    }

    // Now apply the resume representation onto this object
    extend( true, this, rep );

    // Set up metadata
    opts = opts || { };
    if( opts.imp === undefined || opts.imp ) {
      this.imp = this.imp || { };
      this.imp.title = (opts.title || this.imp.title) || this.name;
    }
    // Parse dates, sort dates, and calculate computed values
    (opts.date === undefined || opts.date) && _parseDates.call( this );
    (opts.sort === undefined || opts.sort) && this.sort();
    (opts.compute === undefined || opts.compute) && (this.computed = {
       numYears: this.duration(),
       keywords: this.keywords()
    });
    return this;
  };

  /**
  Initialize the the FreshResume from string data.
  */
  FreshResume.prototype.parse = function( stringData, opts ) {
    return this.parseJSON( JSON.parse( stringData ), opts );
  };

  /**
  Return internal metadata. Create if it doesn't exist.
  */
  FreshResume.prototype.imp = function() {
    this.imp = (this.imp || { });
    return this.imp;
  };

  /**
  Return a unique list of all keywords across all skills.
  */
  FreshResume.prototype.keywords = function() {
    var flatSkills = [];
    if( this.skills ) {
      if( this.skills.sets ) {
        flatSkills = flatSkills.concat( this.skills.sets.map(function(sk) { return sk.name;  }) );
      }
      else if( this.skills.list ) {
        flatSkills = flatSkills.concat( this.skills.list.map(function(sk) { return sk.name;  }) );
      }
    }
    return flatSkills;
  },

  /**
  Reset the sheet to an empty state.
  */
  FreshResume.prototype.clear = function( clearMeta ) {
    clearMeta = ((clearMeta === undefined) && true) || clearMeta;
    clearMeta && (delete this.imp);
    delete this.computed; // Don't use Object.keys() here
    delete this.employment;
    delete this.service;
    delete this.education;
    delete this.recognition;
    delete this.reading;
    delete this.writing;
    delete this.interests;
    delete this.skills;
    delete this.social;
  };

  /**
  Get a safe count of the number of things in a section.
  */
  FreshResume.prototype.count = function( obj ) {
    if( !obj ) return 0;
    if( obj.history ) return obj.history.length;
    if( obj.sets ) return obj.sets.length;
    return obj.length || 0;
  };

  /**
  Get the default (empty) sheet.
  */
  FreshResume.default = function() {
    return new FreshResume().open(
      PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
  };

  /**
  Add work experience to the sheet.
  */
  FreshResume.prototype.add = function( moniker ) {
    var defSheet = FreshResume.default();
    var newObject = defSheet[moniker].history ?
      $.extend( true, {}, defSheet[ moniker ].history[0] ) :
      (moniker === 'skills' ?
        $.extend( true, {}, defSheet.skills.sets[0] ) :
        $.extend( true, {}, defSheet[ moniker ][0] ));
    this[ moniker ] = this[ moniker ] || [];
    if( this[ moniker ].history )
      this[ moniker ].history.push( newObject );
    else if( moniker === 'skills' )
      this.skills.sets.push( newObject );
    else
      this[ moniker ].push( newObject );
    return newObject;
  };

  /**
  Determine if the sheet includes a specific social profile (eg, GitHub).
  */
  FreshResume.prototype.hasProfile = function( socialNetwork ) {
    socialNetwork = socialNetwork.trim().toLowerCase();
    return this.social && _.some( this.social, function(p) {
      return p.network.trim().toLowerCase() === socialNetwork;
    });
  };

  /**
  Return the specified network profile.
  */
  FreshResume.prototype.getProfile = function( socialNetwork ) {
    socialNetwork = socialNetwork.trim().toLowerCase();
    return this.social && _.find( this.social, function(sn) {
      return sn.network.trim().toLowerCase() === socialNetwork;
    });
  };

  /**
  Return an array of profiles for the specified network, for when the user
  has multiple eg. GitHub accounts.
  */
  FreshResume.prototype.getProfiles = function( socialNetwork ) {
    socialNetwork = socialNetwork.trim().toLowerCase();
    return this.social && _.filter( this.social, function(sn){
      return sn.network.trim().toLowerCase() === socialNetwork;
    });
  };

  /**
  Determine if the sheet includes a specific skill.
  */
  FreshResume.prototype.hasSkill = function( skill ) {
    skill = skill.trim().toLowerCase();
    return this.skills && _.some( this.skills, function(sk) {
      return sk.keywords && _.some( sk.keywords, function(kw) {
        return kw.trim().toLowerCase() === skill;
      });
    });
  };

  /**
  Validate the sheet against the FRESH Resume schema.
  */
  FreshResume.prototype.isValid = function( info ) {
    var schemaObj = require('fresca');
    var validator = require('is-my-json-valid');
    var validate = validator( schemaObj, { // Note [1]
      formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
    });
    var ret = validate( this );
    if( !ret ) {
      this.imp = this.imp || { };
      this.imp.validationErrors = validate.errors;
    }
    return ret;
  };

  /**
  Calculate the total duration of the sheet. Assumes this.work has been sorted
  by start date descending, perhaps via a call to Sheet.sort().
  @returns The total duration of the sheet's work history, that is, the number
  of years between the start date of the earliest job on the resume and the
  *latest end date of all jobs in the work history*. This last condition is for
  sheets that have overlapping jobs.
  */
  FreshResume.prototype.duration = function() {
    var empHist = __.get(this, 'employment.history');
    if( empHist && empHist.length ) {
      var firstJob = _.last( this.employment.history );
      var careerStart = firstJob.start ? firstJob.safe.start : '';
      if ((typeof careerStart === 'string' || careerStart instanceof String) &&
          !careerStart.trim())
        return 0;
      var careerLast = _.max( this.employment.history, function( w ) {
        return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
      });
      return careerLast.safe.end.diff( careerStart, 'years' );
    }
    return 0;
  };

  /**
  Sort dated things on the sheet by start date descending. Assumes that dates
  on the sheet have been processed with _parseDates().
  */
  FreshResume.prototype.sort = function( ) {

    __.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
    __.get(this, 'education.history') && this.education.history.sort( byDateDesc );
    __.get(this, 'service.history') && this.service.history.sort( byDateDesc );

    // this.awards && this.awards.sort( function(a, b) {
    //   return( a.safeDate.isBefore(b.safeDate) ) ? 1
    //     : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
    // });
    this.writing && this.writing.sort( function(a, b) {
      return( a.safe.date.isBefore(b.safe.date) ) ? 1
        : ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
    });

    function byDateDesc(a,b) {
      return( a.safe.start.isBefore(b.safe.start) ) ? 1
        : ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
    }

  };

  /**
  Convert human-friendly dates into formal Moment.js dates for all collections.
  We don't want to lose the raw textual date as entered by the user, so we store
  the Moment-ified date as a separate property with a prefix of .safe. For ex:
  job.startDate is the date as entered by the user. job.safeStartDate is the
  parsed Moment.js date that we actually use in processing.
  */
  function _parseDates() {

    var _fmt = require('./fluent-date').fmt;
    var that = this;

    // TODO: refactor recursion
    function replaceDatesInObject( obj ) {

      if( !obj ) return;
      if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
        obj.forEach(function(elem){
          replaceDatesInObject( elem );
        });
      }
      else if (typeof obj === 'object') {
        if( obj._isAMomentObject || obj.safe )
         return;
        Object.keys( obj ).forEach(function(key) {
          replaceDatesInObject( obj[key] );
        });
        ['start','end','date'].forEach( function(val) {
          if( (obj[val] !== undefined) && (!obj.safe || !obj.safe[val] )) {
            obj.safe = obj.safe || { };
            obj.safe[ val ] = _fmt( obj[val] );
            if( obj[val] && (val === 'start') && !obj.end ) {
              obj.safe.end = _fmt('current');
            }
          }
        });
      }
    }

    Object.keys( this ).forEach(function(member){
      replaceDatesInObject( that[ member ] );
    });

  }

  /**
  Export the Sheet function/ctor.
  */
  module.exports = FreshResume;

}());

// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
//
//     /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
//
<commit_msg>Improve keyword acquisition.<commit_after>/**
Definition of the FRESHResume class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module fresh-resume.js
*/

(function() {

  var FS = require('fs')
    , extend = require('../utils/extend')
    , validator = require('is-my-json-valid')
    , _ = require('underscore')
    , __ = require('lodash')
    , PATH = require('path')
    , moment = require('moment')
    , XML = require('xml-escape')
    , MD = require('marked')
    , CONVERTER = require('./convert')
    , JRSResume = require('./jrs-resume');

  /**
  A FRESH-style resume in JSON or YAML.
  @class FreshResume
  */
  function FreshResume() {

  }

  /**
  Open and parse the specified FRESH resume sheet. Merge the JSON object model
  onto this Sheet instance with extend() and convert sheet dates to a safe &
  consistent format. Then sort each section by startDate descending.
  */
  FreshResume.prototype.open = function( file, title ) {
    this.imp = { fileName: file };
    this.imp.raw = FS.readFileSync( file, 'utf8' );
    return this.parse( this.imp.raw, title );
  };

  /**
  Save the sheet to disk (for environments that have disk access).
  */
  FreshResume.prototype.save = function( filename ) {
    this.imp.fileName = filename || this.imp.fileName;
    FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
    return this;
  };

  /**
  Save the sheet to disk in a specific format, either FRESH or JSON Resume.
  */
  FreshResume.prototype.saveAs = function( filename, format ) {

    if( format !== 'JRS' ) {
      this.imp.fileName = filename || this.imp.fileName;
      FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
    }
    else {
      var newRep = CONVERTER.toJRS( this );
      FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' );
    }
    return this;
  };

  FreshResume.prototype.dupe = function() {
    var rnew = new FreshResume();
    rnew.parse( this.stringify(), { } );
    return rnew;
  };

  /**
  Convert the supplied object to a JSON string, sanitizing meta-properties along
  the way.
  */
  FreshResume.stringify = function( obj ) {
    function replacer( key,value ) { // Exclude these keys from stringification
      return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
        'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
        function( val ) { return key.trim() === val; }
      ) ? undefined : value;
    }
    return JSON.stringify( obj, replacer, 2 );
  };

  /**
  Create a copy of this resume in which all string fields have been run through
  a transformation function (such as a Markdown filter or XML encoder).
  */
  FreshResume.prototype.transformStrings = function( filt, transformer ) {

    var that = this;
    var ret = this.dupe();

    // TODO: refactor recursion
    function transformStringsInObject( obj, filters ) {

      if( !obj ) return;
      if( moment.isMoment( obj ) ) return;

      if( _.isArray( obj ) ) {
        obj.forEach( function(elem, idx, ar) {
          if( typeof elem === 'string' || elem instanceof String )
            ar[idx] = transformer( null, elem );
          else if (_.isObject(elem))
            transformStringsInObject( elem, filters );
        });
      }
      else if (_.isObject( obj )) {
        Object.keys( obj ).forEach(function(k) {
          if( filters.length && _.contains(filters, k) )
            return;
          var sub = obj[k];
          if( typeof sub === 'string' || sub instanceof String ) {
            obj[k] = transformer( k, sub );
          }
          else if (_.isObject( sub ))
            transformStringsInObject( sub, filters );
        });
      }

    }

    Object.keys( ret ).forEach(function(member){
      if( !filt || !filt.length || !_.contains(filt, member) )
        transformStringsInObject( ret[ member ], filt || [] );
    });

    return ret;
  };

  /**
  Create a copy of this resume in which all fields have been interpreted as
  Markdown.
  */
  FreshResume.prototype.markdownify = function() {

    function MDIN( txt ){
      return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
    }

    function trx(key, val) {
      if( key === 'summary' ) {
        return MD(val);
      }
      return MDIN(val);
    }

    return this.transformStrings( ['skills','url','start','end','date'], trx );
  };

  /**
  Create a copy of this resume in which all fields have been interpreted as
  Markdown.
  */
  FreshResume.prototype.xmlify = function() {

    function trx(key, val) {
      return XML(val);
    }

    return this.transformStrings( [], trx );
  };

  /**
  Convert this object to a JSON string, sanitizing meta-properties along the
  way. Don't override .toString().
  */
  FreshResume.prototype.stringify = function() {
    return FreshResume.stringify( this );
  };

  /**
  Initialize the FreshResume from JSON data.
  Open and parse the specified FRESH resume. Merge the JSON object model onto
  this Sheet instance with extend() and convert sheet dates to a safe &
  consistent format. Then sort each section by startDate descending.
  */
  FreshResume.prototype.parseJSON = function( rep, opts ) {
    // Convert JSON Resume to FRESH if necessary
    if( rep.basics ) {
      rep = CONVERTER.toFRESH( rep );
      rep.imp = rep.imp || { };
      rep.imp.orgFormat = 'JRS';
    }

    // Now apply the resume representation onto this object
    extend( true, this, rep );

    // Set up metadata
    opts = opts || { };
    if( opts.imp === undefined || opts.imp ) {
      this.imp = this.imp || { };
      this.imp.title = (opts.title || this.imp.title) || this.name;
    }
    // Parse dates, sort dates, and calculate computed values
    (opts.date === undefined || opts.date) && _parseDates.call( this );
    (opts.sort === undefined || opts.sort) && this.sort();
    (opts.compute === undefined || opts.compute) && (this.computed = {
       numYears: this.duration(),
       keywords: this.keywords()
    });
    return this;
  };

  /**
  Initialize the the FreshResume from string data.
  */
  FreshResume.prototype.parse = function( stringData, opts ) {
    return this.parseJSON( JSON.parse( stringData ), opts );
  };

  /**
  Return internal metadata. Create if it doesn't exist.
  */
  FreshResume.prototype.imp = function() {
    this.imp = (this.imp || { });
    return this.imp;
  };

  /**
  Return a unique list of all keywords across all skills.
  */
  FreshResume.prototype.keywords = function() {
    var flatSkills = [];
    if( this.skills ) {
      if( this.skills.sets ) {
        flatSkills = this.skills.sets.map(function(sk) { return sk.skills;  })
          .reduce(function(a,b) { return a.concat(b); });
      }
      else if( this.skills.list ) {
        flatSkills = flatSkills.concat( this.skills.list.map(function(sk) { return sk.name;  }) );
      }
      flatSkills = _.uniq( flatSkills );
    }
    return flatSkills;
  },

  /**
  Reset the sheet to an empty state.
  */
  FreshResume.prototype.clear = function( clearMeta ) {
    clearMeta = ((clearMeta === undefined) && true) || clearMeta;
    clearMeta && (delete this.imp);
    delete this.computed; // Don't use Object.keys() here
    delete this.employment;
    delete this.service;
    delete this.education;
    delete this.recognition;
    delete this.reading;
    delete this.writing;
    delete this.interests;
    delete this.skills;
    delete this.social;
  };

  /**
  Get a safe count of the number of things in a section.
  */
  FreshResume.prototype.count = function( obj ) {
    if( !obj ) return 0;
    if( obj.history ) return obj.history.length;
    if( obj.sets ) return obj.sets.length;
    return obj.length || 0;
  };

  /**
  Get the default (empty) sheet.
  */
  FreshResume.default = function() {
    return new FreshResume().open(
      PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
  };

  /**
  Add work experience to the sheet.
  */
  FreshResume.prototype.add = function( moniker ) {
    var defSheet = FreshResume.default();
    var newObject = defSheet[moniker].history ?
      $.extend( true, {}, defSheet[ moniker ].history[0] ) :
      (moniker === 'skills' ?
        $.extend( true, {}, defSheet.skills.sets[0] ) :
        $.extend( true, {}, defSheet[ moniker ][0] ));
    this[ moniker ] = this[ moniker ] || [];
    if( this[ moniker ].history )
      this[ moniker ].history.push( newObject );
    else if( moniker === 'skills' )
      this.skills.sets.push( newObject );
    else
      this[ moniker ].push( newObject );
    return newObject;
  };

  /**
  Determine if the sheet includes a specific social profile (eg, GitHub).
  */
  FreshResume.prototype.hasProfile = function( socialNetwork ) {
    socialNetwork = socialNetwork.trim().toLowerCase();
    return this.social && _.some( this.social, function(p) {
      return p.network.trim().toLowerCase() === socialNetwork;
    });
  };

  /**
  Return the specified network profile.
  */
  FreshResume.prototype.getProfile = function( socialNetwork ) {
    socialNetwork = socialNetwork.trim().toLowerCase();
    return this.social && _.find( this.social, function(sn) {
      return sn.network.trim().toLowerCase() === socialNetwork;
    });
  };

  /**
  Return an array of profiles for the specified network, for when the user
  has multiple eg. GitHub accounts.
  */
  FreshResume.prototype.getProfiles = function( socialNetwork ) {
    socialNetwork = socialNetwork.trim().toLowerCase();
    return this.social && _.filter( this.social, function(sn){
      return sn.network.trim().toLowerCase() === socialNetwork;
    });
  };

  /**
  Determine if the sheet includes a specific skill.
  */
  FreshResume.prototype.hasSkill = function( skill ) {
    skill = skill.trim().toLowerCase();
    return this.skills && _.some( this.skills, function(sk) {
      return sk.keywords && _.some( sk.keywords, function(kw) {
        return kw.trim().toLowerCase() === skill;
      });
    });
  };

  /**
  Validate the sheet against the FRESH Resume schema.
  */
  FreshResume.prototype.isValid = function( info ) {
    var schemaObj = require('fresca');
    var validator = require('is-my-json-valid');
    var validate = validator( schemaObj, { // Note [1]
      formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
    });
    var ret = validate( this );
    if( !ret ) {
      this.imp = this.imp || { };
      this.imp.validationErrors = validate.errors;
    }
    return ret;
  };

  /**
  Calculate the total duration of the sheet. Assumes this.work has been sorted
  by start date descending, perhaps via a call to Sheet.sort().
  @returns The total duration of the sheet's work history, that is, the number
  of years between the start date of the earliest job on the resume and the
  *latest end date of all jobs in the work history*. This last condition is for
  sheets that have overlapping jobs.
  */
  FreshResume.prototype.duration = function() {
    var empHist = __.get(this, 'employment.history');
    if( empHist && empHist.length ) {
      var firstJob = _.last( this.employment.history );
      var careerStart = firstJob.start ? firstJob.safe.start : '';
      if ((typeof careerStart === 'string' || careerStart instanceof String) &&
          !careerStart.trim())
        return 0;
      var careerLast = _.max( this.employment.history, function( w ) {
        return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
      });
      return careerLast.safe.end.diff( careerStart, 'years' );
    }
    return 0;
  };

  /**
  Sort dated things on the sheet by start date descending. Assumes that dates
  on the sheet have been processed with _parseDates().
  */
  FreshResume.prototype.sort = function( ) {

    __.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
    __.get(this, 'education.history') && this.education.history.sort( byDateDesc );
    __.get(this, 'service.history') && this.service.history.sort( byDateDesc );

    // this.awards && this.awards.sort( function(a, b) {
    //   return( a.safeDate.isBefore(b.safeDate) ) ? 1
    //     : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
    // });
    this.writing && this.writing.sort( function(a, b) {
      return( a.safe.date.isBefore(b.safe.date) ) ? 1
        : ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
    });

    function byDateDesc(a,b) {
      return( a.safe.start.isBefore(b.safe.start) ) ? 1
        : ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
    }

  };

  /**
  Convert human-friendly dates into formal Moment.js dates for all collections.
  We don't want to lose the raw textual date as entered by the user, so we store
  the Moment-ified date as a separate property with a prefix of .safe. For ex:
  job.startDate is the date as entered by the user. job.safeStartDate is the
  parsed Moment.js date that we actually use in processing.
  */
  function _parseDates() {

    var _fmt = require('./fluent-date').fmt;
    var that = this;

    // TODO: refactor recursion
    function replaceDatesInObject( obj ) {

      if( !obj ) return;
      if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
        obj.forEach(function(elem){
          replaceDatesInObject( elem );
        });
      }
      else if (typeof obj === 'object') {
        if( obj._isAMomentObject || obj.safe )
         return;
        Object.keys( obj ).forEach(function(key) {
          replaceDatesInObject( obj[key] );
        });
        ['start','end','date'].forEach( function(val) {
          if( (obj[val] !== undefined) && (!obj.safe || !obj.safe[val] )) {
            obj.safe = obj.safe || { };
            obj.safe[ val ] = _fmt( obj[val] );
            if( obj[val] && (val === 'start') && !obj.end ) {
              obj.safe.end = _fmt('current');
            }
          }
        });
      }
    }

    Object.keys( this ).forEach(function(member){
      replaceDatesInObject( that[ member ] );
    });

  }

  /**
  Export the Sheet function/ctor.
  */
  module.exports = FreshResume;

}());

// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
//
//     /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
//
<|endoftext|>


<commit_before>// @flow
import React from "react";

import { richestMimetype, transforms, displayOrder } from "@nteract/transforms";

type Props = {
  expanded: boolean,
  displayOrder: Array<string>,
  transforms: Object,
  bundle: Object,
  metadata: Object,
  theme: string,
  models?: Object
};

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { error: false };
  }
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    this.setState({ error, info });
  }
  render() {
    if (this.state.error) {
      return (

 Error: {this.state.error.toString()}

 {this.state.info.toString()}

 {this.state.info.componentStack.toString()}

      );
    }
    return this.props.children;
  }
}

export default class RichestMime extends React.Component<Props> {
  static defaultProps = {
    transforms,
    displayOrder,
    theme: "light",
    metadata: {},
    bundle: {},
    models: {}
  };

  shouldComponentUpdate(nextProps: Props): boolean {
    // eslint-disable-line class-methods-use-this
    if (
      nextProps &&
      nextProps.theme &&
      this.props &&
      nextProps.theme !== this.props.theme
    ) {
      return true;
    }
    // return false;
    return true;
  }

  render(): ?React$Element<any> | null {
    const mimetype = richestMimetype(
      this.props.bundle,
      this.props.displayOrder,
      this.props.transforms
    );

    if (!mimetype) {
      // If no mimetype is supported, don't return a component
      return null;
    }

    const Transform = this.props.transforms[mimetype];
    const data = this.props.bundle[mimetype];
    const metadata = this.props.metadata[mimetype];
    return (
      <ErrorBoundary>
        <Transform
          expanded={this.props.expanded}
          data={data}
          metadata={metadata}
          theme={this.props.theme}
          models={this.props.models}
        />
      </ErrorBoundary>
    );
  }
}
<commit_msg>actually make flow pass its tests<commit_after>// @flow
import * as React from "react";

import { richestMimetype, transforms, displayOrder } from "@nteract/transforms";

type Props = {
  expanded: boolean,
  displayOrder: Array<string>,
  transforms: Object,
  bundle: Object,
  metadata: Object,
  theme: string,
  models?: Object
};

type ErrorProps = {
  children?: React.Node
};

type State = {
  error?: Error | boolean,
  info?: Object
};

class ErrorBoundary extends React.Component<ErrorProps, State> {
  constructor(props: ErrorProps) {
    super(props);
    this.state = {
      error: false,
      info: {}
    };
  }

  componentDidCatch(error: Error, info: Object) {
    this.setState({ error, info });
  }

  render() {
    if (this.state.error) {
      return (

 Error: {this.state.error.toString()}

 {this.state.info ? this.state.info.toString() : ""}

            {this.state.info ? this.state.info.componentStack.toString() : ""}

      );
    }
    return this.props.children;
  }
}

export default class RichestMime extends React.Component<Props> {
  static defaultProps = {
    transforms,
    displayOrder,
    theme: "light",
    metadata: {},
    bundle: {},
    models: {}
  };

  shouldComponentUpdate(nextProps: Props): boolean {
    // eslint-disable-line class-methods-use-this
    if (
      nextProps &&
      nextProps.theme &&
      this.props &&
      nextProps.theme !== this.props.theme
    ) {
      return true;
    }
    // return false;
    return true;
  }

  render(): ?React$Element<any> | null {
    const mimetype = richestMimetype(
      this.props.bundle,
      this.props.displayOrder,
      this.props.transforms
    );

    if (!mimetype) {
      // If no mimetype is supported, don't return a component
      return null;
    }

    const Transform = this.props.transforms[mimetype];
    const data = this.props.bundle[mimetype];
    const metadata = this.props.metadata[mimetype];
    return (
      <ErrorBoundary>
        <Transform
          expanded={this.props.expanded}
          data={data}
          metadata={metadata}
          theme={this.props.theme}
          models={this.props.models}
        />
      </ErrorBoundary>
    );
  }
}
<|endoftext|>


<commit_before>var _ = require('underscore');
var moment = require('moment');
var d3 = require('d3');

var AGGREGATION_FORMATS = {
  minute: {
    display: 'HH:mm L',
    unit: 'm'
  },
  hour: {
    display: 'HH:mm L',
    unit: 'h'
  },
  day: {
    display: 'Do MMM YYYY',
    unit: 'd'
  },
  week: {
    display: 'Do MMM YYYY',
    unit: 'w'
  },
  month: {
    display: 'MMM YYYY',
    unit: 'M'
  },
  quarter: {
    display: '[Q]Q YYYY',
    unit: 'Q'
  },
  year: {
    display: 'YYYY',
    unit: 'y'
  }
};

var format = {};

format.formatNumber = function (value, unit) {
  if (!_.isNumber(value) || value === 0) {
    return value;
  }

  var format = d3.format('.2s');
  var p = 0;
  var abs_v = Math.abs(value);

  if (value > 1000) {
    value = format(value) + (unit ? ' ' + unit : '');
    return value;
  }

  if (abs_v > 100) {
    p = 0;
  } else if (abs_v > 10) {
    p = 1;
  } else if (abs_v > 1) {
    p = 2;
  } else if (abs_v > 0) {
    p = Math.max(Math.ceil(Math.abs(Math.log(abs_v) / Math.log(10))) + 2, 3);
  }

  value = value.toFixed(p);
  var m = value.match(/(\.0+)$/);
  if (m) {
    value = value.replace(m[0], '');
  }

  return value;
};

format.formatDate = function (value) {
  return d3.time.format('%Y-%m-%d')(value);
};

format.formatTime = function (value) {
  return d3.time.format('%H:%M:%S %d/%m/%Y')(value);
};

format.timeFactory = function (format) {
  return d3.time.format(format);
};

format.formatValue = function (value) {
  if (_.isNumber(value)) {
    return format.formatNumber(value);
  }
  if (_.isDate(value)) {
    return format.formatDate(value);
  }
  return value;
};

format.timestampFactory = function (aggregation, offset) {
  return function (timestamp, localTimezone) {
    if (!_.has(AGGREGATION_FORMATS, aggregation)) {
      return '-';
    }

    var localOffset = localTimezone ? moment.tz(moment.tz.guess()).utcOffset() * 60 : offset;

    var format = AGGREGATION_FORMATS[aggregation];
    var date = moment.unix(timestamp + localOffset).utc();
    return date.format(format.display);
  };
};

module.exports = format;
<commit_msg>linter<commit_after>var _ = require('underscore');
var d3 = require('d3');
var moment = require('moment');
require('moment-timezone');

var AGGREGATION_FORMATS = {
  minute: {
    display: 'HH:mm L',
    unit: 'm'
  },
  hour: {
    display: 'HH:mm L',
    unit: 'h'
  },
  day: {
    display: 'Do MMM YYYY',
    unit: 'd'
  },
  week: {
    display: 'Do MMM YYYY',
    unit: 'w'
  },
  month: {
    display: 'MMM YYYY',
    unit: 'M'
  },
  quarter: {
    display: '[Q]Q YYYY',
    unit: 'Q'
  },
  year: {
    display: 'YYYY',
    unit: 'y'
  }
};

var format = {};

format.formatNumber = function (value, unit) {
  if (!_.isNumber(value) || value === 0) {
    return value;
  }

  var format = d3.format('.2s');
  var p = 0;
  var abs_v = Math.abs(value);

  if (value > 1000) {
    value = format(value) + (unit ? ' ' + unit : '');
    return value;
  }

  if (abs_v > 100) {
    p = 0;
  } else if (abs_v > 10) {
    p = 1;
  } else if (abs_v > 1) {
    p = 2;
  } else if (abs_v > 0) {
    p = Math.max(Math.ceil(Math.abs(Math.log(abs_v) / Math.log(10))) + 2, 3);
  }

  value = value.toFixed(p);
  var m = value.match(/(\.0+)$/);
  if (m) {
    value = value.replace(m[0], '');
  }

  return value;
};

format.formatDate = function (value) {
  return d3.time.format('%Y-%m-%d')(value);
};

format.formatTime = function (value) {
  return d3.time.format('%H:%M:%S %d/%m/%Y')(value);
};

format.timeFactory = function (format) {
  return d3.time.format(format);
};

format.formatValue = function (value) {
  if (_.isNumber(value)) {
    return format.formatNumber(value);
  }
  if (_.isDate(value)) {
    return format.formatDate(value);
  }
  return value;
};

format.timestampFactory = function (aggregation, offset) {
  return function (timestamp, localTimezone) {
    if (!_.has(AGGREGATION_FORMATS, aggregation)) {
      return '-';
    }

    var localOffset = localTimezone ? moment.tz(moment.tz.guess()).utcOffset() * 60 : offset;

    var format = AGGREGATION_FORMATS[aggregation];
    var date = moment.unix(timestamp + localOffset).utc();
    return date.format(format.display);
  };
};

module.exports = format;
<|endoftext|>


<commit_before>/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { isDict, isStream } from "./primitives.js";
import { DecodeStream } from "./stream.js";
import { Jbig2Image } from "./jbig2.js";
import { shadow } from "../shared/util.js";

/**
 * For JBIG2's we use a library to decode these images and
 * the stream behaves like all the other DecodeStreams.
 */
const Jbig2Stream = (function Jbig2StreamClosure() {
  // eslint-disable-next-line no-shadow
  function Jbig2Stream(stream, maybeLength, dict, params) {
    this.stream = stream;
    this.maybeLength = maybeLength;
    this.dict = dict;
    this.params = params;

    DecodeStream.call(this, maybeLength);
  }

  Jbig2Stream.prototype = Object.create(DecodeStream.prototype);

  Object.defineProperty(Jbig2Stream.prototype, "bytes", {
    get() {
      // If `this.maybeLength` is null, we'll get the entire stream.
      return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
    },
    configurable: true,
  });

  Jbig2Stream.prototype.ensureBuffer = function (requested) {
    // No-op, since `this.readBlock` will always parse the entire image and
    // directly insert all of its data into `this.buffer`.
  };

  Jbig2Stream.prototype.readBlock = function () {
    if (this.eof) {
      return;
    }
    const jbig2Image = new Jbig2Image();

    const chunks = [];
    if (isDict(this.params)) {
      const globalsStream = this.params.get("JBIG2Globals");
      if (isStream(globalsStream)) {
        const globals = globalsStream.getBytes();
        chunks.push({ data: globals, start: 0, end: globals.length });
      }
    }
    chunks.push({ data: this.bytes, start: 0, end: this.bytes.length });
    const data = jbig2Image.parseChunks(chunks);
    const dataLength = data.length;

    // JBIG2 had black as 1 and white as 0, inverting the colors
    for (let i = 0; i < dataLength; i++) {
      data[i] ^= 0xff;
    }
    this.buffer = data;
    this.bufferLength = dataLength;
    this.eof = true;
  };

  return Jbig2Stream;
})();

export { Jbig2Stream };
<commit_msg>Convert `src/core/jbig2_stream.js` to use standard classes<commit_after>/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { isDict, isStream } from "./primitives.js";
import { DecodeStream } from "./stream.js";
import { Jbig2Image } from "./jbig2.js";
import { shadow } from "../shared/util.js";

/**
 * For JBIG2's we use a library to decode these images and
 * the stream behaves like all the other DecodeStreams.
 */
class Jbig2Stream extends DecodeStream {
  constructor(stream, maybeLength, dict, params) {
    super(maybeLength);

    this.stream = stream;
    this.maybeLength = maybeLength;
    this.dict = dict;
    this.params = params;
  }

  get bytes() {
    // If `this.maybeLength` is null, we'll get the entire stream.
    return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
  }

  ensureBuffer(requested) {
    // No-op, since `this.readBlock` will always parse the entire image and
    // directly insert all of its data into `this.buffer`.
  }

  readBlock() {
    if (this.eof) {
      return;
    }
    const jbig2Image = new Jbig2Image();

    const chunks = [];
    if (isDict(this.params)) {
      const globalsStream = this.params.get("JBIG2Globals");
      if (isStream(globalsStream)) {
        const globals = globalsStream.getBytes();
        chunks.push({ data: globals, start: 0, end: globals.length });
      }
    }
    chunks.push({ data: this.bytes, start: 0, end: this.bytes.length });
    const data = jbig2Image.parseChunks(chunks);
    const dataLength = data.length;

    // JBIG2 had black as 1 and white as 0, inverting the colors
    for (let i = 0; i < dataLength; i++) {
      data[i] ^= 0xff;
    }
    this.buffer = data;
    this.bufferLength = dataLength;
    this.eof = true;
  }
}

export { Jbig2Stream };
<|endoftext|>


<commit_before>const { expect } = require('chai');
const { stub, restore } = require('sinon');
const { project } = require('src/contexts');
const projects = require('src/projects');

describe('contexts.project', () => {
  afterEach(() => { restore(); });

  describe('access', () => {
    it("should get the project's access context", () => {
      stub(projects, 'get')
        .withArgs(23)
        .returns(Promise.resolve({
          id: 23,
          organization_id: 21
        }));

        return project.access(23)
          .then(res => expect(res).to.deep.equal({
            projectId: 23,
            organizationId: 21
          }));
    });
  });
});
<commit_msg>Use sandbox for stubbing in contexts.project tests<commit_after>const { expect } = require('chai');
const { sandbox } = require('sinon');
const { project } = require('src/contexts');
const projects = require('src/projects');

describe('contexts.project', () => {
  beforeEach(() => {
    this.sandbox = sandbox.create();
  });

  afterEach(() => {
    this.sandbox.restore();
  });

  describe('access', () => {
    it("should get the project's access context", () => {
      this.sandbox.stub(projects, 'get')
        .withArgs(23)
        .returns(Promise.resolve({
          id: 23,
          organization_id: 21
        }));

        return project.access(23)
          .then(res => expect(res).to.deep.equal({
            projectId: 23,
            organizationId: 21
          }));
    });
  });
});
<|endoftext|>


<commit_before>import Vue from 'vue'
import Router from 'vue-router'
import pages from '@/components/pages/pages.vue'
import intro from '@/components/pages/templates/intro'

Vue.use(Router)

export default new Router({
    routes: [
        {
            path: '/',
            name: 'home',
            component: intro
        },
        {
            path: 'articles/:article',
            name: 'article',
            component: pages
        }
    ]
})
<commit_msg>Fixed router path.<commit_after>import Vue from 'vue'
import Router from 'vue-router'
import pages from '@/components/pages/pages.vue'
import intro from '@/components/pages/templates/intro'

Vue.use(Router)

export default new Router({
    routes: [
        {
            path: '/',
            name: 'home',
            component: intro
        },
        {
            path: '/articles/:article',
            name: 'article',
            component: pages
        }
    ]
})
<|endoftext|>


<commit_before>import React from 'react/addons';
import cx from 'classnames';
import TransitionWrapper from '../transition-wrapper/TransitionWrapper';
const { TransitionGroup: ReactTransitionGroup, cloneWithProps } = React.addons;

const Toaster = React.createClass({

  propTypes: {
    children: React.PropTypes.node.isRequired,
    attachTo: React.PropTypes.string,
    transitionStyles: React.PropTypes.object,
    transitionEnterTimeout: React.PropTypes.number.isRequired,
    transitionLeaveTimeout: React.PropTypes.number.isRequired,
    id: React.PropTypes.string,
    className: React.PropTypes.string,
    style: React.PropTypes.object
  },

  getDefaultProps() {
    return {
      transitionGroup: {}
    };
  },

  componentWillMount() {
    this.appendToaster();
    this.renderToaster();
  },

  componentDidMount() {
    const { position } = this.props.attachTo ? this.toaster.style : this.getDOMNode().parentNode.style;
    if (position !== 'relative' && position !== 'absolute') {
      this.logWarning('Toaster\'s parent node should have "position: relative/absolute"');
    }
  },

  componentWillUnmount() {
    this.removeToaster();
  },

  logWarning(log) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(log);
    }
  },

  getTranslationStyle(i) {
    return {
      transform: `translateY(${100 * i}%)`,
      position: 'absolute',
      top: 0,
      right: 0
    };
  },

  getToasts() {
    const { children, transitionStyles, transitionEnterTimeout, transitionLeaveTimeout } = this.props;
    return React.Children.map(children, (el, i) => {
      return (
        <TransitionWrapper
          {...{ transitionStyles, transitionEnterTimeout, transitionLeaveTimeout }}
          style={this.getTranslationStyle(i)}
          key={el.key}>
            {cloneWithProps(el, { uniqueKey: el.key })}
        </TransitionWrapper>
      );
    });
  },

  appendToaster() {
    if (this.props.attachTo) {
      this.toaster = document.getElementById(this.props.attachTo);
    }
  },

  removeToaster() {
    if (this.toaster && this.props.attachTo) {
      this.toaster.innerHTML = ''; // stupid??
    }
  },

  getToaster() {
    const { style: styleProp, id, className } = this.props;
    const style = {
      position: 'absolute',
      right: 0,
      top: 0,
      height: '100%',
      ...styleProp
    };

    return (

        <ReactTransitionGroup {...this.props.transitionGroup}>
          {this.getToasts()}
        </ReactTransitionGroup>

    );
  },

  renderToaster() {
    if (this.props.attachTo) {
      React.render(this.getToaster(), this.toaster);
    }
  },

  render() {
    if (this.props.attachTo) {
      return null;
    } else {
      return this.getToaster();
    }
  },

  componentDidUpdate() {
    this.renderToaster();
  },

  componentWillReceiveProps(nextProps) {
    if (this.props.attachTo !== nextProps.attachTo) {
      this.logWarning('You can\'t change "attachTo" prop after the first render!');
    }
  }

});

export default Toaster;
<commit_msg>grained import from react addons<commit_after>import React from 'react';
import cx from 'classnames';
import ReactTransitionGroup from 'react/lib/ReactTransitionGroup';
import cloneWithProps from 'react/lib/cloneWithProps';
import TransitionWrapper from '../transition-wrapper/TransitionWrapper';

const Toaster = React.createClass({

  propTypes: {
    children: React.PropTypes.node.isRequired,
    attachTo: React.PropTypes.string,
    transitionStyles: React.PropTypes.object,
    transitionEnterTimeout: React.PropTypes.number.isRequired,
    transitionLeaveTimeout: React.PropTypes.number.isRequired,
    id: React.PropTypes.string,
    className: React.PropTypes.string,
    style: React.PropTypes.object
  },

  getDefaultProps() {
    return {
      transitionGroup: {}
    };
  },

  componentWillMount() {
    this.appendToaster();
    this.renderToaster();
  },

  componentDidMount() {
    const { position } = this.props.attachTo ? this.toaster.style : this.getDOMNode().parentNode.style;
    if (position !== 'relative' && position !== 'absolute') {
      this.logWarning('Toaster\'s parent node should have "position: relative/absolute"');
    }
  },

  componentWillUnmount() {
    this.removeToaster();
  },

  logWarning(log) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(log);
    }
  },

  getTranslationStyle(i) {
    return {
      transform: `translateY(${100 * i}%)`,
      position: 'absolute',
      top: 0,
      right: 0
    };
  },

  getToasts() {
    const { children, transitionStyles, transitionEnterTimeout, transitionLeaveTimeout } = this.props;
    return React.Children.map(children, (el, i) => {
      return (
        <TransitionWrapper
          {...{ transitionStyles, transitionEnterTimeout, transitionLeaveTimeout }}
          style={this.getTranslationStyle(i)}
          key={el.key}>
            {cloneWithProps(el, { uniqueKey: el.key })}
        </TransitionWrapper>
      );
    });
  },

  appendToaster() {
    if (this.props.attachTo) {
      this.toaster = document.getElementById(this.props.attachTo);
    }
  },

  removeToaster() {
    if (this.toaster && this.props.attachTo) {
      this.toaster.innerHTML = ''; // stupid??
    }
  },

  getToaster() {
    const { style: styleProp, id, className } = this.props;
    const style = {
      position: 'absolute',
      right: 0,
      top: 0,
      height: '100%',
      ...styleProp
    };

    return (

        <ReactTransitionGroup {...this.props.transitionGroup}>
          {this.getToasts()}
        </ReactTransitionGroup>

    );
  },

  renderToaster() {
    if (this.props.attachTo) {
      React.render(this.getToaster(), this.toaster);
    }
  },

  render() {
    if (this.props.attachTo) {
      return null;
    } else {
      return this.getToaster();
    }
  },

  componentDidUpdate() {
    this.renderToaster();
  },

  componentWillReceiveProps(nextProps) {
    if (this.props.attachTo !== nextProps.attachTo) {
      this.logWarning('You can\'t change "attachTo" prop after the first render!');
    }
  }

});

export default Toaster;
<|endoftext|>


<commit_before>"use strict";

(function() {
  // XXX navbar hack
  function navbarHack() { scrollBy(0, -40) }

  window.addEventListener("hashchange", navbarHack)

  function hop(op, k) {setTimeout(function() {op(k)}, 0)}
  function ignore() {}
  function queue(op) {hop(op, ignore)}

  function par() {
    var ops = Array.prototype.slice.call(arguments)
    return function(k) {
      var n = ops.length
      if (!n) {
        k()
      } else {
        function co() {
          if (!--n)
            k()
        }
        for (var i=0; i<ops.length; ++i)
          ops[i](co)
      }
    }
  }

  function seq() {
    var ops = Array.prototype.slice.call(arguments)
    return function(k) {
      var i = 0
      function lp() {
        if (i < ops.length)
          ops[i++](lp)
        else
          k()
      }
      lp()
    }
  }

  function loadScript(url) {
    return function(k) {
      var script = document.createElement("script")
      script.onload = k
      script.type = "text/javascript"
      script.src = url
      document.getElementsByTagName("head")[0].appendChild(script)
    }
  }

  function wrapWithSpan(elem) {
    var parent = elem.parentElement
    var span = document.createElement("span")
    parent.replaceChild(span, elem)
    span.appendChild(elem)
    return span
  }

  function descriptionOf(elem) {
    if (null === elem)
      return null
    if (elem.getAttribute("class") === "description")
      return elem
    return descriptionOf(elem.parentElement)
  }

  function removeIds(elem) {
    elem.removeAttribute("id")
    for (var i=0, n=elem.childElementCount; i < n; ++i)
      removeIds(elem.children[i])
  }

  var init = function(k) {
    $(document).ready(function() {
      navbarHack()

      $("code").each(function(i, block) {
        hljs.highlightBlock(block)
      })

      var tips = []

      $("a").each(function(i, link) {
        var href = link.getAttribute("href")

        if (href.indexOf("#def:") !== 0)
          return

        var target = document.getElementById(href.substring(1).replace(/%20/g, " "))
        if (!target)
          return

        var desc = descriptionOf(target)
        if (!desc || desc.children.count < 2)
          return

        var code = desc.children[0].children[0].cloneNode(true)
        var p = desc.children[1].cloneNode(true)

        removeIds(code)
        removeIds(p)

        var tooltip = document.createElement("span")
        tooltip.setAttribute("class", "tooltip")

        tooltip.appendChild(code)
        tooltip.appendChild(p)

        tips.push({link: link, tooltip: tooltip})
      })

      tips.forEach(tip => {
        var span = wrapWithSpan(tip.link)
        span.setAttribute("class", "anchor")

        span.appendChild(tip.tooltip)
      })
    })
    k()
  }

  queue(seq(par(seq(loadScript("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"),
                    loadScript("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/languages/fsharp.min.js")),
                loadScript("https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js")),
            init))
})()
<commit_msg>JS fixes<commit_after>"use strict";

(function() {
  // XXX navbar hack
  function navbarHack() { scrollBy(0, -40) }

  window.addEventListener("hashchange", navbarHack)

  function hop(op, k) {setTimeout(function() {op(k)}, 0)}
  function ignore() {}
  function queue(op) {hop(op, ignore)}

  function par() {
    var ops = Array.prototype.slice.call(arguments)
    return function(k) {
      var n = ops.length
      function co() {
        if (!--n)
          k()
      }
      if (!n) {
        k()
      } else {
        for (var i=0; i<ops.length; ++i)
          ops[i](co)
      }
    }
  }

  function seq() {
    var ops = Array.prototype.slice.call(arguments)
    return function(k) {
      var i = 0
      function lp() {
        if (i < ops.length)
          ops[i++](lp)
        else
          k()
      }
      lp()
    }
  }

  function loadScript(url) {
    return function(k) {
      var script = document.createElement("script")
      script.onload = k
      script.type = "text/javascript"
      script.src = url
      document.getElementsByTagName("head")[0].appendChild(script)
    }
  }

  function wrapWithSpan(elem) {
    var parent = elem.parentElement
    var span = document.createElement("span")
    parent.replaceChild(span, elem)
    span.appendChild(elem)
    return span
  }

  function descriptionOf(elem) {
    if (null === elem)
      return null
    if (elem.getAttribute("class") === "description")
      return elem
    return descriptionOf(elem.parentElement)
  }

  function removeIds(elem) {
    elem.removeAttribute("id")
    for (var i=0, n=elem.childElementCount; i < n; ++i)
      removeIds(elem.children[i])
  }

  var init = function(k) {
    $(document).ready(function() {
      navbarHack()

      $("code").each(function(i, block) {
        hljs.highlightBlock(block)
      })

      var tips = []

      $("a").each(function(i, link) {
        var href = link.getAttribute("href")

        if (href.indexOf("#def:") !== 0)
          return

        var target = document.getElementById(href.substring(1).replace(/%20/g, " "))
        if (!target)
          return

        var desc = descriptionOf(target)
        if (!desc || desc.children.count < 2)
          return

        var code = desc.children[0].children[0].cloneNode(true)
        var p = desc.children[1].cloneNode(true)

        removeIds(code)
        removeIds(p)

        var tooltip = document.createElement("span")
        tooltip.setAttribute("class", "tooltip")

        tooltip.appendChild(code)
        tooltip.appendChild(p)

        tips.push({link: link, tooltip: tooltip})
      })

      tips.forEach(function (tip) {
        var span = wrapWithSpan(tip.link)
        span.setAttribute("class", "anchor")

        span.appendChild(tip.tooltip)
      })
    })
    k()
  }

  queue(seq(par(seq(loadScript("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"),
                    loadScript("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/languages/fsharp.min.js")),
                loadScript("https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js")),
            init))
})()
<|endoftext|>


<commit_before>/*
Copyright 2018 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO - update when Workbox v3 is published
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js');

workbox.precaching.precacheAndRoute([
  {
    "url": "img/amp_logo_white.svg",
    "revision": "ff1c832025faf6ebb36c3385ee1434c5"
  },
  {
    "url": "offline.html",
    "revision": "df128d26bb435e2363e54cb03a59117c"
  },
  {
    "url": "icons/icon-128x128.png",
    "revision": "931389ae9534e0116433ba245d7ccbd2"
  },
  {
    "url": "icons/icon-144x144.png",
    "revision": "fcbfa0e31cbe00db1e7e333b0b4085a9"
  },
  {
    "url": "icons/icon-152x152.png",
    "revision": "15c7666e3262c9a01198cc97e3dfeee2"
  },
  {
    "url": "icons/icon-192x192.png",
    "revision": "15bbfa9c5eda938db344c081acc21160"
  },
  {
    "url": "icons/icon-384x384.png",
    "revision": "435fa69761cfca1bdb521ee34e0b8dd7"
  },
  {
    "url": "icons/icon-512x512.png",
    "revision": "85456ca00d684bfe60a52c57b3416b8b"
  },
  {
    "url": "icons/icon-72x72.png",
    "revision": "3fe1e591c8f138f676c0b4d3f9eb58de"
  },
  {
    "url": "icons/icon-96x96.png",
    "revision": "3c0ded96a9d6cde35894280216bfb5d9"
  }
]);

self.addEventListener('install', event => {
  const urls = [
    'https://cdn.ampproject.org/v0.js',
    'https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js',
    'https://cdn.ampproject.org/shadow-v0.js',
    'index.html',
    '/'
  ];
  const cacheName = workbox.core.cacheNames.runtime;
  event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(urls)));
});

workbox.routing.registerRoute(/(index|\/articles\/)(.*)html|(.*)\/$/, args => {
  return workbox.strategies.networkFirst().handle(args).then(response => {
    if (!response) {
      return caches.match('offline.html');
    }
    return response;
  });
});

workbox.routing.registerRoute(/\.(?:js|css|png|gif|jpg|svg)$/,
  workbox.strategies.cacheFirst()
);

workbox.routing.registerRoute(/(.*)cdn\.ampproject\.org(.*)/,
  workbox.strategies.staleWhileRevalidate()
);
<commit_msg>updated generated sw to match updated original<commit_after>/*
Copyright 2018 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO - update when Workbox v3 is published
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js');

workbox.precaching.precacheAndRoute([
  {
    "url": "img/amp_logo_white.svg",
    "revision": "ff1c832025faf6ebb36c3385ee1434c5"
  },
  {
    "url": "offline.html",
    "revision": "df128d26bb435e2363e54cb03a59117c"
  },
  {
    "url": "icons/icon-128x128.png",
    "revision": "931389ae9534e0116433ba245d7ccbd2"
  },
  {
    "url": "icons/icon-144x144.png",
    "revision": "fcbfa0e31cbe00db1e7e333b0b4085a9"
  },
  {
    "url": "icons/icon-152x152.png",
    "revision": "15c7666e3262c9a01198cc97e3dfeee2"
  },
  {
    "url": "icons/icon-192x192.png",
    "revision": "15bbfa9c5eda938db344c081acc21160"
  },
  {
    "url": "icons/icon-384x384.png",
    "revision": "435fa69761cfca1bdb521ee34e0b8dd7"
  },
  {
    "url": "icons/icon-512x512.png",
    "revision": "85456ca00d684bfe60a52c57b3416b8b"
  },
  {
    "url": "icons/icon-72x72.png",
    "revision": "3fe1e591c8f138f676c0b4d3f9eb58de"
  },
  {
    "url": "icons/icon-96x96.png",
    "revision": "3c0ded96a9d6cde35894280216bfb5d9"
  }
]);

self.addEventListener('install', event => {
  const urls = [
    'https://cdn.ampproject.org/v0.js',
    'https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js',
    'https://cdn.ampproject.org/shadow-v0.js',
    'index.html',
    '/'
  ];
  const cacheName = workbox.core.cacheNames.runtime;
  event.waitUntil(caches.open(cacheName).then(cache => cache.addAll(urls)));
});

workbox.routing.registerRoute(/(index|\/articles\/)(.*)html|(.*)\/$/, args => {
  return workbox.strategies.networkFirst().handle(args).then(response => {
    if (!response) {
      return caches.match('offline.html');
    }
    return response;
  });
});

workbox.routing.registerRoute(/\.(?:js|css|png|gif|jpg|svg)$/,
  workbox.strategies.cacheFirst()
);

workbox.routing.registerRoute(/(.*)cdn\.ampproject\.org(.*)/,
  workbox.strategies.staleWhileRevalidate()
);
<|endoftext|>


<commit_before>// Code ©2017 - 2019 Ecstatic Life Inc. All rights reserved.
console.log(window.location.href.indexOf('ecstaticliving.com') > -1 ? 'Welcome to EcstaticLiving.com' : 'TEST code at ', window.location.href)

// Urls
const containsUrl = str => window.location.href.indexOf(str) > -1
const endsWithUrl = str => window.location.href.endsWith(str)

// Page
const page = () => {
  if (containsUrl('/events/'))  					return 'Event'
  if (endsWithUrl('/update'))  						return 'Update'
  return null
}

// Device
const deviceOrientation = () => window.innerWidth > window.innerHeight
	? 'landscape'
	: 'portrait'

const deviceType = () => {
	//	Some large tablets exist, but for all intents and purposes, we’ll treat them as desktops.
	if (
		(deviceOrientation() === 'portrait' && window.innerWidth > 1024)
		|| (deviceOrientation() === 'landscape' && window.innerWidth > 1366)
	) {
		return 'desktop'
	}
	if (
		(deviceOrientation() === 'portrait' && window.innerWidth > 768)
		|| (deviceOrientation() === 'landscape' && window.innerWidth > 1024)
	) {
		return 'large tablet'
	}
	if (
		(deviceOrientation() === 'portrait' && window.innerWidth > 414)
		|| (deviceOrientation() === 'landscape' && window.innerWidth > 736)
	) {
		return 'tablet'
	}
	return 'mobile'
}

// Format input fields
const formatPhone = elem => {
	let intTel = window.intlTelInput(elem, {
		// Need to use cleave.js for validation instead, since IntTel removed formatting-as-you-type: https://github.com/jackocnr/intl-tel-input/issues/346
		formatOnDisplay: false,
		nationalMode: false,
		preferredCountries: ['us', 'ca'],
		separateDialCode: false,
		utilsScript: 'https://ecstaticliving.github.io/ecstaticliving.com/src/other/inttel-utils.js'
	})
	// Uses cleave.js for format-as-you-type validation
	const cleave = new Cleave(elem, {
		phone: true,
		phoneRegionCode: 'us'
	})
	elem.addEventListener("countrychange", e => cleave.phoneRegionCode = intTel.getSelectedCountryData().iso2)
}
const formatDate = elem => {
	new Cleave(elem, {
		date: true,
    delimiter: '/',
    datePattern: ['m', 'd', 'Y']
	})
}
const phoneInputs = document.querySelectorAll('input[name="phone"]')
phoneInputs.forEach(phoneInput => formatPhone(phoneInput))

// Element Collections
const querySelectorAll = (className, i) => document.querySelectorAll(className)[i || 0]
const querySelector = className => document.querySelector(className)
const getAttribute = (elem, attribute) => elem.getAttribute(attribute)
const getElementById = elem => document.getElementById(elem)
const getElementByClassName = (className, i) => document.getElementsByClassName(className)[i || 0]
const getElementsByClassName = className => document.getElementsByClassName(className)
const getElementsByTag = tag => document.getElementsByTagName(tag)

// Window Event Listener
const windowEventListener = (triggers, f) => triggers.forEach(trigger => window.addEventListener(trigger, f))

// Element Event Listeners
const onClick = (elem, f) => elem.addEventListener('click', e => f(e))
const onChange = (elem, f) => elem.addEventListener('change', e => f(e))
const onInput = (elem, f) => elem.addEventListener('input', f)
const onKeyPress = (elem, f) => elem.addEventListener('keypress', e => f(e))
const onLoad = (elem, f) => elem.addEventListener('load', f)
const onOrientationChange = (elem, f) => elem.addEventListener('orientationchange', f)
const onScroll = (elem, f) => elem.addEventListener('scroll', f)
const onSubmit = (elem, f) => elem.addEventListener('submit', f)

// Form behaviours
const formSubmit = elem => elem.submit()
const formErrorInit = elem => { console.log(elem); elem.parsley() }
const formErrorValidation = elem => elem.parsley().validate()
const formReset = elem => elem.reset()

// Element Conditions
const isInput = elem => elem.type === 'input'
const isRadio = elem => elem.type === 'radio'
const isBlank = elem => getValue(elem) === '' && getValue(elem) === ''
const isChecked = elem => elem.checked
const isVisible = elem => elem.style.display

// Element Behaviours
const animateElement = (elem, keyframes, options) => elem.animate(keyframes, options)
const clickElement = elem => elem.click()
const fadeInElement = elem => elem.style.opacity = '1.0'
const fadeOutElement = elem => elem.style.opacity = '0'
const focusElement = elem => elem.focus()
const hideElement = elem => elem.style.display = 'none'
const showElement = elem => elem.style.display = 'block'
const flexElement = elem => {
	elem.style.display = 'flex'
	elem.style.flexDirection = 'column'
}

// Values
const getValue = elem => elem.value
const emptyValue = elem => elem.value === ''
const setValue = (elem, val) => elem.value = val

// Text
const getText = elem => elem.textContent
const setText = (elem, val) => elem.textContent = val
const emptyText = elem => elem.textContent = ''
const properCase = text => text.toLowerCase().charAt(0).toUpperCase() + text.slice(1)

// Select
const emptySelect = elem => {
	for (let i = 0; i < elem.options.length; i++) {
		elem.options[i] = null
	}
}
const appendSelect = (elem, option) => elem.add(option)
const getIndex = elem => elem.selectedIndex

// HTML
const setHtml = (elem, val) => elem.innerHTML = val
const setCss = (elem, css) => elem.className = css

// Check radio
const checkElement = elem => elem.checked = true
const unCheckElement = elem => elem.checked = false

// Combined Behaviours
const emptyHideText = elem => {
	emptyText(elem)
	hideElement(elem)
}
const emptyHideValue = elem => {
	emptyValue(elem)
	hideElement(elem)
}
const showAndScrollTo = elem => {
	showElement(elem)
	window.scrollTo(0, (window.pageYOffset || document.documentElement.scrollTop) - (document.documentElement.clientTop || 0) + 1)
}<commit_msg>006 - minor bug fix<commit_after>// Code ©2017 - 2019 Ecstatic Life Inc. All rights reserved.
console.log(window.location.href.indexOf('ecstaticliving.com') > -1 ? 'Welcome to EcstaticLiving.com' : 'TEST code at ', window.location.href)

// Urls
const containsUrl = str => window.location.href.indexOf(str) > -1
const endsWithUrl = str => window.location.href.endsWith(str)

// Page
const page = () => {
  if (containsUrl('/events/'))  					return 'Event'
  if (endsWithUrl('/update'))  						return 'Update'
  return null
}

// Device
const deviceOrientation = () => window.innerWidth > window.innerHeight
	? 'landscape'
	: 'portrait'

const deviceType = () => {
	//	Some large tablets exist, but for all intents and purposes, we’ll treat them as desktops.
	if (
		(deviceOrientation() === 'portrait' && window.innerWidth > 1024)
		|| (deviceOrientation() === 'landscape' && window.innerWidth > 1366)
	) {
		return 'desktop'
	}
	if (
		(deviceOrientation() === 'portrait' && window.innerWidth > 768)
		|| (deviceOrientation() === 'landscape' && window.innerWidth > 1024)
	) {
		return 'large tablet'
	}
	if (
		(deviceOrientation() === 'portrait' && window.innerWidth > 414)
		|| (deviceOrientation() === 'landscape' && window.innerWidth > 736)
	) {
		return 'tablet'
	}
	return 'mobile'
}

// Format input fields
const formatPhone = elem => {
	let intTel = window.intlTelInput(elem, {
		// Need to use cleave.js for validation instead, since IntTel removed formatting-as-you-type: https://github.com/jackocnr/intl-tel-input/issues/346
		formatOnDisplay: false,
		nationalMode: false,
		preferredCountries: ['us', 'ca'],
		separateDialCode: false,
		utilsScript: 'https://ecstaticliving.github.io/ecstaticliving.com/src/other/inttel-utils.js'
	})
	// Uses cleave.js for format-as-you-type validation
	const cleave = new Cleave(elem, {
		phone: true,
		phoneRegionCode: 'us'
	})
	elem.addEventListener("countrychange", e => cleave.phoneRegionCode = intTel.getSelectedCountryData().iso2)
}
const formatDate = elem => {
	new Cleave(elem, {
		date: true,
    delimiter: '/',
    datePattern: ['m', 'd', 'Y']
	})
}
const phoneInputs = document.querySelectorAll('input[name="phone"]')
phoneInputs.forEach(phoneInput => formatPhone(phoneInput))

// Element Collections
const querySelectorAll = (className, i) => document.querySelectorAll(className)[i || 0]
const querySelector = className => document.querySelector(className)
const getAttribute = (elem, attribute) => elem.getAttribute(attribute)
const getElementById = elem => document.getElementById(elem)
const getElementByClassName = (className, i) => document.getElementsByClassName(className)[i || 0]
const getElementsByClassName = className => document.getElementsByClassName(className)
const getElementsByTag = tag => document.getElementsByTagName(tag)

// Window Event Listener
const windowEventListener = (triggers, f) => triggers.forEach(trigger => window.addEventListener(trigger, f))

// Element Event Listeners
const onClick = (elem, f) => elem.addEventListener('click', e => f(e))
const onChange = (elem, f) => elem.addEventListener('change', e => f(e))
const onInput = (elem, f) => elem.addEventListener('input', f)
const onKeyPress = (elem, f) => elem.addEventListener('keypress', e => f(e))
const onLoad = (elem, f) => elem.addEventListener('load', f)
const onOrientationChange = (elem, f) => elem.addEventListener('orientationchange', f)
const onScroll = (elem, f) => elem.addEventListener('scroll', f)
const onSubmit = (elem, f) => elem.addEventListener('submit', f)

// Form behaviours
const formSubmit = elem => elem.submit()
const formErrorInit = elem => elem.parsley()
const formErrorValidation = elem => {
	console.log(elem)
	elem.parsley().validate()
}
const formReset = elem => elem.reset()

// Element Conditions
const isInput = elem => elem.type === 'input'
const isRadio = elem => elem.type === 'radio'
const isBlank = elem => getValue(elem) === '' && getValue(elem) === ''
const isChecked = elem => elem.checked
const isVisible = elem => elem.style.display

// Element Behaviours
const animateElement = (elem, keyframes, options) => elem.animate(keyframes, options)
const clickElement = elem => elem.click()
const fadeInElement = elem => elem.style.opacity = '1.0'
const fadeOutElement = elem => elem.style.opacity = '0'
const focusElement = elem => elem.focus()
const hideElement = elem => elem.style.display = 'none'
const showElement = elem => elem.style.display = 'block'
const flexElement = elem => {
	elem.style.display = 'flex'
	elem.style.flexDirection = 'column'
}

// Values
const getValue = elem => elem.value
const emptyValue = elem => elem.value === ''
const setValue = (elem, val) => elem.value = val

// Text
const getText = elem => elem.textContent
const setText = (elem, val) => elem.textContent = val
const emptyText = elem => elem.textContent = ''
const properCase = text => text.toLowerCase().charAt(0).toUpperCase() + text.slice(1)

// Select
const emptySelect = elem => {
	for (let i = 0; i < elem.options.length; i++) {
		elem.options[i] = null
	}
}
const appendSelect = (elem, option) => elem.add(option)
const getIndex = elem => elem.selectedIndex

// HTML
const setHtml = (elem, val) => elem.innerHTML = val
const setCss = (elem, css) => elem.className = css

// Check radio
const checkElement = elem => elem.checked = true
const unCheckElement = elem => elem.checked = false

// Combined Behaviours
const emptyHideText = elem => {
	emptyText(elem)
	hideElement(elem)
}
const emptyHideValue = elem => {
	emptyValue(elem)
	hideElement(elem)
}
const showAndScrollTo = elem => {
	showElement(elem)
	window.scrollTo(0, (window.pageYOffset || document.documentElement.scrollTop) - (document.documentElement.clientTop || 0) + 1)
}<|endoftext|>


<commit_before>/**
 * libjass
 *
 * https://github.com/Arnavion/libjass
 *
 * Copyright 2013 Arnav Singh
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(["intern!tdd", "intern/chai!assert", "require", "intern/dojo/node!leadfoot/helpers/pollUntil"], function (tdd, assert, require, pollUntil) {
	tdd.suite("Auto clock", function () {
		tdd.test("Operations", function () {
		this.remote.session.setExecuteAsyncTimeout(10000);

		return this.remote
			.get(require.toUrl("tests/support/browser-test-page.html"))
			.then(pollUntil('return (document.readyState === "complete") ? true : null;'), 100)
			.execute(function () {
				window.driverStartTime = 0;
				window.driverStartTimeAt = new Date();
				window.driver = function () {
					return (new Date() - driverStartTimeAt) / 1000 + driverStartTime;
				};

				window.clock = new libjass.renderers.AutoClock(driver);

				window.events = [];

				clock.addEventListener(libjass.renderers.ClockEvent.Play, function () {
					events.push("play");
				});

				clock.addEventListener(libjass.renderers.ClockEvent.Tick, function () {
					events.push("tick");
				});

				clock.addEventListener(libjass.renderers.ClockEvent.Pause, function () {
					events.push("pause");
				});

				clock.addEventListener(libjass.renderers.ClockEvent.Stop, function () {
					events.push("stop");
				});

				return { enabled: clock.enabled, paused: clock.paused, rate: clock.rate, events: events.slice() };
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, true);
				assert.strictEqual(clock.rate, 1);
				assert.deepEqual(clock.events, []);
			})
			.executeAsync(function (callback) {
				clock.play();

				setTimeout(function () {
					callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
				}, 1000);
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, false);

				assert(clock.events.length >= 2);
				assert.strictEqual(clock.events[0], "play");
				for (var i = 1; i < clock.events.length; i++) {
					assert.strictEqual(clock.events[i], "tick");
				}
			})
			.execute(function () {
				events = [];

				clock.disable();

				return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, false);
				assert.strictEqual(clock.paused, true);
				assert.deepEqual(clock.events, ["pause", "stop"]);
			})
			.execute(function () {
				events = [];

				clock.enable();

				return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, true);
				assert.deepEqual(clock.events, []);
			})
			.executeAsync(function (callback) {
				clock.play();

				setTimeout(function () {
					callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
				}, 1000);
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, false);

				assert(clock.events.length >= 2);
				assert.strictEqual(clock.events[0], "play");
				for (var i = 1; i < clock.events.length; i++) {
					assert.strictEqual(clock.events[i], "tick");
				}
			})
			.execute(function () {
				events = [];

				driverStartTime = 30000;
				clock.seeking();

				var result = { enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() };
				events = [];
				return result;
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, true);
				assert(clock.currentTime >= 30);
				assert.deepEqual(clock.events, ["pause", "stop", "play", "tick", "pause"]);
			})
			.executeAsync(function (callback) {
				setTimeout(function () {
					callback({ enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() });
				}, 1000);
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, false);
				assert(clock.currentTime >= 31);

				assert(clock.events.length >= 2);
				assert.strictEqual(clock.events[0], "play");
				for (var i = 1; i < clock.events.length; i++) {
					assert.strictEqual(clock.events[i], "tick");
				}
			});
		});
	});
});
<commit_msg>Removed unnecessary call to play(). Enabling an AutoClock should also make it start playing automatically if the clock source is providing ticks.<commit_after>/**
 * libjass
 *
 * https://github.com/Arnavion/libjass
 *
 * Copyright 2013 Arnav Singh
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(["intern!tdd", "intern/chai!assert", "require", "intern/dojo/node!leadfoot/helpers/pollUntil"], function (tdd, assert, require, pollUntil) {
	tdd.suite("Auto clock", function () {
		tdd.test("Operations", function () {
		this.remote.session.setExecuteAsyncTimeout(10000);

		return this.remote
			.get(require.toUrl("tests/support/browser-test-page.html"))
			.then(pollUntil('return (document.readyState === "complete") ? true : null;'), 100)
			.execute(function () {
				window.driverStartTime = 0;
				window.driverStartTimeAt = new Date();
				window.driver = function () {
					return (new Date() - driverStartTimeAt) / 1000 + driverStartTime;
				};

				window.clock = new libjass.renderers.AutoClock(driver);

				window.events = [];

				clock.addEventListener(libjass.renderers.ClockEvent.Play, function () {
					events.push("play");
				});

				clock.addEventListener(libjass.renderers.ClockEvent.Tick, function () {
					events.push("tick");
				});

				clock.addEventListener(libjass.renderers.ClockEvent.Pause, function () {
					events.push("pause");
				});

				clock.addEventListener(libjass.renderers.ClockEvent.Stop, function () {
					events.push("stop");
				});

				return { enabled: clock.enabled, paused: clock.paused, rate: clock.rate, events: events.slice() };
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, true);
				assert.strictEqual(clock.rate, 1);
				assert.deepEqual(clock.events, []);
			})
			.executeAsync(function (callback) {
				clock.play();

				setTimeout(function () {
					callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
				}, 1000);
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, false);

				assert(clock.events.length >= 2);
				assert.strictEqual(clock.events[0], "play");
				for (var i = 1; i < clock.events.length; i++) {
					assert.strictEqual(clock.events[i], "tick");
				}
			})
			.execute(function () {
				events = [];

				clock.disable();

				return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, false);
				assert.strictEqual(clock.paused, true);
				assert.deepEqual(clock.events, ["pause", "stop"]);
			})
			.execute(function () {
				events = [];

				clock.enable();

				return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, true);
				assert.deepEqual(clock.events, []);
			})
			.executeAsync(function (callback) {
				setTimeout(function () {
					callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
				}, 1000);
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, false);

				assert(clock.events.length >= 2);
				assert.strictEqual(clock.events[0], "play");
				for (var i = 1; i < clock.events.length; i++) {
					assert.strictEqual(clock.events[i], "tick");
				}
			})
			.execute(function () {
				events = [];

				driverStartTime = 30000;
				clock.seeking();

				var result = { enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() };
				events = [];
				return result;
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, true);
				assert(clock.currentTime >= 30);
				assert.deepEqual(clock.events, ["pause", "stop", "play", "tick", "pause"]);
			})
			.executeAsync(function (callback) {
				setTimeout(function () {
					callback({ enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() });
				}, 1000);
			})
			.then(function (clock) {
				assert.strictEqual(clock.enabled, true);
				assert.strictEqual(clock.paused, false);
				assert(clock.currentTime >= 31);

				assert(clock.events.length >= 2);
				assert.strictEqual(clock.events[0], "play");
				for (var i = 1; i < clock.events.length; i++) {
					assert.strictEqual(clock.events[i], "tick");
				}
			});
		});
	});
});
<|endoftext|>


<commit_before>/**
 * Default Configuration
 *
 * Extend the class exported from this module to create your own local
 * configuration objects or use this directly.
 * A configuration file is any ES6 file that exports a single default
 * object containing all of the configuration options.
 *
 * Avoid requiring this file into any file that might need this one.
 * In general game logic should never need this, a config object should
 * be passed around through constructors.
 *
 * Try to keep the structure as flat as possible while grouping things logically
 */

import COLORS from '../constants/colors';
import GAMES from '../constants/games';

export default class DefaultConfig {
  constructor({
    me = "",
    sculpture1 = "sculpture1",
    sculpture2 = "sculpture2",
    sculpture3 = "sculpture3",
  } = {}) {
    this.defaultSculptureId = 'anyware'; // Read-only sculpture
    this.sculpture1 = sculpture1;
    this.sculpture2 = sculpture2;
    this.sculpture3 = sculpture3;

    // The ID of this sculpture
    this.me = me || this.defaultSculptureId;

    // Local sculptures will time out after this number of minutes without interaction
    this.ACTIVITY_TIMEOUT = 300;

    // A mapping between sculpture IDs and the colors that represent them
    this.COLORS = {
      LOCATIONS: {
        // sculpture : color
        [this.sculpture1]: COLORS.SCULPTURE1,
        [this.sculpture2]: COLORS.SCULPTURE2,
        [this.sculpture3]: COLORS.SCULPTURE3
      },
      ERROR: 'error'
    };

    // The sequence of the games to be run. The first game is run on startup
    this.GAMES_SEQUENCE = [
      GAMES.HANDSHAKE,
      GAMES.MOLE,
      GAMES.DISK,
      GAMES.SIMON
    ];

    /******* LIGHTS  ********************/
    this.LIGHTS = {
      // Name : strip Id (corresponds to hardware)
      STRIP_A: '0',
      STRIP_B: '1',
      STRIP_C: '2',
      RGB_STRIPS: '3',
      HANDSHAKE_STRIP: '5',
      ART_LIGHTS_STRIP: '6'
    };
    this.GAME_STRIPS = [
      this.LIGHTS.STRIP_A,
      this.LIGHTS.STRIP_B,
      this.LIGHTS.STRIP_C
    ];
    this.PANELS = {
      // stripId : [all associated panel Ids]
      [this.LIGHTS.STRIP_A]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
      [this.LIGHTS.STRIP_B]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
      [this.LIGHTS.STRIP_C]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    };

    // These settings effect the default behaviour of panels outside of
    // any custom logic in any of the games
    this.PANEL_DEFAULTS = {
      ACTIVE_INTENSITY: 100,
      INACTIVE_INTENSITY: 0
    };

    /******* GAMES CONFIGURATION *******/

    this.HANDSHAKE_GAME = {
      TRANSITION_OUT_TIME: 4000 // Time (ms) from handshake is touched until we start the next game
    };
    this.MOLE_GAME = {
      GAME_END: 30,
      INITIAL_PANELS: [
        {stripId: this.LIGHTS.STRIP_A, panelId: '3'},
        {stripId: this.LIGHTS.STRIP_A, panelId: '7'},
        {stripId: this.LIGHTS.STRIP_C, panelId: '6'},
        {stripId: this.LIGHTS.STRIP_B, panelId: '5'}
      ],
      NUM_ACTIVE_PANELS: {
        10: 1, // At panelCount of 10, increase # of simultaneusly active panels
        20: 1,
        25: -1, // At panelCount of 25, decrease # of simultaneusly active panels
        27: -1
      },
      PANEL_LIFETIME: [
        {count: 0, range: [10, 10]}, // Initial timeout
        {count: 4, range: [4, 6]}, // At panelCount of 4, set panel lifetime to 4-6 seconds. Gradually interpolate to next timeout level
        {count: 20, range: [2, 3]},
        {count: 30, range: [1.5, 2]}
      ],
      // How long to wait before enabling the next panel, on success
      PANEL_SUCCESS_DELAY: 1000,
      // How long to wait before enabling the next panel, on automatic panel move
      PANEL_MOVE_DELAY: 200,
      // The intensity to use on active panels
      ACTIVE_PANEL_INTENSITY: 100,
      // The intensity to use on inactive panels
      INACTIVE_PANEL_INTENSITY: 0,
      // The intensity to use on ignored panels (panels turned to location color)
      COLORED_PANEL_INTENSITY: 75,
      // We don't use failure sounds in the default setup due to too many accidental touches
      ENABLE_FAILURE_SOUND: false
    };

    this.DISK_GAME = {
      MAX_SPEED: 360 / 5, // degrees/sec
      RELATIVE_TOLERANCE: 5, // degrees tolerance for disks relative to each other
      ABSOLUTE_TOLERANCE: 8, // degrees tolerance for the absolute disk positions
      // The intensity of the panels that the user can use to play the sequence
      CONTROL_PANEL_COLOR: COLORS.WHITE,
      CONTROL_PANEL_INTENSITY: 20,
      CONFLICT_INTENSITY: 20,
      ACTIVE_CONTROL_PANEL_INTENSITY: 100,
      SHADOW_LIGHTS: {
        // stripId: [panelId..]
        '6': ['0', '1', '2']
      },
      SHADOW_LIGHT_INTENSITY: 100,
      LEVELS: [
        // level 0
        {
          marker0: {
            disk0: [310, 156],
            disk1: [133, 48],
            disk2: [168, 0],
          },
          marker1: {
            disk0: [347, 85],
            disk1: [211, 265],
            disk2: [165, 291],
          },
          marker2: {
            disk0: [352, 173],
            disk1: [168, 270],
            disk2: [182, 235],
          },
        },
      ],
      CONTROL_MAPPINGS: {
        STRIP_TO_DISK: {
          [this.LIGHTS.STRIP_A]: 'disk0',
          [this.LIGHTS.STRIP_B]: 'disk1',
          [this.LIGHTS.STRIP_C]: 'disk2',
        },
      }
    };

    this.SIMON_GAME = {
      PATTERN_LEVELS: [
        // level 0 sequence
        {
          stripId: this.LIGHTS.STRIP_A,
          // Each array of panel IDs is lit up one at a time
          // Each array within this array is called a "frame" in the "sequence"
          panelSequence: [['3'], ['5'], ['7']],
          frameDelay: 750 // Overriding default frame delay to make first level slower
        },
        // level 1 sequence
        {
          stripId: '1',
          panelSequence: [['1'], ['8'], ['5']]
        },
        // level 2 sequence
        {
          stripId: '2',
          panelSequence: [['3'], ['6'], ['2'], ['9']]
        }
      ],
      // The intensity of the panels when they are pressed or when the sequence is playing
      TARGET_PANEL_INTENSITY: 100,
      // The intensity of the panels that the user can use to play the sequence
      AVAILABLE_PANEL_INTENSITY: 1,
      // The delay in ms between sequence frames
      SEQUENCE_ANIMATION_FRAME_DELAY: 500,
      // The delay in ms to wait before replaying the sequence
      // Only replayed if no input is received from the user
      DELAY_BETWEEN_PLAYS: 5000,
      // The time after input to wait for the user to finish the sequence
      INPUT_TIMEOUT: 10000,
      // The default color to set the panels to when
      DEFAULT_SIMON_PANEL_COLOR: COLORS.WHITE,
      // Wait while playing final sound
      TRANSITION_OUT_TIME: 10000
    };
  }

  getLocationColor(loc) {
    return this.COLORS.LOCATIONS[loc];
  }
}

<commit_msg>Adjusted mole game start panels<commit_after>/**
 * Default Configuration
 *
 * Extend the class exported from this module to create your own local
 * configuration objects or use this directly.
 * A configuration file is any ES6 file that exports a single default
 * object containing all of the configuration options.
 *
 * Avoid requiring this file into any file that might need this one.
 * In general game logic should never need this, a config object should
 * be passed around through constructors.
 *
 * Try to keep the structure as flat as possible while grouping things logically
 */

import COLORS from '../constants/colors';
import GAMES from '../constants/games';

export default class DefaultConfig {
  constructor({
    me = "",
    sculpture1 = "sculpture1",
    sculpture2 = "sculpture2",
    sculpture3 = "sculpture3",
  } = {}) {
    this.defaultSculptureId = 'anyware'; // Read-only sculpture
    this.sculpture1 = sculpture1;
    this.sculpture2 = sculpture2;
    this.sculpture3 = sculpture3;

    // The ID of this sculpture
    this.me = me || this.defaultSculptureId;

    // Local sculptures will time out after this number of minutes without interaction
    this.ACTIVITY_TIMEOUT = 300;

    // A mapping between sculpture IDs and the colors that represent them
    this.COLORS = {
      LOCATIONS: {
        // sculpture : color
        [this.sculpture1]: COLORS.SCULPTURE1,
        [this.sculpture2]: COLORS.SCULPTURE2,
        [this.sculpture3]: COLORS.SCULPTURE3
      },
      ERROR: 'error'
    };

    // The sequence of the games to be run. The first game is run on startup
    this.GAMES_SEQUENCE = [
      GAMES.HANDSHAKE,
      GAMES.MOLE,
      GAMES.DISK,
      GAMES.SIMON
    ];

    /******* LIGHTS  ********************/
    this.LIGHTS = {
      // Name : strip Id (corresponds to hardware)
      STRIP_A: '0',
      STRIP_B: '1',
      STRIP_C: '2',
      RGB_STRIPS: '3',
      HANDSHAKE_STRIP: '5',
      ART_LIGHTS_STRIP: '6'
    };
    this.GAME_STRIPS = [
      this.LIGHTS.STRIP_A,
      this.LIGHTS.STRIP_B,
      this.LIGHTS.STRIP_C
    ];
    this.PANELS = {
      // stripId : [all associated panel Ids]
      [this.LIGHTS.STRIP_A]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
      [this.LIGHTS.STRIP_B]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
      [this.LIGHTS.STRIP_C]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    };

    // These settings effect the default behaviour of panels outside of
    // any custom logic in any of the games
    this.PANEL_DEFAULTS = {
      ACTIVE_INTENSITY: 100,
      INACTIVE_INTENSITY: 0
    };

    /******* GAMES CONFIGURATION *******/

    this.HANDSHAKE_GAME = {
      TRANSITION_OUT_TIME: 4000 // Time (ms) from handshake is touched until we start the next game
    };
    this.MOLE_GAME = {
      GAME_END: 30,
      INITIAL_PANELS: [
        {stripId: this.LIGHTS.STRIP_C, panelId: '3'},
        {stripId: this.LIGHTS.STRIP_C, panelId: '7'},
        {stripId: this.LIGHTS.STRIP_B, panelId: '6'},
        {stripId: this.LIGHTS.STRIP_A, panelId: '5'}
      ],
      NUM_ACTIVE_PANELS: {
        10: 1, // At panelCount of 10, increase # of simultaneusly active panels
        20: 1,
        25: -1, // At panelCount of 25, decrease # of simultaneusly active panels
        27: -1
      },
      PANEL_LIFETIME: [
        {count: 0, range: [10, 10]}, // Initial timeout
        {count: 4, range: [4, 6]}, // At panelCount of 4, set panel lifetime to 4-6 seconds. Gradually interpolate to next timeout level
        {count: 20, range: [2, 3]},
        {count: 30, range: [1.5, 2]}
      ],
      // How long to wait before enabling the next panel, on success
      PANEL_SUCCESS_DELAY: 1000,
      // How long to wait before enabling the next panel, on automatic panel move
      PANEL_MOVE_DELAY: 200,
      // The intensity to use on active panels
      ACTIVE_PANEL_INTENSITY: 100,
      // The intensity to use on inactive panels
      INACTIVE_PANEL_INTENSITY: 0,
      // The intensity to use on ignored panels (panels turned to location color)
      COLORED_PANEL_INTENSITY: 75,
      // We don't use failure sounds in the default setup due to too many accidental touches
      ENABLE_FAILURE_SOUND: false
    };

    this.DISK_GAME = {
      MAX_SPEED: 360 / 5, // degrees/sec
      RELATIVE_TOLERANCE: 5, // degrees tolerance for disks relative to each other
      ABSOLUTE_TOLERANCE: 8, // degrees tolerance for the absolute disk positions
      // The intensity of the panels that the user can use to play the sequence
      CONTROL_PANEL_COLOR: COLORS.WHITE,
      CONTROL_PANEL_INTENSITY: 20,
      CONFLICT_INTENSITY: 20,
      ACTIVE_CONTROL_PANEL_INTENSITY: 100,
      SHADOW_LIGHTS: {
        // stripId: [panelId..]
        '6': ['0', '1', '2']
      },
      SHADOW_LIGHT_INTENSITY: 100,
      LEVELS: [
        // level 0
        {
          marker0: {
            disk0: [310, 156],
            disk1: [133, 48],
            disk2: [168, 0],
          },
          marker1: {
            disk0: [347, 85],
            disk1: [211, 265],
            disk2: [165, 291],
          },
          marker2: {
            disk0: [352, 173],
            disk1: [168, 270],
            disk2: [182, 235],
          },
        },
      ],
      CONTROL_MAPPINGS: {
        STRIP_TO_DISK: {
          [this.LIGHTS.STRIP_A]: 'disk0',
          [this.LIGHTS.STRIP_B]: 'disk1',
          [this.LIGHTS.STRIP_C]: 'disk2',
        },
      }
    };

    this.SIMON_GAME = {
      PATTERN_LEVELS: [
        // level 0 sequence
        {
          stripId: this.LIGHTS.STRIP_A,
          // Each array of panel IDs is lit up one at a time
          // Each array within this array is called a "frame" in the "sequence"
          panelSequence: [['3'], ['5'], ['7']],
          frameDelay: 750 // Overriding default frame delay to make first level slower
        },
        // level 1 sequence
        {
          stripId: '1',
          panelSequence: [['1'], ['8'], ['5']]
        },
        // level 2 sequence
        {
          stripId: '2',
          panelSequence: [['3'], ['6'], ['2'], ['9']]
        }
      ],
      // The intensity of the panels when they are pressed or when the sequence is playing
      TARGET_PANEL_INTENSITY: 100,
      // The intensity of the panels that the user can use to play the sequence
      AVAILABLE_PANEL_INTENSITY: 1,
      // The delay in ms between sequence frames
      SEQUENCE_ANIMATION_FRAME_DELAY: 500,
      // The delay in ms to wait before replaying the sequence
      // Only replayed if no input is received from the user
      DELAY_BETWEEN_PLAYS: 5000,
      // The time after input to wait for the user to finish the sequence
      INPUT_TIMEOUT: 10000,
      // The default color to set the panels to when
      DEFAULT_SIMON_PANEL_COLOR: COLORS.WHITE,
      // Wait while playing final sound
      TRANSITION_OUT_TIME: 10000
    };
  }

  getLocationColor(loc) {
    return this.COLORS.LOCATIONS[loc];
  }
}

<|endoftext|>