<commit_before>'use strict';

const AmfParser = require('./amf-parser');
const BufferUtils = require('../buffer-utils');

const Movie = require('../movie');
const AudioTrack = require('../audio-track');
const VideoTrack = require('../video-track');
const AudioSample = require('../audio-sample');
const VideoSample = require('../video-sample');
const SourceReader = require('../readers/source-reader');
const CodecParser = require('../codecs/parser');

const MOVIE_TIMESCALE = 1000;
const HEADER_SIZE = 9;
const HEADER_PREFIX = 'FLV';
const HEADER_VERSION = 1;

const TYPE_SCRIPT = 18;
const TYPE_VIDEO = 9;
const TYPE_AUDIO = 8;

const AUDIO_FORMAT_AAC = 10;
const VIDEO_FORMAT_H264 = 7;

class Parser {

    /**
     * Parse FLV file
     * @param {(int|Buffer)} source
     * @returns {Movie}
     */
    static parse(source) {
        let reader = SourceReader.create(source);
        let sourceSize = reader.size();

        // Parse header
        let bufHeader = Buffer.allocUnsafe(HEADER_SIZE);
        reader.read(bufHeader, 0);
        if (bufHeader.toString('ascii', 0, 3) !== HEADER_PREFIX || bufHeader[3] !== HEADER_VERSION) {
            throw new Error('FLV header not found');
        }
        let pos = bufHeader[8];

        // Create movie
        let movie = new Movie();
        movie.timescale = MOVIE_TIMESCALE;
        let videoTrack = new VideoTrack();
        videoTrack.timescale = MOVIE_TIMESCALE;
        let audioTrack = new AudioTrack();
        audioTrack.timescale = MOVIE_TIMESCALE;

        // Parse tags
        let bufType = Buffer.allocUnsafe(15);
        let setAudioData = false;
        while (pos < sourceSize) {
            pos += reader.read(bufType, pos);
            let type = bufType[4];
            if (undefined === type) {
                break;
            }
            let dataSize = BufferUtils.readInt24BE(bufType, 5);
            let timestamp = BufferUtils.readInt24BE(bufType, 8);

            if (TYPE_SCRIPT === type) {
                let bufTag = Buffer.allocUnsafe(dataSize);
                pos += reader.read(bufTag, pos);
                let data = AmfParser.parse(bufTag);
                if (data && data.length > 1) {
                    let metaData = data[1];
                    if (metaData['duration'] !== undefined) {
                        let duration = metaData['duration'] * MOVIE_TIMESCALE;
                        movie.duration = duration;
                        videoTrack.duration = duration;
                        audioTrack.duration = duration;
                    }
                    if (metaData['width'] !== undefined) {
                        videoTrack.width = metaData['width'];
                    }
                    if (metaData['height'] !== undefined) {
                        videoTrack.height = metaData['height'];
                    }
                }
            } else if (TYPE_AUDIO === type) {
                let bufTag = Buffer.allocUnsafe(2);
                reader.read(bufTag, pos);

                let headerSize = 1;
                let flags = bufTag[0];
                let soundType = flags & 0x01;
                let soundSize = (flags & 0x02) >> 1;
                let soundRate = (flags & 0x0c) >> 2;
                let soundFormat = (flags & 0xf0) >> 4;

                if (!setAudioData) {
                    audioTrack.channels = 1 === soundType ? 2 : 1;
                    audioTrack.sampleRate = parseInt(5512.5 * (1 << soundRate), 10);
                    audioTrack.sampleSize = 1 === soundSize ? 16 : 8;
                    setAudioData = true;
                }

                if (AUDIO_FORMAT_AAC === soundFormat) {
                    headerSize += 1;
                    let packetType = bufTag[1];
                    if (0 === packetType) {
                        let extraData = Buffer.allocUnsafe(4 + dataSize - headerSize);
                        extraData.write('mp4a');
                        reader.read(extraData, pos + headerSize, 4);
                        audioTrack.extraData = extraData;
                        audioTrack.codec = CodecParser.parse(extraData).codec();
                        pos += dataSize;
                        continue;
                    }
                }

                let sample = Object.create(AudioSample.prototype);
                sample.timestamp = timestamp;
                sample.timescale = audioTrack.timescale;
                sample.size = dataSize - headerSize;
                sample.offset = pos + headerSize;
                if (0 < sample.size) {
                    audioTrack.samples.push(sample);
                    if (movie.duration === 0) {
                        audioTrack.duration = sample.timestamp;
                    }
                }
                pos += dataSize;
            } else if (TYPE_VIDEO === type) {
                let bufTag = Buffer.allocUnsafe(5);
                reader.read(bufTag, pos);

                let headerSize = 1;
                let flags = bufTag[0];

                let videoFormat = flags & 0x0f;
                let frameType = (flags & 0xf0) >> 4;
                let compTime = 0;
                if (VIDEO_FORMAT_H264 === videoFormat) {
                    headerSize += 4;
                    let packetType = bufTag[1];
                    compTime = BufferUtils.readInt24BE(bufTag, 2);
                    if (0 === packetType) {
                        let extraData = Buffer.allocUnsafe(4 + dataSize - headerSize);
                        extraData.write('avcC');
                        reader.read(extraData, pos + headerSize, 4);
                        videoTrack.extraData = extraData;
                        videoTrack.codec = CodecParser.parse(extraData).codec();
                        pos += dataSize;
                        continue;
                    }
                }

                let sample = Object.create(VideoSample.prototype);
                sample.compositionOffset = compTime;
                sample.keyframe = 1 === frameType;
                sample.timestamp = timestamp;
                sample.timescale = videoTrack.timescale;
                sample.size = dataSize - headerSize;
                sample.offset = pos + headerSize;
                if (0 < sample.size) {
                    videoTrack.samples.push(sample);
                    if (movie.duration === 0) {
                        videoTrack.duration = sample.timestamp;
                    }
                }

                pos += dataSize;
            } else {
                pos += dataSize;
            }
        }

        // Add tracks
        [videoTrack, audioTrack].map((track) => {
            if (track.samples.length > 0) {
                movie.addTrack(track);
            }
        });

        // Set movie duration
        if (movie.duration === 0) {
            movie.duration = movie.tracks.reduce((duration, track) => {
                return Math.max(duration, track.duration);
            }, 0);
        }

        // Return movie object
        return movie;
    }

    /**
     * Check FLV file
     * @param {Buffer} buffer
     * @returns {boolean}
     * @private
     */
    static _check(buffer) {
        return buffer.toString('ascii', 0, 3) === HEADER_PREFIX && buffer[3] === HEADER_VERSION;
    }

}

module.exports = Parser;
<commit_msg>Fix a bug in FLV parser<commit_after>'use strict';

const AmfParser = require('./amf-parser');
const BufferUtils = require('../buffer-utils');

const Movie = require('../movie');
const AudioTrack = require('../audio-track');
const VideoTrack = require('../video-track');
const AudioSample = require('../audio-sample');
const VideoSample = require('../video-sample');
const SourceReader = require('../readers/source-reader');
const CodecParser = require('../codecs/parser');

const MOVIE_TIMESCALE = 1000;
const HEADER_SIZE = 9;
const HEADER_PREFIX = 'FLV';
const HEADER_VERSION = 1;

const TYPE_SCRIPT = 18;
const TYPE_VIDEO = 9;
const TYPE_AUDIO = 8;

const AUDIO_FORMAT_AAC = 10;
const VIDEO_FORMAT_H264 = 7;

class Parser {

    /**
     * Parse FLV file
     * @param {(int|Buffer)} source
     * @returns {Movie}
     */
    static parse(source) {
        let reader = SourceReader.create(source);
        let sourceSize = reader.size();

        // Parse header
        let bufHeader = Buffer.allocUnsafe(HEADER_SIZE);
        reader.read(bufHeader, 0);
        if (bufHeader.toString('ascii', 0, 3) !== HEADER_PREFIX || bufHeader[3] !== HEADER_VERSION) {
            throw new Error('FLV header not found');
        }
        let pos = bufHeader[8];

        // Create movie
        let movie = new Movie();
        movie.timescale = MOVIE_TIMESCALE;
        let videoTrack = new VideoTrack();
        videoTrack.timescale = MOVIE_TIMESCALE;
        let audioTrack = new AudioTrack();
        audioTrack.timescale = MOVIE_TIMESCALE;

        // Parse tags
        let bufType = Buffer.allocUnsafe(15);
        let setAudioData = false;
        while (pos < sourceSize) {
            pos += reader.read(bufType, pos);
            let type = bufType[4];
            if (undefined === type) {
                break;
            }
            let dataSize = BufferUtils.readInt24BE(bufType, 5);
            let timestamp = BufferUtils.readInt24BE(bufType, 8);

            if (TYPE_SCRIPT === type) {
                let bufTag = Buffer.allocUnsafe(dataSize);
                pos += reader.read(bufTag, pos);
                let data = AmfParser.parse(bufTag);
                if (data && data.length > 1) {
                    let metaData = data[1];
                    if (metaData['duration'] !== undefined) {
                        let duration = metaData['duration'] * MOVIE_TIMESCALE;
                        movie.duration = duration;
                        videoTrack.duration = duration;
                        audioTrack.duration = duration;
                    }
                    if (metaData['width'] !== undefined) {
                        videoTrack.width = metaData['width'];
                    }
                    if (metaData['height'] !== undefined) {
                        videoTrack.height = metaData['height'];
                    }
                }
            } else if (TYPE_AUDIO === type) {
                let bufTag = Buffer.allocUnsafe(2);
                reader.read(bufTag, pos);

                let headerSize = 1;
                let flags = bufTag[0];
                let soundType = flags & 0x01;
                let soundSize = (flags & 0x02) >> 1;
                let soundRate = (flags & 0x0c) >> 2;
                let soundFormat = (flags & 0xf0) >> 4;

                if (!setAudioData) {
                    audioTrack.channels = 1 === soundType ? 2 : 1;
                    audioTrack.sampleRate = parseInt(5512.5 * (1 << soundRate), 10);
                    audioTrack.sampleSize = 1 === soundSize ? 16 : 8;
                    setAudioData = true;
                }

                if (AUDIO_FORMAT_AAC === soundFormat) {
                    headerSize += 1;
                    let packetType = bufTag[1];
                    if (0 === packetType) {
                        let extraData = Buffer.allocUnsafe(4 + dataSize - headerSize);
                        extraData.write('mp4a');
                        reader.read(extraData, pos + headerSize, 4);
                        audioTrack.extraData = extraData;
                        audioTrack.codec = CodecParser.parse(extraData).codec();
                        pos += dataSize;
                        continue;
                    }
                }

                let sample = Object.create(AudioSample.prototype);
                sample.timestamp = timestamp;
                sample.timescale = audioTrack.timescale;
                sample.size = dataSize - headerSize;
                sample.offset = pos + headerSize;
                if (0 < sample.size) {
                    audioTrack.samples.push(sample);
                    if (movie.duration === 0) {
                        audioTrack.duration = sample.timestamp;
                    }
                }
                pos += dataSize;
            } else if (TYPE_VIDEO === type) {
                let bufTag = Buffer.allocUnsafe(5);
                reader.read(bufTag, pos);

                let headerSize = 1;
                let flags = bufTag[0];

                let videoFormat = flags & 0x0f;
                let frameType = (flags & 0xf0) >> 4;
                let compTime = 0;
                if (VIDEO_FORMAT_H264 === videoFormat) {
                    headerSize += 4;
                    let packetType = bufTag[1];
                    compTime = BufferUtils.readInt24BE(bufTag, 2);
                    if (0 === packetType) {
                        let extraData = Buffer.allocUnsafe(4 + dataSize - headerSize);
                        extraData.write('avcC');
                        reader.read(extraData, pos + headerSize, 4);
                        videoTrack.extraData = extraData;
                        videoTrack.codec = CodecParser.parse(extraData).codec();
                        pos += dataSize;
                        continue;
                    }
                }

                let sample = Object.create(VideoSample.prototype);
                sample.compositionOffset = compTime;
                sample.keyframe = 1 === frameType;
                sample.timestamp = timestamp;
                sample.timescale = videoTrack.timescale;
                sample.size = dataSize - headerSize;
                sample.offset = pos + headerSize;
                if (0 < sample.size) {
                    videoTrack.samples.push(sample);
                    if (movie.duration === 0) {
                        videoTrack.duration = sample.timestamp;
                    }
                }

                pos += dataSize;
            } else {
                pos += dataSize;
            }
        }

        // Add tracks
        [videoTrack, audioTrack].map((track) => {
            if (track.extraData && track.samples.length > 0) {
                movie.addTrack(track);
            }
        });

        // Set movie duration
        if (movie.duration === 0) {
            movie.duration = movie.tracks.reduce((duration, track) => {
                return Math.max(duration, track.duration);
            }, 0);
        }

        // Return movie object
        return movie;
    }

    /**
     * Check FLV file
     * @param {Buffer} buffer
     * @returns {boolean}
     * @private
     */
    static _check(buffer) {
        return buffer.toString('ascii', 0, 3) === HEADER_PREFIX && buffer[3] === HEADER_VERSION;
    }

}

module.exports = Parser;
<|endoftext|>


<commit_before>/*
 * Copyright 2009-2011 Mozilla Foundation and contributors
 * Licensed under the New BSD license. See LICENSE.txt or:
 * http://opensource.org/licenses/BSD-3-Clause
 */

define(function(require, exports, module) {
var types = exports;

var Argument = require('gcli/argument').Argument;

/**
 * Some types can detect validity, that is to say they can distinguish between
 * valid and invalid values.
 * We might want to change these constants to be numbers for better performance
 */
var Status = {
  /**
   * The conversion process worked without any problem, and the value is
   * valid. There are a number of failure states, so the best way to check
   * for failure is (x !== Status.VALID)
   */
  VALID: {
    toString: function() { return 'VALID'; },
    valueOf: function() { return 0; }
  },

  /**
   * A conversion process failed, however it was noted that the string
   * provided to 'parse()' could be VALID by the addition of more characters,
   * so the typing may not be actually incorrect yet, just unfinished.
   * @see Status.ERROR
   */
  INCOMPLETE: {
    toString: function() { return 'INCOMPLETE'; },
    valueOf: function() { return 1; }
  },

  /**
   * The conversion process did not work, the value should be null and a
   * reason for failure should have been provided. In addition some
   * completion values may be available.
   * @see Status.INCOMPLETE
   */
  ERROR: {
    toString: function() { return 'ERROR'; },
    valueOf: function() { return 2; }
  },

  /**
   * A combined status is the worser of the provided statuses. The statuses
   * can be provided either as a set of arguments or a single array
   */
  combine: function() {
    var combined = Status.VALID;
    for (var i = 0; i < arguments.length; i++) {
      var status = arguments[i];
      if (Array.isArray(status)) {
        status = Status.combine.apply(null, status);
      }
      if (status > combined) {
        combined = status;
      }
    }
    return combined;
  }
};
types.Status = Status;

/**
 * The type.parse() method converts an Argument into a value, Conversion is
 * a wrapper to that value.
 * Conversion is needed to collect a number of properties related to that
 * conversion in one place, i.e. to handle errors and provide traceability.
 * @param value The result of the conversion
 * @param arg The data from which the conversion was made
 * @param status See the Status values [VALID|INCOMPLETE|ERROR] defined above.
 * The default status is Status.VALID.
 * @param message If status=ERROR, there should be a message to describe the
 * error. A message is not needed unless for other statuses, but could be
 * present for any status including VALID (in the case where we want to note a
 * warning, for example).
 * See BUG 664676: GCLI conversion error messages should be localized
 * @param predictions If status=INCOMPLETE, there could be predictions as to
 * the options available to complete the input.
 * We generally expect there to be about 7 predictions (to match human list
 * comprehension ability) however it is valid to provide up to about 20,
 * or less. It is the job of the predictor to decide a smart cut-off.
 * For example if there are 4 very good matches and 4 very poor ones,
 * probably only the 4 very good matches should be presented.
 * The predictions are presented either as an array of prediction objects or as
 * a function which returns this array when called with no parameters.
 * Each prediction object has the following shape:
 *     {
 *       name: '...',     // textual completion. i.e. what the cli uses
 *       value: { ... },  // value behind the textual completion
 *       incomplete: true // this completion is only partial (optional)
 *     }
 * The 'incomplete' property could be used to denote a valid completion which
 * could have sub-values (e.g. for tree navigation).
 */
function Conversion(value, arg, status, message, predictions) {
  // The result of the conversion process. Will be null if status != VALID
  this.value = value;

  // Allow us to trace where this Conversion came from
  this.arg = arg;
  if (arg == null) {
    throw new Error('Missing arg');
  }

  this._status = status || Status.VALID;
  this.message = message;
  this.predictions = predictions;
}

types.Conversion = Conversion;

/**
 * Ensure that all arguments that are part of this conversion know what they
 * are assigned to.
 * @param assignment The Assignment (param/conversion link) to inform the
 * argument about.
 */
Conversion.prototype.assign = function(assignment) {
  this.arg.assign(assignment);
};

/**
 * Work out if there is information provided in the contained argument.
 */
Conversion.prototype.isDataProvided = function() {
  var argProvided = this.arg.text !== '';
  return this.value !== undefined || argProvided;
};

/**
 * 2 conversions are equal if and only if their args are equal (argEquals) and
 * their values are equal (valueEquals).
 * @param that The conversion object to compare against.
 */
Conversion.prototype.equals = function(that) {
  if (this === that) {
    return true;
  }
  if (that == null) {
    return false;
  }
  return this.valueEquals(that) && this.argEquals(that);
};

/**
 * Check that the value in this conversion is strict equal to the value in the
 * provided conversion.
 * @param that The conversion to compare values with
 */
Conversion.prototype.valueEquals = function(that) {
  return this.value === that.value;
};

/**
 * Check that the argument in this conversion is equal to the value in the
 * provided conversion as defined by the argument (i.e. arg.equals).
 * @param that The conversion to compare arguments with
 */
Conversion.prototype.argEquals = function(that) {
  return this.arg.equals(that.arg);
};

/**
 * Accessor for the status of this conversion
 */
Conversion.prototype.getStatus = function(arg) {
  return this._status;
};

/**
 * Defined by the toString() value provided by the argument
 */
Conversion.prototype.toString = function() {
  return this.arg.toString();
};

/**
 * If status === INCOMPLETE, then we may be able to provide predictions as to
 * how the argument can be completed.
 * @return An array of items, where each item is an object with the following
 * properties:
 * - name (mandatory): Displayed to the user, and typed in. No whitespace
 * - description (optional): Short string for display in a tool-tip
 * - manual (optional): Longer description which details usage
 * - incomplete (optional): Indicates that the prediction if used should not
 *   be considered necessarily sufficient, which typically will mean that the
 *   UI should not append a space to the completion
 * - value (optional): If a value property is present, this will be used as the
 *   value of the conversion, otherwise the item itself will be used.
 */
Conversion.prototype.getPredictions = function() {
  if (typeof this.predictions === 'function') {
    return this.predictions();
  }
  return this.predictions || [];
};

/**
 * ArrayConversion is a special Conversion, needed because arrays are converted
 * member by member rather then as a whole, which means we can track the
 * conversion if individual array elements. So an ArrayConversion acts like a
 * normal Conversion (which is needed as Assignment requires a Conversion) but
 * it can also be devolved into a set of Conversions for each array member.
 */
function ArrayConversion(conversions, arg) {
  this.arg = arg;
  this.conversions = conversions;
  this.value = conversions.map(function(conversion) {
    return conversion.value;
  }, this);

  this._status = Status.combine(conversions.map(function(conversion) {
    return conversion.getStatus();
  }));

  // This message is just for reporting errors like "not enough values"
  // rather that for problems with individual values.
  this.message = '';

  // Predictions are generally provided by individual values
  this.predictions = [];
}

ArrayConversion.prototype = Object.create(Conversion.prototype);

ArrayConversion.prototype.assign = function(assignment) {
  this.conversions.forEach(function(conversion) {
    conversion.assign(assignment);
  }, this);
  this.assignment = assignment;
};

ArrayConversion.prototype.getStatus = function(arg) {
  if (arg && arg.conversion) {
    return arg.conversion.getStatus();
  }
  return this._status;
};

ArrayConversion.prototype.isDataProvided = function() {
  return this.conversions.length > 0;
};

ArrayConversion.prototype.valueEquals = function(that) {
  if (!(that instanceof ArrayConversion)) {
    throw new Error('Can\'t compare values with non ArrayConversion');
  }

  if (this.value === that.value) {
    return true;
  }

  if (this.value.length !== that.value.length) {
    return false;
  }

  for (var i = 0; i < this.conversions.length; i++) {
    if (!this.conversions[i].valueEquals(that.conversions[i])) {
      return false;
    }
  }

  return true;
};

ArrayConversion.prototype.toString = function() {
  return '[ ' + this.conversions.map(function(conversion) {
    return conversion.toString();
  }, this).join(', ') + ' ]';
};

types.ArrayConversion = ArrayConversion;

/**
 * Most of our types are 'static' e.g. there is only one type of 'string',
 * however some types like 'selection' and 'deferred' are customizable.
 * The basic Type type isn't useful, but does provide documentation about what
 * types do.
 */
function Type() {
}

/**
 * Convert the given <tt>value</tt> to a string representation.
 * Where possible, there should be round-tripping between values and their
 * string representations.
 */
Type.prototype.stringify = function(value) {
  throw new Error('Not implemented');
};

/**
 * Convert the given <tt>arg</tt> to an instance of this type.
 * Where possible, there should be round-tripping between values and their
 * string representations.
 * @param arg An instance of <tt>Argument</tt> to convert.
 * @return Conversion
 */
Type.prototype.parse = function(arg) {
  throw new Error('Not implemented');
};

/**
 * A convenience method for times when you don't have an argument to parse
 * but instead have a string.
 * @see #parse(arg)
 */
Type.prototype.parseString = function(str) {
  return this.parse(new Argument(str));
},

/**
 * The plug-in system, and other things need to know what this type is
 * called. The name alone is not enough to fully specify a type. Types like
 * 'selection' and 'deferred' need extra data, however this function returns
 * only the name, not the extra data.
 */
Type.prototype.name = undefined;

/**
 * If there is some concept of a higher value, return it,
 * otherwise return undefined.
 */
Type.prototype.increment = function(value) {
  return undefined;
};

/**
 * If there is some concept of a lower value, return it,
 * otherwise return undefined.
 */
Type.prototype.decrement = function(value) {
  return undefined;
};

/**
 * There is interesting information (like predictions) in a conversion of
 * nothing, the output of this can sometimes be customized.
 * @return Conversion
 */
Type.prototype.getDefault = undefined;

types.Type = Type;

/**
 * Private registry of types
 * Invariant: types[name] = type.name
 */
var registeredTypes = {};

types.getTypeNames = function() {
  return Object.keys(registeredTypes);
};

/**
 * Add a new type to the list available to the system.
 * You can pass 2 things to this function - either an instance of Type, in
 * which case we return this instance when #getType() is called with a 'name'
 * that matches type.name.
 * Also you can pass in a constructor (i.e. function) in which case when
 * #getType() is called with a 'name' that matches Type.prototype.name we will
 * pass the typeSpec into this constructor.
 */
types.registerType = function(type) {
  if (typeof type === 'object') {
    if (type instanceof Type) {
      if (!type.name) {
        throw new Error('All registered types must have a name');
      }
      registeredTypes[type.name] = type;
    }
    else {
      throw new Error('Can\'t registerType using: ' + type);
    }
  }
  else if (typeof type === 'function') {
    if (!type.prototype.name) {
      throw new Error('All registered types must have a name');
    }
    registeredTypes[type.prototype.name] = type;
  }
  else {
    throw new Error('Unknown type: ' + type);
  }
};

types.registerTypes = function registerTypes(newTypes) {
  Object.keys(newTypes).forEach(function(name) {
    var type = newTypes[name];
    type.name = name;
    newTypes.registerType(type);
  });
};

/**
 * Remove a type from the list available to the system
 */
types.deregisterType = function(type) {
  delete registeredTypes[type.name];
};

/**
 * Find a type, previously registered using #registerType()
 */
types.getType = function(typeSpec) {
  var type;
  if (typeof typeSpec === 'string') {
    type = registeredTypes[typeSpec];
    if (typeof type === 'function') {
      type = new type();
    }
    return type;
  }

  if (typeof typeSpec === 'object') {
    if (!typeSpec.name) {
      throw new Error('Missing \'name\' member to typeSpec');
    }

    type = registeredTypes[typeSpec.name];
    if (typeof type === 'function') {
      type = new type(typeSpec);
    }
    return type;
  }

  throw new Error('Can\'t extract type from ' + typeSpec);
};

});
<commit_msg>Bug 720971 (typedefault): Provide default implementation of getDefault().<commit_after>/*
 * Copyright 2009-2011 Mozilla Foundation and contributors
 * Licensed under the New BSD license. See LICENSE.txt or:
 * http://opensource.org/licenses/BSD-3-Clause
 */

define(function(require, exports, module) {
var types = exports;

var Argument = require('gcli/argument').Argument;

/**
 * Some types can detect validity, that is to say they can distinguish between
 * valid and invalid values.
 * We might want to change these constants to be numbers for better performance
 */
var Status = {
  /**
   * The conversion process worked without any problem, and the value is
   * valid. There are a number of failure states, so the best way to check
   * for failure is (x !== Status.VALID)
   */
  VALID: {
    toString: function() { return 'VALID'; },
    valueOf: function() { return 0; }
  },

  /**
   * A conversion process failed, however it was noted that the string
   * provided to 'parse()' could be VALID by the addition of more characters,
   * so the typing may not be actually incorrect yet, just unfinished.
   * @see Status.ERROR
   */
  INCOMPLETE: {
    toString: function() { return 'INCOMPLETE'; },
    valueOf: function() { return 1; }
  },

  /**
   * The conversion process did not work, the value should be null and a
   * reason for failure should have been provided. In addition some
   * completion values may be available.
   * @see Status.INCOMPLETE
   */
  ERROR: {
    toString: function() { return 'ERROR'; },
    valueOf: function() { return 2; }
  },

  /**
   * A combined status is the worser of the provided statuses. The statuses
   * can be provided either as a set of arguments or a single array
   */
  combine: function() {
    var combined = Status.VALID;
    for (var i = 0; i < arguments.length; i++) {
      var status = arguments[i];
      if (Array.isArray(status)) {
        status = Status.combine.apply(null, status);
      }
      if (status > combined) {
        combined = status;
      }
    }
    return combined;
  }
};
types.Status = Status;

/**
 * The type.parse() method converts an Argument into a value, Conversion is
 * a wrapper to that value.
 * Conversion is needed to collect a number of properties related to that
 * conversion in one place, i.e. to handle errors and provide traceability.
 * @param value The result of the conversion
 * @param arg The data from which the conversion was made
 * @param status See the Status values [VALID|INCOMPLETE|ERROR] defined above.
 * The default status is Status.VALID.
 * @param message If status=ERROR, there should be a message to describe the
 * error. A message is not needed unless for other statuses, but could be
 * present for any status including VALID (in the case where we want to note a
 * warning, for example).
 * See BUG 664676: GCLI conversion error messages should be localized
 * @param predictions If status=INCOMPLETE, there could be predictions as to
 * the options available to complete the input.
 * We generally expect there to be about 7 predictions (to match human list
 * comprehension ability) however it is valid to provide up to about 20,
 * or less. It is the job of the predictor to decide a smart cut-off.
 * For example if there are 4 very good matches and 4 very poor ones,
 * probably only the 4 very good matches should be presented.
 * The predictions are presented either as an array of prediction objects or as
 * a function which returns this array when called with no parameters.
 * Each prediction object has the following shape:
 *     {
 *       name: '...',     // textual completion. i.e. what the cli uses
 *       value: { ... },  // value behind the textual completion
 *       incomplete: true // this completion is only partial (optional)
 *     }
 * The 'incomplete' property could be used to denote a valid completion which
 * could have sub-values (e.g. for tree navigation).
 */
function Conversion(value, arg, status, message, predictions) {
  // The result of the conversion process. Will be null if status != VALID
  this.value = value;

  // Allow us to trace where this Conversion came from
  this.arg = arg;
  if (arg == null) {
    throw new Error('Missing arg');
  }

  this._status = status || Status.VALID;
  this.message = message;
  this.predictions = predictions;
}

types.Conversion = Conversion;

/**
 * Ensure that all arguments that are part of this conversion know what they
 * are assigned to.
 * @param assignment The Assignment (param/conversion link) to inform the
 * argument about.
 */
Conversion.prototype.assign = function(assignment) {
  this.arg.assign(assignment);
};

/**
 * Work out if there is information provided in the contained argument.
 */
Conversion.prototype.isDataProvided = function() {
  var argProvided = this.arg.text !== '';
  return this.value !== undefined || argProvided;
};

/**
 * 2 conversions are equal if and only if their args are equal (argEquals) and
 * their values are equal (valueEquals).
 * @param that The conversion object to compare against.
 */
Conversion.prototype.equals = function(that) {
  if (this === that) {
    return true;
  }
  if (that == null) {
    return false;
  }
  return this.valueEquals(that) && this.argEquals(that);
};

/**
 * Check that the value in this conversion is strict equal to the value in the
 * provided conversion.
 * @param that The conversion to compare values with
 */
Conversion.prototype.valueEquals = function(that) {
  return this.value === that.value;
};

/**
 * Check that the argument in this conversion is equal to the value in the
 * provided conversion as defined by the argument (i.e. arg.equals).
 * @param that The conversion to compare arguments with
 */
Conversion.prototype.argEquals = function(that) {
  return this.arg.equals(that.arg);
};

/**
 * Accessor for the status of this conversion
 */
Conversion.prototype.getStatus = function(arg) {
  return this._status;
};

/**
 * Defined by the toString() value provided by the argument
 */
Conversion.prototype.toString = function() {
  return this.arg.toString();
};

/**
 * If status === INCOMPLETE, then we may be able to provide predictions as to
 * how the argument can be completed.
 * @return An array of items, where each item is an object with the following
 * properties:
 * - name (mandatory): Displayed to the user, and typed in. No whitespace
 * - description (optional): Short string for display in a tool-tip
 * - manual (optional): Longer description which details usage
 * - incomplete (optional): Indicates that the prediction if used should not
 *   be considered necessarily sufficient, which typically will mean that the
 *   UI should not append a space to the completion
 * - value (optional): If a value property is present, this will be used as the
 *   value of the conversion, otherwise the item itself will be used.
 */
Conversion.prototype.getPredictions = function() {
  if (typeof this.predictions === 'function') {
    return this.predictions();
  }
  return this.predictions || [];
};

/**
 * ArrayConversion is a special Conversion, needed because arrays are converted
 * member by member rather then as a whole, which means we can track the
 * conversion if individual array elements. So an ArrayConversion acts like a
 * normal Conversion (which is needed as Assignment requires a Conversion) but
 * it can also be devolved into a set of Conversions for each array member.
 */
function ArrayConversion(conversions, arg) {
  this.arg = arg;
  this.conversions = conversions;
  this.value = conversions.map(function(conversion) {
    return conversion.value;
  }, this);

  this._status = Status.combine(conversions.map(function(conversion) {
    return conversion.getStatus();
  }));

  // This message is just for reporting errors like "not enough values"
  // rather that for problems with individual values.
  this.message = '';

  // Predictions are generally provided by individual values
  this.predictions = [];
}

ArrayConversion.prototype = Object.create(Conversion.prototype);

ArrayConversion.prototype.assign = function(assignment) {
  this.conversions.forEach(function(conversion) {
    conversion.assign(assignment);
  }, this);
  this.assignment = assignment;
};

ArrayConversion.prototype.getStatus = function(arg) {
  if (arg && arg.conversion) {
    return arg.conversion.getStatus();
  }
  return this._status;
};

ArrayConversion.prototype.isDataProvided = function() {
  return this.conversions.length > 0;
};

ArrayConversion.prototype.valueEquals = function(that) {
  if (!(that instanceof ArrayConversion)) {
    throw new Error('Can\'t compare values with non ArrayConversion');
  }

  if (this.value === that.value) {
    return true;
  }

  if (this.value.length !== that.value.length) {
    return false;
  }

  for (var i = 0; i < this.conversions.length; i++) {
    if (!this.conversions[i].valueEquals(that.conversions[i])) {
      return false;
    }
  }

  return true;
};

ArrayConversion.prototype.toString = function() {
  return '[ ' + this.conversions.map(function(conversion) {
    return conversion.toString();
  }, this).join(', ') + ' ]';
};

types.ArrayConversion = ArrayConversion;

/**
 * Most of our types are 'static' e.g. there is only one type of 'string',
 * however some types like 'selection' and 'deferred' are customizable.
 * The basic Type type isn't useful, but does provide documentation about what
 * types do.
 */
function Type() {
}

/**
 * Convert the given <tt>value</tt> to a string representation.
 * Where possible, there should be round-tripping between values and their
 * string representations.
 */
Type.prototype.stringify = function(value) {
  throw new Error('Not implemented');
};

/**
 * Convert the given <tt>arg</tt> to an instance of this type.
 * Where possible, there should be round-tripping between values and their
 * string representations.
 * @param arg An instance of <tt>Argument</tt> to convert.
 * @return Conversion
 */
Type.prototype.parse = function(arg) {
  throw new Error('Not implemented');
};

/**
 * A convenience method for times when you don't have an argument to parse
 * but instead have a string.
 * @see #parse(arg)
 */
Type.prototype.parseString = function(str) {
  return this.parse(new Argument(str));
},

/**
 * The plug-in system, and other things need to know what this type is
 * called. The name alone is not enough to fully specify a type. Types like
 * 'selection' and 'deferred' need extra data, however this function returns
 * only the name, not the extra data.
 */
Type.prototype.name = undefined;

/**
 * If there is some concept of a higher value, return it,
 * otherwise return undefined.
 */
Type.prototype.increment = function(value) {
  return undefined;
};

/**
 * If there is some concept of a lower value, return it,
 * otherwise return undefined.
 */
Type.prototype.decrement = function(value) {
  return undefined;
};

/**
 * There is interesting information (like predictions) in a conversion of
 * nothing, the output of this can sometimes be customized.
 * @return Conversion
 */
Type.prototype.getDefault = function() {
  return this.parseString('');
};

types.Type = Type;

/**
 * Private registry of types
 * Invariant: types[name] = type.name
 */
var registeredTypes = {};

types.getTypeNames = function() {
  return Object.keys(registeredTypes);
};

/**
 * Add a new type to the list available to the system.
 * You can pass 2 things to this function - either an instance of Type, in
 * which case we return this instance when #getType() is called with a 'name'
 * that matches type.name.
 * Also you can pass in a constructor (i.e. function) in which case when
 * #getType() is called with a 'name' that matches Type.prototype.name we will
 * pass the typeSpec into this constructor.
 */
types.registerType = function(type) {
  if (typeof type === 'object') {
    if (type instanceof Type) {
      if (!type.name) {
        throw new Error('All registered types must have a name');
      }
      registeredTypes[type.name] = type;
    }
    else {
      throw new Error('Can\'t registerType using: ' + type);
    }
  }
  else if (typeof type === 'function') {
    if (!type.prototype.name) {
      throw new Error('All registered types must have a name');
    }
    registeredTypes[type.prototype.name] = type;
  }
  else {
    throw new Error('Unknown type: ' + type);
  }
};

types.registerTypes = function registerTypes(newTypes) {
  Object.keys(newTypes).forEach(function(name) {
    var type = newTypes[name];
    type.name = name;
    newTypes.registerType(type);
  });
};

/**
 * Remove a type from the list available to the system
 */
types.deregisterType = function(type) {
  delete registeredTypes[type.name];
};

/**
 * Find a type, previously registered using #registerType()
 */
types.getType = function(typeSpec) {
  var type;
  if (typeof typeSpec === 'string') {
    type = registeredTypes[typeSpec];
    if (typeof type === 'function') {
      type = new type();
    }
    return type;
  }

  if (typeof typeSpec === 'object') {
    if (!typeSpec.name) {
      throw new Error('Missing \'name\' member to typeSpec');
    }

    type = registeredTypes[typeSpec.name];
    if (typeof type === 'function') {
      type = new type(typeSpec);
    }
    return type;
  }

  throw new Error('Can\'t extract type from ' + typeSpec);
};

});
<|endoftext|>


<commit_before>const {tokenize} = require('kuromojin');
const toJapanese = require('jp-num/toJapanese');
const iconv = require('iconv-lite');
const {toZenKana} = require('jaconv');
const path = require('path');
const fs = require('fs');
const download = require('download');
const {promisify} = require('util');
const {escapeRegExp} = require('lodash');
const {katakanize} = require('japanese');

const loadingPromise = (async () => {
	const englishDictPath = path.resolve(__dirname, 'bep-ss-2.3', 'bep-eng.dic');

	const englishDictExists = await new Promise((resolve) => {
		fs.access(englishDictPath, fs.constants.F_OK, (error) => {
			resolve(!error);
		});
	});

	if (!englishDictExists) {
		await download('http://www.argv.org/bep/files/linux/beta/bep-ss-2.3.tar.gz', __dirname, {extract: true});
	}

	const englishDictBuffer = await promisify(fs.readFile)(englishDictPath);
	const englishDictText = iconv.decode(englishDictBuffer, 'sjis');
	const englishDict = new Map([
		...englishDictText
			.split('\n')
			.map((line) => {
				const [english, japanese] = line.split(' ');
				if (!japanese) {
					return null;
				}
				return [english.toLowerCase(), toZenKana(japanese).replace(/ゥ$/, '')];
			})
			.filter((entry) => entry),
		['a', 'えー'],
		['b', 'びー'],
		['c', 'しー'],
		['d', 'でぃー'],
		['e', 'いー'],
		['f', 'えふ'],
		['g', 'じー'],
		['h', 'えいち'],
		['i', 'あい'],
		['j', 'じぇい'],
		['k', 'けい'],
		['l', 'える'],
		['m', 'えむ'],
		['n', 'えぬ'],
		['o', 'おー'],
		['p', 'ぴー'],
		['q', 'きゅー'],
		['r', 'あーる'],
		['s', 'えす'],
		['t', 'てぃー'],
		['u', 'ゆー'],
		['v', 'ぶい'],
		['w', 'だぶりゅー'],
		['x', 'えっくす'],
		['y', 'わい'],
		['z', 'ずぃー'],
	]);
	const englishDictRegex = new RegExp(
		`\\b(${Array.from(englishDict.keys())
			.map((word) => escapeRegExp(word))
			.join('|')})\\b`,
		'gi'
	);
	return {
		englishDict,
	};
})();

const kanizeEnglish = async (word_) => {
	const {englishDict} = await loadingPromise;

	const genLattice = (word) => {
		// eslint-disable-next-line array-plural/array-plural
		const dp = Array(word.length).fill().map(() => Array(word.length).fill());
		const rec = (a, b) => {
			if (b === word.length) {
				return {to: [], range: [a, b], eos: true};
			}
			if (dp[a][b]) {
				return dp[a][b];
			}
			const node = {to: [], range: [a, b], eos: false};
			for (let i = 0; i < word.length - b; ++i) {
				if (englishDict.has(word.substring(b, b + i + 1))) {
					const node2 = rec(b, b + i + 1);
					if (node2.to || node2.eos) {
						node.to.push(node2);
					}
				}
			}
			dp[a][b] = node;
			return node;
		};
		return rec(0, 0);
	};
	const findShortestPath = (word) => {
		// eslint-disable-next-line array-plural/array-plural
		const nodeQueue = [{words: [], node: genLattice(word)}];
		while (true) {
			const {node, words} = nodeQueue.shift();
			if (node.eos) {
				return words.concat([node.range]);
			}
			for (const n of node.to) {
				nodeQueue.push({words: words.concat([node.range]), node: n});
			}
		}
	};

	return findShortestPath(word_.toLowerCase())
		.slice(1)
		.map(([a, b]) => englishDict.get(word_.toLowerCase().substring(a, b)))
		.join('');
};

module.exports = async (text) => {
	const englishWords = text.match(/\b[a-zA-Z]+\b/g);
	const readingMap = new Map(await Promise.all(englishWords.map(async (word) => {
		const reading = await kanizeEnglish(word);
		return [word, reading];
	})))
	const normalizedText = text.replace(/\b[a-zA-Z]+\b/g, (english) => readingMap.get(english.toLowerCase()) || english);
	const tokens = await tokenize(normalizedText.replace(/[\d,]+/g, (number) => toJapanese(number.replace(/,/g, ''))));
	const reading = Array.from(katakanize(tokens.map(({reading: read, surface_form}) => read || surface_form || '').join('')))
		.join('')
		.replace(/\P{Script_Extensions=Katakana}/gu, '');
	return toZenKana(reading);
}
<commit_msg>commit wasure tsurai<commit_after>const {tokenize} = require('kuromojin');
const toJapanese = require('jp-num/toJapanese');
const iconv = require('iconv-lite');
const {toZenKana} = require('jaconv');
const path = require('path');
const fs = require('fs');
const download = require('download');
const {promisify} = require('util');
const {escapeRegExp} = require('lodash');
const {katakanize} = require('japanese');

const loadingPromise = (async () => {
	const englishDictPath = path.resolve(__dirname, 'bep-ss-2.3', 'bep-eng.dic');

	const englishDictExists = await new Promise((resolve) => {
		fs.access(englishDictPath, fs.constants.F_OK, (error) => {
			resolve(!error);
		});
	});

	if (!englishDictExists) {
		await download('http://www.argv.org/bep/files/linux/beta/bep-ss-2.3.tar.gz', __dirname, {extract: true});
	}

	const englishDictBuffer = await promisify(fs.readFile)(englishDictPath);
	const englishDictText = iconv.decode(englishDictBuffer, 'sjis');
	const englishDict = new Map([
		...englishDictText
			.split('\n')
			.map((line) => {
				const [english, japanese] = line.split(' ');
				if (!japanese) {
					return null;
				}
				return [english.toLowerCase(), toZenKana(japanese).replace(/ゥ$/, '')];
			})
			.filter((entry) => entry),
		['a', 'えー'],
		['b', 'びー'],
		['c', 'しー'],
		['d', 'でぃー'],
		['e', 'いー'],
		['f', 'えふ'],
		['g', 'じー'],
		['h', 'えいち'],
		['i', 'あい'],
		['j', 'じぇい'],
		['k', 'けい'],
		['l', 'える'],
		['m', 'えむ'],
		['n', 'えぬ'],
		['o', 'おー'],
		['p', 'ぴー'],
		['q', 'きゅー'],
		['r', 'あーる'],
		['s', 'えす'],
		['t', 'てぃー'],
		['u', 'ゆー'],
		['v', 'ぶい'],
		['w', 'だぶりゅー'],
		['x', 'えっくす'],
		['y', 'わい'],
		['z', 'ずぃー'],
	]);
	const englishDictRegex = new RegExp(
		`\\b(${Array.from(englishDict.keys())
			.map((word) => escapeRegExp(word))
			.join('|')})\\b`,
		'gi'
	);
	return {
		englishDict,
	};
})();

const kanizeEnglish = async (word_) => {
	const {englishDict} = await loadingPromise;

	const genLattice = (word) => {
		// eslint-disable-next-line array-plural/array-plural
		const dp = Array(word.length).fill().map(() => Array(word.length).fill());
		const rec = (a, b) => {
			if (b === word.length) {
				return {to: [], range: [a, b], eos: true};
			}
			if (dp[a][b]) {
				return dp[a][b];
			}
			const node = {to: [], range: [a, b], eos: false};
			for (let i = 0; i < word.length - b; ++i) {
				if (englishDict.has(word.substring(b, b + i + 1))) {
					const node2 = rec(b, b + i + 1);
					if (node2.to || node2.eos) {
						node.to.push(node2);
					}
				}
			}
			dp[a][b] = node;
			return node;
		};
		return rec(0, 0);
	};
	const findShortestPath = (word) => {
		// eslint-disable-next-line array-plural/array-plural
		const nodeQueue = [{words: [], node: genLattice(word)}];
		while (true) {
			const {node, words} = nodeQueue.shift();
			if (node.eos) {
				return words.concat([node.range]);
			}
			for (const n of node.to) {
				nodeQueue.push({words: words.concat([node.range]), node: n});
			}
		}
	};

	return findShortestPath(word_.toLowerCase())
		.slice(1)
		.map(([a, b]) => englishDict.get(word_.toLowerCase().substring(a, b)))
		.join('');
};

module.exports = async (text) => {
	const englishWords = text.match(/\b[a-zA-Z]+\b/g) || [];
	const readingMap = new Map(await Promise.all(englishWords.map(async (word) => {
		const reading = await kanizeEnglish(word);
		return [word, reading];
	})))
	const normalizedText = text.replace(/\b[a-zA-Z]+\b/g, (english) => readingMap.get(english.toLowerCase()) || english);
	const tokens = await tokenize(normalizedText.replace(/[\d,]+/g, (number) => toJapanese(number.replace(/,/g, ''))));
	const reading = Array.from(katakanize(tokens.map(({reading: read, surface_form}) => read || surface_form || '').join('')))
		.join('')
		.replace(/\P{Script_Extensions=Katakana}/gu, '');
	return toZenKana(reading);
}
<|endoftext|>


<commit_before>
        //node built in
var   path = require('path')
        //lib code
    , log = require('./utils/log')
        //dep code
    , tmp = require('temporary')

module.exports = {

        //convenience for external
    valid:false,
        //the active consumer
    consumer:null,
        //the active hxml data
    hxml_file:null,
    hxml_content:null,
    hxml_cwd:null,
        //the temp file cache path
    tmp_path:null,

//lifecycle

    init: function(state) {

        this.set(state);

        this.tmp_dir = new tmp.Dir('atom-haxe');
        this.tmp_path = this.tmp_dir.path;

        log.debug('state set cache path ' + this.tmp_path);

    },

    dispose:function() {
        this.tmp_dir.rmdir();
    },

    serialize:function() {

        var o = {
            hxml_file: this.hxml_file,
            hxml_cwd: this.hxml_cwd,
            hxml_content: this.hxml_content
        }

        // console.log('state',o);

        return o;
    },

//Consumer

    set_consumer: function(opt) {

            //notify existing
        if(this.consumer && this.consumer.onConsumerLost) {
            this.consumer.onConsumerLost();
        }

            //set new
        this.consumer = opt;

            //apply
        if(opt) {
            if(this.consumer.hxml_content) {
                this.set_hxml_content(opt.hxml_content, opt.hxml_cwd);
            } else if(opt.hxml_file) {
                this.set_hxml_file(opt.hxml_file, opt.hxml_cwd);
            }
        }

    }, //set_consumer

//State queries

    as_args:function(plus_args) {

        if(!this.valid) return null;

        var args = [];

        if(this.hxml_cwd) {
            args.push('--cwd');
            args.push(this.hxml_cwd);
        }

        args = args.concat(this.hxml_as_args());

        if(plus_args) {
            args = args.concat(plus_args);
        }

        return args;

    }, //as_args

    hxml_as_args:function(options) {

        if(!this.valid) return null;

        options = options || {};

        var args = [];
            //check if hxml content is set
        var hxml = options.hxml_content || this.hxml_content;

            //if not, check for a file
        if(!hxml) {
                //check if there's a file given instead
            hxml = options.hxml_file || this.hxml_file;
                //if there is, make it relative
            if(hxml) {
                hxml = path.relative(this.hxml_cwd, hxml);
                args = [hxml];
            }

        } else {

            args = hxml.split(/[ \n]/g);
            args = args.filter(function(a) {
                if(a) return true;
            });

        }

        return args;

    }, //hxml_as_args

//State updates

    set_hxml_file:function(file, cwd) {
        this.unset(true);
        this.hxml_file = file;
        this.hxml_cwd = cwd || path.dirname(file);
        log.debug('state hxml file to ' + this.hxml_file);
        log.debug('state hxml cwd to ' + this.hxml_cwd);
        this.valid = true;
    },

    set_hxml_content:function(file_content, cwd) {
        this.unset(true);
        this.hxml_content = file_content;
        this.hxml_cwd = cwd;
        // log.debug('state hxml file content set to ' + this.hxml_content);
        log.debug('state set hxml, and cwd: ' + this.hxml_cwd);
        this.valid = true;
    },

    unset:function(internal) {
        this.hxml_file = null;
        this.hxml_content = null;
        this.hxml_cwd = null;
        this.valid = false;
        if(!internal) log.debug('state unset');
    },

    set:function(state) {

        if(state) {

                //:todo: possibly more resilient since
                //this may be called from elsewhere later.
                //right now it's only called internally.
            var cwd = state.hxml_cwd;

            if(state.hxml_file) {
                this.set_hxml_file(state.hxml_file, cwd);
            } else if(state.hxml_content) {
                this.set_hxml_content(state.hxml_content, cwd);
            }

            this.valid = true;

        } //if state

    }, //set

} //module.exports
<commit_msg>state; more logging<commit_after>
        //node built in
var   path = require('path')
        //lib code
    , log = require('./utils/log')
        //dep code
    , tmp = require('temporary')

module.exports = {

        //convenience for external
    valid:false,
        //the active consumer
    consumer:null,
        //the active hxml data
    hxml_file:null,
    hxml_content:null,
    hxml_cwd:null,
        //the temp file cache path
    tmp_path:null,

//lifecycle

    init: function(state) {

        this.set(state);

        this.tmp_dir = new tmp.Dir('atom-haxe');
        this.tmp_path = this.tmp_dir.path;

        log.debug('state set cache path ' + this.tmp_path);

    },

    dispose:function() {
        this.tmp_dir.rmdir();
    },

    serialize:function() {

        var o = {
            hxml_file: this.hxml_file,
            hxml_cwd: this.hxml_cwd,
            hxml_content: this.hxml_content
        }

        // console.log('state',o);

        return o;
    },

//Consumer

    set_consumer: function(opt) {

            //notify existing
        if(this.consumer && this.consumer.onConsumerLost) {
            this.consumer.onConsumerLost();
        }

            //set new
        this.consumer = opt;

            //apply
        if(opt) {

            log.info('consumer; set external consumer to ' + this.consumer.name);

            if(this.consumer.hxml_content) {
                this.set_hxml_content(opt.hxml_content, opt.hxml_cwd);
            } else if(opt.hxml_file) {
                this.set_hxml_file(opt.hxml_file, opt.hxml_cwd);
            }
        } else {
            log.info('consumer; unset external consumer');
        }

    }, //set_consumer

//State queries

    as_args:function(plus_args) {

        if(!this.valid) return null;

        var args = [];

        if(this.hxml_cwd) {
            args.push('--cwd');
            args.push(this.hxml_cwd);
        }

        args = args.concat(this.hxml_as_args());

        if(plus_args) {
            args = args.concat(plus_args);
        }

        return args;

    }, //as_args

    hxml_as_args:function(options) {

        if(!this.valid) return null;

        options = options || {};

        var args = [];
            //check if hxml content is set
        var hxml = options.hxml_content || this.hxml_content;

            //if not, check for a file
        if(!hxml) {
                //check if there's a file given instead
            hxml = options.hxml_file || this.hxml_file;
                //if there is, make it relative
            if(hxml) {
                hxml = path.relative(this.hxml_cwd, hxml);
                args = [hxml];
            }

        } else {

            args = hxml.split(/[ \n]/g);
            args = args.filter(function(a) {
                if(a) return true;
            });

        }

        return args;

    }, //hxml_as_args

//State updates

    set_hxml_file:function(file, cwd) {
        this.unset(true);
        this.hxml_file = file;
        this.hxml_cwd = cwd || path.dirname(file);
        log.debug('state hxml file to ' + this.hxml_file);
        log.debug('state hxml cwd to ' + this.hxml_cwd);
        this.valid = true;
    },

    set_hxml_content:function(file_content, cwd) {
        this.unset(true);
        this.hxml_content = file_content;
        this.hxml_cwd = cwd;
        // log.debug('state hxml file content set to ' + this.hxml_content);
        log.debug('state set hxml, and cwd: ' + this.hxml_cwd);
        this.valid = true;
    },

    unset:function(internal) {
        this.hxml_file = null;
        this.hxml_content = null;
        this.hxml_cwd = null;
        this.valid = false;
        if(!internal) log.debug('state unset');
    },

    set:function(state) {

        if(state) {

                //:todo: possibly more resilient since
                //this may be called from elsewhere later.
                //right now it's only called internally.
            var cwd = state.hxml_cwd;

            if(state.hxml_file) {
                this.set_hxml_file(state.hxml_file, cwd);
            } else if(state.hxml_content) {
                this.set_hxml_content(state.hxml_content, cwd);
            }

            this.valid = true;

        } //if state

    }, //set

} //module.exports
<|endoftext|>


<commit_before>'use strict';

const Promise = require('bluebird');
const { sep, join, dirname } = require('path');
const tildify = require('tildify');
const Database = require('warehouse');
const _ = require('lodash');
const chalk = require('chalk');
const { EventEmitter } = require('events');
const fs = require('hexo-fs');
const Module = require('module');
const vm = require('vm');
const pkg = require('../../package.json');
const logger = require('hexo-log');
const extend = require('../extend');
const Render = require('./render');
const registerModels = require('./register_models');
const Post = require('./post');
const Scaffold = require('./scaffold');
const Source = require('./source');
const Router = require('./router');
const Theme = require('../theme');
const Locals = require('./locals');
const defaultConfig = require('./default_config');
const loadDatabase = require('./load_database');
const multiConfigPath = require('./multi_config_path');
const resolve = require('resolve');
const full_url_for = require('../plugins/helper/full_url_for');
const { inherits } = require('util');

const libDir = dirname(__dirname);
const dbVersion = 1;

const stopWatcher = box => { if (box.isWatching()) box.unwatch(); };

const routeCache = new WeakMap();

const unique = array => array.filter((item, index, self) => self.indexOf(item) === index);

const castArray = obj => { return Array.isArray(obj) ? obj : [obj]; };

const createLoadThemeRoute = function(generatorResult, locals, ctx) {
  const { log, theme } = ctx;
  const { path, cache: useCache } = locals;

  const layout = unique(castArray(generatorResult.layout));
  const layoutLength = layout.length;

  // allways use cache in fragment_cache
  locals.cache = true;
  return () => {
    if (useCache && routeCache.has(generatorResult)) return routeCache.get(generatorResult);

    for (let i = 0; i < layoutLength; i++) {
      const name = layout[i];
      const view = theme.getView(name);

      if (view) {
        log.debug(`Rendering HTML ${name}: ${chalk.magenta(path)}`);
        return view.render(locals).tap(result => {
          if (useCache) {
            routeCache.set(generatorResult, result);
          }
        }).tapCatch(err => {
          log.error({ err }, `Render HTML failed: ${chalk.magenta(path)}`);
        });
      }
    }

    log.warn(`No layout: ${chalk.magenta(path)}`);
  };
};

function debounce(func, wait, immediate) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    }, wait);
    if (immediate && !timeout) func.apply(context, args);
  };
}

class Hexo {
  constructor(base = process.cwd(), args = {}) {
    Reflect.apply(EventEmitter, this, []);

    this.base_dir = base + sep;
    this.public_dir = join(base, 'public') + sep;
    this.source_dir = join(base, 'source') + sep;
    this.plugin_dir = join(base, 'node_modules') + sep;
    this.script_dir = join(base, 'scripts') + sep;
    this.scaffold_dir = join(base, 'scaffolds') + sep;
    this.theme_dir = join(base, 'themes', defaultConfig.theme) + sep;
    this.theme_script_dir = join(this.theme_dir, 'scripts') + sep;

    this.env = {
      args,
      debug: Boolean(args.debug),
      safe: Boolean(args.safe),
      silent: Boolean(args.silent),
      env: process.env.NODE_ENV || 'development',
      version: pkg.version,
      cmd: args._ ? args._[0] : '',
      init: false
    };

    this.extend = {
      console: new extend.Console(),
      deployer: new extend.Deployer(),
      filter: new extend.Filter(),
      generator: new extend.Generator(),
      helper: new extend.Helper(),
      migrator: new extend.Migrator(),
      processor: new extend.Processor(),
      renderer: new extend.Renderer(),
      tag: new extend.Tag()
    };

    this.config = Object.assign({}, defaultConfig);

    this.log = logger(this.env);

    this.render = new Render(this);

    this.route = new Router();

    this.post = new Post(this);

    this.scaffold = new Scaffold(this);

    this._dbLoaded = false;

    this._isGenerating = false;

    // If `output` is provided, use that as the
    // root for saving the db. Otherwise default to `base`.
    const dbPath = args.output || base;

    this.log.d(`Writing database to ${dbPath}/db.json`);

    this.database = new Database({
      version: dbVersion,
      path: join(dbPath, 'db.json')
    });

    const mcp = multiConfigPath(this);

    this.config_path = args.config ? mcp(base, args.config, args.output)
      : join(base, '_config.yml');

    registerModels(this);

    this.source = new Source(this);
    this.theme = new Theme(this);
    this.locals = new Locals(this);
    this._bindLocals();
  }

  _bindLocals() {
    const db = this.database;
    const { locals } = this;

    locals.set('posts', () => {
      const query = {};

      if (!this.config.future) {
        query.date = {$lte: Date.now()};
      }

      if (!this._showDrafts()) {
        query.published = true;
      }

      return db.model('Post').find(query);
    });

    locals.set('pages', () => {
      const query = {};

      if (!this.config.future) {
        query.date = {$lte: Date.now()};
      }

      return db.model('Page').find(query);
    });

    locals.set('categories', () => {
      // Ignore categories with zero posts
      return db.model('Category').filter(category => category.length);
    });

    locals.set('tags', () => {
      // Ignore tags with zero posts
      return db.model('Tag').filter(tag => tag.length);
    });

    locals.set('data', () => {
      const obj = {};

      db.model('Data').forEach(data => {
        obj[data._id] = data.data;
      });

      return obj;
    });
  }

  init() {
    this.log.debug('Hexo version: %s', chalk.magenta(this.version));
    this.log.debug('Working directory: %s', chalk.magenta(tildify(this.base_dir)));

    // Load internal plugins
    require('../plugins/console')(this);
    require('../plugins/filter')(this);
    require('../plugins/generator')(this);
    require('../plugins/helper')(this);
    require('../plugins/processor')(this);
    require('../plugins/renderer')(this);
    require('../plugins/tag')(this);

    // Load config
    return Promise.each([
      'update_package', // Update package.json
      'load_config', // Load config
      'load_plugins' // Load external plugins & scripts
    ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, {context: this})).then(() => {
      // Ready to go!
      this.emit('ready');
    });
  }

  call(name, args, callback) {
    if (!callback && typeof args === 'function') {
      callback = args;
      args = {};
    }

    return new Promise((resolve, reject) => {
      const c = this.extend.console.get(name);

      if (c) {
        Reflect.apply(c, this, [args]).then(resolve, reject);
      } else {
        reject(new Error(`Console \`${name}\` has not been registered yet!`));
      }
    }).asCallback(callback);
  }

  model(name, schema) {
    return this.database.model(name, schema);
  }

  resolvePlugin(name) {
    const baseDir = this.base_dir;

    try {
      // Try to resolve the plugin with the resolve.sync.
      return resolve.sync(name, { basedir: baseDir });
    } catch (err) {
      // There was an error (likely the plugin wasn't found), so return a possibly
      // non-existing path that a later part of the resolution process will check.
      return join(baseDir, 'node_modules', name);
    }
  }

  loadPlugin(path, callback) {
    return fs.readFile(path).then(script => {
      // Based on: https://github.com/joyent/node/blob/v0.10.33/src/node.js#L516
      const module = new Module(path);
      module.filename = path;
      module.paths = Module._nodeModulePaths(path);

      function require(path) {
        return module.require(path);
      }

      require.resolve = request => Module._resolveFilename(request, module);

      require.main = process.mainModule;
      require.extensions = Module._extensions;
      require.cache = Module._cache;

      script = `(function(exports, require, module, __filename, __dirname, hexo){${script}});`;

      const fn = vm.runInThisContext(script, path);

      return fn(module.exports, require, module, path, dirname(path), this);
    }).asCallback(callback);
  }

  _showDrafts() {
    const { args } = this.env;
    return args.draft || args.drafts || this.config.render_drafts;
  }

  load(callback) {
    return loadDatabase(this).then(() => {
      this.log.info('Start processing');

      return Promise.all([
        this.source.process(),
        this.theme.process()
      ]);
    }).then(() => this._generate({cache: false})).asCallback(callback);
  }

  watch(callback) {
    let useCache = false;
    if (this.env.cmd.startsWith('s')) {
      // enable cache when run hexo server
      useCache = true;
    }
    this._watchBox = debounce(() => this._generate({cache: useCache}), 100);

    return loadDatabase(this).then(() => {
      this.log.info('Start processing');

      return Promise.all([
        this.source.watch(),
        this.theme.watch()
      ]);
    }).then(() => {
      this.source.on('processAfter', this._watchBox);
      this.theme.on('processAfter', this._watchBox);

      return this._generate({cache: useCache});
    }).asCallback(callback);
  }

  unwatch() {
    if (this._watchBox != null) {
      this.source.removeListener('processAfter', this._watchBox);
      this.theme.removeListener('processAfter', this._watchBox);

      this._watchBox = null;
    }

    stopWatcher(this.source);
    stopWatcher(this.theme);
  }

  _generateLocals() {
    const { config, theme } = this;
    const ctx = { config: { url: this.config.url } };

    function Locals(path, locals) {
      this.page = typeof locals === 'object' ? locals : {};
      if (this.page.path == null) this.page.path = path;

      this.path = path;
      this.url = full_url_for.call(ctx, path);
    }

    Locals.prototype.config = config;
    Locals.prototype.theme = Object.assign({}, config, theme.config, config.theme_config);
    Locals.prototype._ = _;
    Locals.prototype.layout = 'layout';
    Locals.prototype.env = this.env;
    Locals.prototype.view_dir = join(this.theme_dir, 'layout') + sep;
    Locals.prototype.site = this.locals.toObject();

    return Locals;
  }

  _runGenerators() {
    this.locals.invalidate();
    const siteLocals = this.locals.toObject();
    const generators = this.extend.generator.list();
    const { log } = this;

    // Run generators
    return Promise.map(Object.keys(generators), key => {
      const generator = generators[key];

      return Reflect.apply(generator, this, [siteLocals]).then(data => {
        log.debug('Generator: %s', chalk.magenta(key));
        return data;
      });
    }).reduce((result, data) => {
      return data ? result.concat(data) : result;
    }, []);
  }

  _routerReflesh(runningGenerators, useCache) {
    const { route } = this;
    const routeList = route.list();
    const Locals = this._generateLocals();
    Locals.prototype.cache = useCache;

    return runningGenerators.map(generatorResult => {
      if (typeof generatorResult !== 'object' || generatorResult.path == null) return undefined;

      // add Route
      const path = route.format(generatorResult.path);
      const { data, layout } = generatorResult;

      if (!layout) {
        route.set(path, data);
        return path;
      }

      return this.execFilter('template_locals', new Locals(path, data), {context: this})
        .then(locals => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })
        .thenReturn(path);
    }).then(newRouteList => {
      // Remove old routes
      const removed = routeList.filter(item => !newRouteList.includes(item));

      for (let i = 0, len = removed.length; i < len; i++) {
        route.remove(removed[i]);
      }
    });
  }

  _generate(options = {}) {
    if (this._isGenerating) return;

    const useCache = options.cache;

    this._isGenerating = true;

    this.emit('generateBefore');

    // Run before_generate filters
    return this.execFilter('before_generate', this.locals.get('data'), {context: this})
      .then(() => this._routerReflesh(this._runGenerators(), useCache)).then(() => {
        this.emit('generateAfter');

        // Run after_generate filters
        return this.execFilter('after_generate', null, {context: this});
      }).finally(() => {
        this._isGenerating = false;
      });
  }

  exit(err) {
    if (err) {
      this.log.fatal(
        {err},
        'Something\'s wrong. Maybe you can find the solution here: %s',
        chalk.underline('https://hexo.io/docs/troubleshooting.html')
      );
    }

    return this.execFilter('before_exit', null, {context: this}).then(() => {
      this.emit('exit', err);
    });
  }

  execFilter(type, data, options) {
    return this.extend.filter.exec(type, data, options);
  }

  execFilterSync(type, data, options) {
    return this.extend.filter.execSync(type, data, options);
  }
}

inherits(Hexo, EventEmitter);

Hexo.lib_dir = libDir + sep;
Hexo.prototype.lib_dir = libDir + sep;

Hexo.core_dir = dirname(libDir) + sep;
Hexo.prototype.core_dir = dirname(libDir) + sep;

Hexo.version = pkg.version;
Hexo.prototype.version = pkg.version;

module.exports = Hexo;
<commit_msg>refactor(hexo/index): destructure modules (#3953)<commit_after>'use strict';

const Promise = require('bluebird');
const { sep, join, dirname } = require('path');
const tildify = require('tildify');
const Database = require('warehouse');
const _ = require('lodash');
const { magenta, underline } = require('chalk');
const { EventEmitter } = require('events');
const { readFile } = require('hexo-fs');
const Module = require('module');
const { runInThisContext } = require('vm');
const { version } = require('../../package.json');
const logger = require('hexo-log');
const { Console, Deployer, Filter, Generator, Helper, Migrator, Processor, Renderer, Tag } = require('../extend');
const Render = require('./render');
const registerModels = require('./register_models');
const Post = require('./post');
const Scaffold = require('./scaffold');
const Source = require('./source');
const Router = require('./router');
const Theme = require('../theme');
const Locals = require('./locals');
const defaultConfig = require('./default_config');
const loadDatabase = require('./load_database');
const multiConfigPath = require('./multi_config_path');
const { sync } = require('resolve');
const full_url_for = require('../plugins/helper/full_url_for');
const { inherits } = require('util');

const libDir = dirname(__dirname);
const dbVersion = 1;

const stopWatcher = box => { if (box.isWatching()) box.unwatch(); };

const routeCache = new WeakMap();

const unique = array => array.filter((item, index, self) => self.indexOf(item) === index);

const castArray = obj => { return Array.isArray(obj) ? obj : [obj]; };

const createLoadThemeRoute = function(generatorResult, locals, ctx) {
  const { log, theme } = ctx;
  const { path, cache: useCache } = locals;

  const layout = unique(castArray(generatorResult.layout));
  const layoutLength = layout.length;

  // allways use cache in fragment_cache
  locals.cache = true;
  return () => {
    if (useCache && routeCache.has(generatorResult)) return routeCache.get(generatorResult);

    for (let i = 0; i < layoutLength; i++) {
      const name = layout[i];
      const view = theme.getView(name);

      if (view) {
        log.debug(`Rendering HTML ${name}: ${magenta(path)}`);
        return view.render(locals).tap(result => {
          if (useCache) {
            routeCache.set(generatorResult, result);
          }
        }).tapCatch(err => {
          log.error({ err }, `Render HTML failed: ${magenta(path)}`);
        });
      }
    }

    log.warn(`No layout: ${magenta(path)}`);
  };
};

function debounce(func, wait, immediate) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    }, wait);
    if (immediate && !timeout) func.apply(context, args);
  };
}

class Hexo {
  constructor(base = process.cwd(), args = {}) {
    Reflect.apply(EventEmitter, this, []);

    this.base_dir = base + sep;
    this.public_dir = join(base, 'public') + sep;
    this.source_dir = join(base, 'source') + sep;
    this.plugin_dir = join(base, 'node_modules') + sep;
    this.script_dir = join(base, 'scripts') + sep;
    this.scaffold_dir = join(base, 'scaffolds') + sep;
    this.theme_dir = join(base, 'themes', defaultConfig.theme) + sep;
    this.theme_script_dir = join(this.theme_dir, 'scripts') + sep;

    this.env = {
      args,
      debug: Boolean(args.debug),
      safe: Boolean(args.safe),
      silent: Boolean(args.silent),
      env: process.env.NODE_ENV || 'development',
      version,
      cmd: args._ ? args._[0] : '',
      init: false
    };

    this.extend = {
      console: new Console(),
      deployer: new Deployer(),
      filter: new Filter(),
      generator: new Generator(),
      helper: new Helper(),
      migrator: new Migrator(),
      processor: new Processor(),
      renderer: new Renderer(),
      tag: new Tag()
    };

    this.config = Object.assign({}, defaultConfig);

    this.log = logger(this.env);

    this.render = new Render(this);

    this.route = new Router();

    this.post = new Post(this);

    this.scaffold = new Scaffold(this);

    this._dbLoaded = false;

    this._isGenerating = false;

    // If `output` is provided, use that as the
    // root for saving the db. Otherwise default to `base`.
    const dbPath = args.output || base;

    this.log.d(`Writing database to ${dbPath}/db.json`);

    this.database = new Database({
      version: dbVersion,
      path: join(dbPath, 'db.json')
    });

    const mcp = multiConfigPath(this);

    this.config_path = args.config ? mcp(base, args.config, args.output)
      : join(base, '_config.yml');

    registerModels(this);

    this.source = new Source(this);
    this.theme = new Theme(this);
    this.locals = new Locals(this);
    this._bindLocals();
  }

  _bindLocals() {
    const db = this.database;
    const { locals } = this;

    locals.set('posts', () => {
      const query = {};

      if (!this.config.future) {
        query.date = {$lte: Date.now()};
      }

      if (!this._showDrafts()) {
        query.published = true;
      }

      return db.model('Post').find(query);
    });

    locals.set('pages', () => {
      const query = {};

      if (!this.config.future) {
        query.date = {$lte: Date.now()};
      }

      return db.model('Page').find(query);
    });

    locals.set('categories', () => {
      // Ignore categories with zero posts
      return db.model('Category').filter(category => category.length);
    });

    locals.set('tags', () => {
      // Ignore tags with zero posts
      return db.model('Tag').filter(tag => tag.length);
    });

    locals.set('data', () => {
      const obj = {};

      db.model('Data').forEach(data => {
        obj[data._id] = data.data;
      });

      return obj;
    });
  }

  init() {
    this.log.debug('Hexo version: %s', magenta(this.version));
    this.log.debug('Working directory: %s', magenta(tildify(this.base_dir)));

    // Load internal plugins
    require('../plugins/console')(this);
    require('../plugins/filter')(this);
    require('../plugins/generator')(this);
    require('../plugins/helper')(this);
    require('../plugins/processor')(this);
    require('../plugins/renderer')(this);
    require('../plugins/tag')(this);

    // Load config
    return Promise.each([
      'update_package', // Update package.json
      'load_config', // Load config
      'load_plugins' // Load external plugins & scripts
    ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, {context: this})).then(() => {
      // Ready to go!
      this.emit('ready');
    });
  }

  call(name, args, callback) {
    if (!callback && typeof args === 'function') {
      callback = args;
      args = {};
    }

    return new Promise((resolve, reject) => {
      const c = this.extend.console.get(name);

      if (c) {
        Reflect.apply(c, this, [args]).then(resolve, reject);
      } else {
        reject(new Error(`Console \`${name}\` has not been registered yet!`));
      }
    }).asCallback(callback);
  }

  model(name, schema) {
    return this.database.model(name, schema);
  }

  resolvePlugin(name) {
    const baseDir = this.base_dir;

    try {
      // Try to resolve the plugin with the resolve.sync.
      return sync(name, { basedir: baseDir });
    } catch (err) {
      // There was an error (likely the plugin wasn't found), so return a possibly
      // non-existing path that a later part of the resolution process will check.
      return join(baseDir, 'node_modules', name);
    }
  }

  loadPlugin(path, callback) {
    return readFile(path).then(script => {
      // Based on: https://github.com/joyent/node/blob/v0.10.33/src/node.js#L516
      const module = new Module(path);
      module.filename = path;
      module.paths = Module._nodeModulePaths(path);

      function require(path) {
        return module.require(path);
      }

      require.resolve = request => Module._resolveFilename(request, module);

      require.main = process.mainModule;
      require.extensions = Module._extensions;
      require.cache = Module._cache;

      script = `(function(exports, require, module, __filename, __dirname, hexo){${script}});`;

      const fn = runInThisContext(script, path);

      return fn(module.exports, require, module, path, dirname(path), this);
    }).asCallback(callback);
  }

  _showDrafts() {
    const { args } = this.env;
    return args.draft || args.drafts || this.config.render_drafts;
  }

  load(callback) {
    return loadDatabase(this).then(() => {
      this.log.info('Start processing');

      return Promise.all([
        this.source.process(),
        this.theme.process()
      ]);
    }).then(() => this._generate({cache: false})).asCallback(callback);
  }

  watch(callback) {
    let useCache = false;
    if (this.env.cmd.startsWith('s')) {
      // enable cache when run hexo server
      useCache = true;
    }
    this._watchBox = debounce(() => this._generate({cache: useCache}), 100);

    return loadDatabase(this).then(() => {
      this.log.info('Start processing');

      return Promise.all([
        this.source.watch(),
        this.theme.watch()
      ]);
    }).then(() => {
      this.source.on('processAfter', this._watchBox);
      this.theme.on('processAfter', this._watchBox);

      return this._generate({cache: useCache});
    }).asCallback(callback);
  }

  unwatch() {
    if (this._watchBox != null) {
      this.source.removeListener('processAfter', this._watchBox);
      this.theme.removeListener('processAfter', this._watchBox);

      this._watchBox = null;
    }

    stopWatcher(this.source);
    stopWatcher(this.theme);
  }

  _generateLocals() {
    const { config, theme } = this;
    const ctx = { config: { url: this.config.url } };

    function Locals(path, locals) {
      this.page = typeof locals === 'object' ? locals : {};
      if (this.page.path == null) this.page.path = path;

      this.path = path;
      this.url = full_url_for.call(ctx, path);
    }

    Locals.prototype.config = config;
    Locals.prototype.theme = Object.assign({}, config, theme.config, config.theme_config);
    Locals.prototype._ = _;
    Locals.prototype.layout = 'layout';
    Locals.prototype.env = this.env;
    Locals.prototype.view_dir = join(this.theme_dir, 'layout') + sep;
    Locals.prototype.site = this.locals.toObject();

    return Locals;
  }

  _runGenerators() {
    this.locals.invalidate();
    const siteLocals = this.locals.toObject();
    const generators = this.extend.generator.list();
    const { log } = this;

    // Run generators
    return Promise.map(Object.keys(generators), key => {
      const generator = generators[key];

      return Reflect.apply(generator, this, [siteLocals]).then(data => {
        log.debug('Generator: %s', magenta(key));
        return data;
      });
    }).reduce((result, data) => {
      return data ? result.concat(data) : result;
    }, []);
  }

  _routerReflesh(runningGenerators, useCache) {
    const { route } = this;
    const routeList = route.list();
    const Locals = this._generateLocals();
    Locals.prototype.cache = useCache;

    return runningGenerators.map(generatorResult => {
      if (typeof generatorResult !== 'object' || generatorResult.path == null) return undefined;

      // add Route
      const path = route.format(generatorResult.path);
      const { data, layout } = generatorResult;

      if (!layout) {
        route.set(path, data);
        return path;
      }

      return this.execFilter('template_locals', new Locals(path, data), {context: this})
        .then(locals => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })
        .thenReturn(path);
    }).then(newRouteList => {
      // Remove old routes
      const removed = routeList.filter(item => !newRouteList.includes(item));

      for (let i = 0, len = removed.length; i < len; i++) {
        route.remove(removed[i]);
      }
    });
  }

  _generate(options = {}) {
    if (this._isGenerating) return;

    const useCache = options.cache;

    this._isGenerating = true;

    this.emit('generateBefore');

    // Run before_generate filters
    return this.execFilter('before_generate', this.locals.get('data'), {context: this})
      .then(() => this._routerReflesh(this._runGenerators(), useCache)).then(() => {
        this.emit('generateAfter');

        // Run after_generate filters
        return this.execFilter('after_generate', null, {context: this});
      }).finally(() => {
        this._isGenerating = false;
      });
  }

  exit(err) {
    if (err) {
      this.log.fatal(
        {err},
        'Something\'s wrong. Maybe you can find the solution here: %s',
        underline('https://hexo.io/docs/troubleshooting.html')
      );
    }

    return this.execFilter('before_exit', null, {context: this}).then(() => {
      this.emit('exit', err);
    });
  }

  execFilter(type, data, options) {
    return this.extend.filter.exec(type, data, options);
  }

  execFilterSync(type, data, options) {
    return this.extend.filter.execSync(type, data, options);
  }
}

inherits(Hexo, EventEmitter);

Hexo.lib_dir = libDir + sep;
Hexo.prototype.lib_dir = libDir + sep;

Hexo.core_dir = dirname(libDir) + sep;
Hexo.prototype.core_dir = dirname(libDir) + sep;

Hexo.version = version;
Hexo.prototype.version = version;

module.exports = Hexo;
<|endoftext|>