<commit_before>'use strict';

var get = require('lodash.get');
var plurals = require('./plurals');

module.exports = Gettext;

/**
 * Creates and returns a new Gettext instance.
 *
 * @constructor
 * @param  {Object}  [options]             A set of options
 * @param  {String}  options.sourceLocale  The locale that the source code and its
 *                                         texts are written in. Translations for
 *                                         this locale is not necessary.
 * @param  {Boolean} options.debug         Whether to output debug info into the
 *                                         console.
 * @return {Object}  A Gettext instance
 */
function Gettext(options) {
    options = options || {};

    this.catalogs = {};
    this.locale = '';
    this.domain = 'messages';

    this.listeners = [];

    // Set source locale
    this.sourceLocale = '';
    if (options.sourceLocale) {
        if (typeof options.sourceLocale === 'string') {
            this.sourceLocale = options.sourceLocale;
        }
        else {
            this.warn('The `sourceLocale` option should be a string');
        }
    }

    // Set debug flag
    this.debug = 'debug' in options && options.debug === true;
}

/**
 * Adds an event listener.
 *
 * @param  {String}   eventName  An event name
 * @param  {Function} callback   An event handler function
 */
Gettext.prototype.on = function(eventName, callback) {
    this.listeners.push({
        eventName: eventName,
        callback: callback
    });
};

/**
 * Removes an event listener.
 *
 * @param  {String}   eventName  An event name
 * @param  {Function} callback   A previously registered event handler function
 */
Gettext.prototype.off = function(eventName, callback) {
    this.listeners = this.listeners.filter(function(listener) {
        return (
            listener.eventName === eventName &&
            listener.callback === callback
        ) === false;
    });
};

/**
 * Emits an event to all registered event listener.
 *
 * @private
 * @param  {String} eventName  An event name
 * @param  {any}    eventData  Data to pass to event listeners
 */
Gettext.prototype.emit = function(eventName, eventData) {
    for (var i = 0; i < this.listeners.length; i++) {
        var listener = this.listeners[i];
        if (listener.eventName === eventName) {
            listener.callback(eventData);
        }
    }
};

/**
 * Logs a warning to the console if debug mode is enabled.
 *
 * @ignore
 * @param  {String} message  A warning message
 */
Gettext.prototype.warn = function(message) {
    if (this.debug) {
        console.warn(message);
    }

    this.emit('error', message);
};

/**
 * Stores a set of translations in the set of gettext
 * catalogs.
 *
 * @example
 *     gt.addTranslations('sv-SE', 'messages', translationsObject)
 *
 * @param {String} locale        A locale string
 * @param {String} domain        A domain name
 * @param {Object} translations  An object of gettext-parser JSON shape
 */
Gettext.prototype.addTranslations = function(locale, domain, translations) {
    if (!this.catalogs[locale]) {
        this.catalogs[locale] = {};
    }

    this.catalogs[locale][domain] = translations;
};

/**
 * Sets the locale to get translated messages for.
 *
 * @example
 *     gt.setLocale('sv-SE')
 *
 * @param {String} locale  A locale
 */
Gettext.prototype.setLocale = function(locale) {
    if (typeof locale !== 'string') {
        this.warn(
            'You called setLocale() with an argument of type ' + (typeof locale) + '. ' +
            'The locale must be a string.'
        );
        return;
    }

    if (locale.trim() === '') {
        this.warn('You called setLocale() with an empty value, which makes little sense.');
    }

    if (locale !== this.sourceLocale && !this.catalogs[locale]) {
        this.warn('You called setLocale() with "' + locale + '", but no translations for that locale has been added.');
    }

    this.locale = locale;
};

/**
 * Sets the default gettext domain.
 *
 * @example
 *     gt.setTextDomain('domainname')
 *
 * @param {String} domain  A gettext domain name
 */
Gettext.prototype.setTextDomain = function(domain) {
    if (typeof domain !== 'string') {
        this.warn(
            'You called setTextDomain() with an argument of type ' + (typeof domain) + '. ' +
            'The domain must be a string.'
        );
        return;
    }

    if (domain.trim() === '') {
        this.warn('You called setTextDomain() with an empty `domain` value.');
    }

    this.domain = domain;
};

/**
 * Translates a string using the default textdomain
 *
 * @example
 *     gt.gettext('Some text')
 *
 * @param  {String} msgid  String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.gettext = function(msgid) {
    return this.dnpgettext(this.domain, '', msgid);
};

/**
 * Translates a string using a specific domain
 *
 * @example
 *     gt.dgettext('domainname', 'Some text')
 *
 * @param  {String} domain  A gettext domain name
 * @param  {String} msgid   String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dgettext = function(domain, msgid) {
    return this.dnpgettext(domain, '', msgid);
};

/**
 * Translates a plural string using the default textdomain
 *
 * @example
 *     gt.ngettext('One thing', 'Many things', numberOfThings)
 *
 * @param  {String} msgid        String to be translated when count is not plural
 * @param  {String} msgidPlural  String to be translated when count is plural
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.ngettext = function(msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a plural string using a specific textdomain
 *
 * @example
 *     gt.dngettext('domainname', 'One thing', 'Many things', numberOfThings)
 *
 * @param  {String} domain       A gettext domain name
 * @param  {String} msgid        String to be translated when count is not plural
 * @param  {String} msgidPlural  String to be translated when count is plural
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dngettext = function(domain, msgid, msgidPlural, count) {
    return this.dnpgettext(domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a string from a specific context using the default textdomain
 *
 * @example
 *    gt.pgettext('sports', 'Back')
 *
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.pgettext = function(msgctxt, msgid) {
    return this.dnpgettext(this.domain, msgctxt, msgid);
};

/**
 * Translates a string from a specific context using s specific textdomain
 *
 * @example
 *     gt.dpgettext('domainname', 'sports', 'Back')
 *
 * @param  {String} domain   A gettext domain name
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dpgettext = function(domain, msgctxt, msgid) {
    return this.dnpgettext(domain, msgctxt, msgid);
};

/**
 * Translates a plural string from a specific context using the default textdomain
 *
 * @example
 *     gt.npgettext('sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param  {String} msgctxt      Translation context
 * @param  {String} msgid        String to be translated when count is not plural
 * @param  {String} msgidPlural  String to be translated when count is plural
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.npgettext = function(msgctxt, msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, msgctxt, msgid, msgidPlural, count);
};

/**
 * Translates a plural string from a specifi context using a specific textdomain
 *
 * @example
 *     gt.dnpgettext('domainname', 'sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param  {String} domain       A gettext domain name
 * @param  {String} msgctxt      Translation context
 * @param  {String} msgid        String to be translated
 * @param  {String} msgidPlural  If no translation was found, return this on count!=1
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dnpgettext = function(domain, msgctxt, msgid, msgidPlural, count) {
    var defaultTranslation = msgid;
    var translation;
    var index;

    msgctxt = msgctxt || '';

    if (!isNaN(count) && count !== 1) {
        defaultTranslation = msgidPlural || msgid;
    }

    translation = this._getTranslation(domain, msgctxt, msgid);

    if (translation) {
        if (typeof count === 'number') {
            var pluralsFunc = plurals[Gettext.getLanguageCode(this.locale)].pluralsFunc;
            index = pluralsFunc(count);
            if (typeof index === 'boolean') {
                index = index ? 1 : 0;
            }
        } else {
            index = 0;
        }

        return translation.msgstr[index] || defaultTranslation;
    }
    else if (!this.sourceLocale || this.locale !== this.sourceLocale) {
        this.warn('No translation was found for msgid "' + msgid + '" in msgctxt "' + msgctxt + '" and domain "' + domain + '"');
    }

    return defaultTranslation;
};

/**
 * Retrieves comments object for a translation. The comments object
 * has the shape `{ translator, extracted, reference, flag, previous }`.
 *
 * @example
 *     const comment = gt.getComment('domainname', 'sports', 'Backs')
 *
 * @private
 * @param  {String} domain   A gettext domain name
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {Object} Comments object or false if not found
 */
Gettext.prototype.getComment = function(domain, msgctxt, msgid) {
    var translation;

    translation = this._getTranslation(domain, msgctxt, msgid);
    if (translation) {
        return translation.comments || {};
    }

    return {};
};

/**
 * Retrieves translation object from the domain and context
 *
 * @private
 * @param  {String} domain   A gettext domain name
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {Object} Translation object or false if not found
 */
Gettext.prototype._getTranslation = function(domain, msgctxt, msgid) {
    msgctxt = msgctxt || '';

    return get(this.catalogs, [this.locale, domain, 'translations', msgctxt, msgid]);
};

/**
 * Returns the language code part of a locale
 *
 * @example
 *     Gettext.getLanguageCode('sv-SE')
 *     // -> "sv"
 *
 * @private
 * @param   {String} locale  A case-insensitive locale string
 * @returns {String} A language code
 */
Gettext.getLanguageCode = function(locale) {
    return locale.split(/[\-_]/)[0].toLowerCase();
};

/* C-style aliases */

/**
 * C-style alias for [setTextDomain](#gettextsettextdomaindomain)
 *
 * @see Gettext#setTextDomain
 */
Gettext.prototype.textdomain = function(domain) {
    if (this.debug) {
        console.warn('textdomain(domain) was used to set locales in node-gettext v1. ' +
            'Make sure you are using it for domains, and switch to setLocale(locale) if you are not.\n\n ' +
            'To read more about the migration from node-gettext v1 to v2, ' +
            'see https://github.com/alexanderwallin/node-gettext/#migrating-from-1x-to-2x\n\n' +
            'This warning will be removed in the final 2.0.0');
    }

    this.setTextDomain(domain);
};

/**
 * C-style alias for [setLocale](#gettextsetlocalelocale)
 *
 * @see Gettext#setLocale
 */
Gettext.prototype.setlocale = function(locale) {
    this.setLocale(locale);
};

/* Deprecated functions */

/**
 * This function will be removed in the final 2.0.0 release.
 *
 * @deprecated
 */
Gettext.prototype.addTextdomain = function() {
    console.error('addTextdomain() is deprecated.\n\n' +
        '* To add translations, use addTranslations()\n' +
        '* To set the default domain, use setTextDomain() (or its alias textdomain())\n' +
        '\n' +
        'To read more about the migration from node-gettext v1 to v2, ' +
        'see https://github.com/alexanderwallin/node-gettext/#migrating-from-1x-to-2x');
};
<commit_msg>Emit errors as Errors<commit_after>'use strict';

var get = require('lodash.get');
var plurals = require('./plurals');

module.exports = Gettext;

/**
 * Creates and returns a new Gettext instance.
 *
 * @constructor
 * @param  {Object}  [options]             A set of options
 * @param  {String}  options.sourceLocale  The locale that the source code and its
 *                                         texts are written in. Translations for
 *                                         this locale is not necessary.
 * @param  {Boolean} options.debug         Whether to output debug info into the
 *                                         console.
 * @return {Object}  A Gettext instance
 */
function Gettext(options) {
    options = options || {};

    this.catalogs = {};
    this.locale = '';
    this.domain = 'messages';

    this.listeners = [];

    // Set source locale
    this.sourceLocale = '';
    if (options.sourceLocale) {
        if (typeof options.sourceLocale === 'string') {
            this.sourceLocale = options.sourceLocale;
        }
        else {
            this.warn('The `sourceLocale` option should be a string');
        }
    }

    // Set debug flag
    this.debug = 'debug' in options && options.debug === true;
}

/**
 * Adds an event listener.
 *
 * @param  {String}   eventName  An event name
 * @param  {Function} callback   An event handler function
 */
Gettext.prototype.on = function(eventName, callback) {
    this.listeners.push({
        eventName: eventName,
        callback: callback
    });
};

/**
 * Removes an event listener.
 *
 * @param  {String}   eventName  An event name
 * @param  {Function} callback   A previously registered event handler function
 */
Gettext.prototype.off = function(eventName, callback) {
    this.listeners = this.listeners.filter(function(listener) {
        return (
            listener.eventName === eventName &&
            listener.callback === callback
        ) === false;
    });
};

/**
 * Emits an event to all registered event listener.
 *
 * @private
 * @param  {String} eventName  An event name
 * @param  {any}    eventData  Data to pass to event listeners
 */
Gettext.prototype.emit = function(eventName, eventData) {
    for (var i = 0; i < this.listeners.length; i++) {
        var listener = this.listeners[i];
        if (listener.eventName === eventName) {
            listener.callback(eventData);
        }
    }
};

/**
 * Logs a warning to the console if debug mode is enabled.
 *
 * @ignore
 * @param  {String} message  A warning message
 */
Gettext.prototype.warn = function(message) {
    if (this.debug) {
        console.warn(message);
    }

    this.emit('error', new Error(message));
};

/**
 * Stores a set of translations in the set of gettext
 * catalogs.
 *
 * @example
 *     gt.addTranslations('sv-SE', 'messages', translationsObject)
 *
 * @param {String} locale        A locale string
 * @param {String} domain        A domain name
 * @param {Object} translations  An object of gettext-parser JSON shape
 */
Gettext.prototype.addTranslations = function(locale, domain, translations) {
    if (!this.catalogs[locale]) {
        this.catalogs[locale] = {};
    }

    this.catalogs[locale][domain] = translations;
};

/**
 * Sets the locale to get translated messages for.
 *
 * @example
 *     gt.setLocale('sv-SE')
 *
 * @param {String} locale  A locale
 */
Gettext.prototype.setLocale = function(locale) {
    if (typeof locale !== 'string') {
        this.warn(
            'You called setLocale() with an argument of type ' + (typeof locale) + '. ' +
            'The locale must be a string.'
        );
        return;
    }

    if (locale.trim() === '') {
        this.warn('You called setLocale() with an empty value, which makes little sense.');
    }

    if (locale !== this.sourceLocale && !this.catalogs[locale]) {
        this.warn('You called setLocale() with "' + locale + '", but no translations for that locale has been added.');
    }

    this.locale = locale;
};

/**
 * Sets the default gettext domain.
 *
 * @example
 *     gt.setTextDomain('domainname')
 *
 * @param {String} domain  A gettext domain name
 */
Gettext.prototype.setTextDomain = function(domain) {
    if (typeof domain !== 'string') {
        this.warn(
            'You called setTextDomain() with an argument of type ' + (typeof domain) + '. ' +
            'The domain must be a string.'
        );
        return;
    }

    if (domain.trim() === '') {
        this.warn('You called setTextDomain() with an empty `domain` value.');
    }

    this.domain = domain;
};

/**
 * Translates a string using the default textdomain
 *
 * @example
 *     gt.gettext('Some text')
 *
 * @param  {String} msgid  String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.gettext = function(msgid) {
    return this.dnpgettext(this.domain, '', msgid);
};

/**
 * Translates a string using a specific domain
 *
 * @example
 *     gt.dgettext('domainname', 'Some text')
 *
 * @param  {String} domain  A gettext domain name
 * @param  {String} msgid   String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dgettext = function(domain, msgid) {
    return this.dnpgettext(domain, '', msgid);
};

/**
 * Translates a plural string using the default textdomain
 *
 * @example
 *     gt.ngettext('One thing', 'Many things', numberOfThings)
 *
 * @param  {String} msgid        String to be translated when count is not plural
 * @param  {String} msgidPlural  String to be translated when count is plural
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.ngettext = function(msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a plural string using a specific textdomain
 *
 * @example
 *     gt.dngettext('domainname', 'One thing', 'Many things', numberOfThings)
 *
 * @param  {String} domain       A gettext domain name
 * @param  {String} msgid        String to be translated when count is not plural
 * @param  {String} msgidPlural  String to be translated when count is plural
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dngettext = function(domain, msgid, msgidPlural, count) {
    return this.dnpgettext(domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a string from a specific context using the default textdomain
 *
 * @example
 *    gt.pgettext('sports', 'Back')
 *
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.pgettext = function(msgctxt, msgid) {
    return this.dnpgettext(this.domain, msgctxt, msgid);
};

/**
 * Translates a string from a specific context using s specific textdomain
 *
 * @example
 *     gt.dpgettext('domainname', 'sports', 'Back')
 *
 * @param  {String} domain   A gettext domain name
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dpgettext = function(domain, msgctxt, msgid) {
    return this.dnpgettext(domain, msgctxt, msgid);
};

/**
 * Translates a plural string from a specific context using the default textdomain
 *
 * @example
 *     gt.npgettext('sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param  {String} msgctxt      Translation context
 * @param  {String} msgid        String to be translated when count is not plural
 * @param  {String} msgidPlural  String to be translated when count is plural
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.npgettext = function(msgctxt, msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, msgctxt, msgid, msgidPlural, count);
};

/**
 * Translates a plural string from a specifi context using a specific textdomain
 *
 * @example
 *     gt.dnpgettext('domainname', 'sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param  {String} domain       A gettext domain name
 * @param  {String} msgctxt      Translation context
 * @param  {String} msgid        String to be translated
 * @param  {String} msgidPlural  If no translation was found, return this on count!=1
 * @param  {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dnpgettext = function(domain, msgctxt, msgid, msgidPlural, count) {
    var defaultTranslation = msgid;
    var translation;
    var index;

    msgctxt = msgctxt || '';

    if (!isNaN(count) && count !== 1) {
        defaultTranslation = msgidPlural || msgid;
    }

    translation = this._getTranslation(domain, msgctxt, msgid);

    if (translation) {
        if (typeof count === 'number') {
            var pluralsFunc = plurals[Gettext.getLanguageCode(this.locale)].pluralsFunc;
            index = pluralsFunc(count);
            if (typeof index === 'boolean') {
                index = index ? 1 : 0;
            }
        } else {
            index = 0;
        }

        return translation.msgstr[index] || defaultTranslation;
    }
    else if (!this.sourceLocale || this.locale !== this.sourceLocale) {
        this.warn('No translation was found for msgid "' + msgid + '" in msgctxt "' + msgctxt + '" and domain "' + domain + '"');
    }

    return defaultTranslation;
};

/**
 * Retrieves comments object for a translation. The comments object
 * has the shape `{ translator, extracted, reference, flag, previous }`.
 *
 * @example
 *     const comment = gt.getComment('domainname', 'sports', 'Backs')
 *
 * @private
 * @param  {String} domain   A gettext domain name
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {Object} Comments object or false if not found
 */
Gettext.prototype.getComment = function(domain, msgctxt, msgid) {
    var translation;

    translation = this._getTranslation(domain, msgctxt, msgid);
    if (translation) {
        return translation.comments || {};
    }

    return {};
};

/**
 * Retrieves translation object from the domain and context
 *
 * @private
 * @param  {String} domain   A gettext domain name
 * @param  {String} msgctxt  Translation context
 * @param  {String} msgid    String to be translated
 * @return {Object} Translation object or false if not found
 */
Gettext.prototype._getTranslation = function(domain, msgctxt, msgid) {
    msgctxt = msgctxt || '';

    return get(this.catalogs, [this.locale, domain, 'translations', msgctxt, msgid]);
};

/**
 * Returns the language code part of a locale
 *
 * @example
 *     Gettext.getLanguageCode('sv-SE')
 *     // -> "sv"
 *
 * @private
 * @param   {String} locale  A case-insensitive locale string
 * @returns {String} A language code
 */
Gettext.getLanguageCode = function(locale) {
    return locale.split(/[\-_]/)[0].toLowerCase();
};

/* C-style aliases */

/**
 * C-style alias for [setTextDomain](#gettextsettextdomaindomain)
 *
 * @see Gettext#setTextDomain
 */
Gettext.prototype.textdomain = function(domain) {
    if (this.debug) {
        console.warn('textdomain(domain) was used to set locales in node-gettext v1. ' +
            'Make sure you are using it for domains, and switch to setLocale(locale) if you are not.\n\n ' +
            'To read more about the migration from node-gettext v1 to v2, ' +
            'see https://github.com/alexanderwallin/node-gettext/#migrating-from-1x-to-2x\n\n' +
            'This warning will be removed in the final 2.0.0');
    }

    this.setTextDomain(domain);
};

/**
 * C-style alias for [setLocale](#gettextsetlocalelocale)
 *
 * @see Gettext#setLocale
 */
Gettext.prototype.setlocale = function(locale) {
    this.setLocale(locale);
};

/* Deprecated functions */

/**
 * This function will be removed in the final 2.0.0 release.
 *
 * @deprecated
 */
Gettext.prototype.addTextdomain = function() {
    console.error('addTextdomain() is deprecated.\n\n' +
        '* To add translations, use addTranslations()\n' +
        '* To set the default domain, use setTextDomain() (or its alias textdomain())\n' +
        '\n' +
        'To read more about the migration from node-gettext v1 to v2, ' +
        'see https://github.com/alexanderwallin/node-gettext/#migrating-from-1x-to-2x');
};
<|endoftext|>


<commit_before>/* jslint node: true, esnext: true */

"use strict";

const rootToken = {
	precedence: 0,
	toString() {
		return `${this.type}: ${this.value} [${this.precedence}]`;
	},
	led(grammar, left) {
		return 0;
	},
	nud(grammar) {
		return this;
	},
	combine() {
		return 0;
	}
};

/**
 * Token representing "end of file"
 */
const EOF = Object.create(rootToken, {
	type: {
		value: 'EOF'
	}
});

/**
 * Creates a grammar for later parsing
 * @param {Object} options
 * @return {Object} parser
 */
function createGrammar(options) {
	const maxOperatorLengthForFirstChar = {};
	const registeredTokens = {};

	const operatorTypes = {
		"prefix": {
			nud(grammar) {
				return this.combine(grammar.expression(this.precedence));
			}
		},
		"infix": {
			led(grammar, left) {
				return this.combine(left, grammar.expression(this.precedence));
			}
		},
		"infixr": {
			led(grammar, left) {
				return this.combine(left, grammar.expression(this.precedence - 1));
			}
		}
	};

	function registerOperator(id, options, operatorType) {
		const props = {
			type: {
				value: 'operator'
			},
			value: {
				value: id
			},
			led: {
				value: operatorType.led,
				writable: true
			}
		};

		const op = Object.create(rootToken, props);
		Object.assign(op, options);

		registeredTokens[id] = op;
		return op;
	}

	for (let operatorTypeName in operatorTypes) {
		const ops = options[operatorTypeName];
		const operatorType = operatorTypes[operatorTypeName];

		for (let c in ops) {
			const firstChar = c[0];
			const maxLength = maxOperatorLengthForFirstChar[firstChar] || 0;

			if (maxLength < c.length) {
				maxOperatorLengthForFirstChar[firstChar] = c.length;
			}
			registerOperator(c, ops[c], operatorType);
		}
	}

	const tokenizer = function* (chunk) {
		let lineNumber = 1;

		function error(message, values) {
			message += "," + lineNumber;
			if (values) message += ': ' + JSON.stringify(values);
			throw new Error(message);
		}

		function makeIdentifier(value) {
			return Object.create(rootToken, {
				parseWithPrefix: {
					value: function (token) {
						console.log(`got identifier: ${token.value}`);
						return 0;
					}
				},
				type: {
					value: 'identifier'
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeOperator(value) {
			return Object.create(registeredTokens[value], {
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeNumber(value) {
			return Object.create(rootToken, {
				type: {
					value: 'number'
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeString(value) {
			return Object.create(rootToken, {
				type: {
					value: 'string'
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeToken(type, value) {
			return Object.create(rootToken, {
				type: {
					value: type
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		let c, str;
		let firstCharInLine = 0;
		let i = 0;
		let op, operatorLength;
		const length = chunk.length;

		while (c = chunk[i]) {
			if (c <= ' ') {
				if (c === '\n') {
					lineNumber += 1;
					firstCharInLine = i;
				}
				i += 1;
				c = chunk[i];
			} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_') {
				str = c;
				i += 1;
				for (;;) {
					c = chunk[i];
					if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
						(c >= '0' && c <= '9') || c === '_') {
						str += c;
						i += 1;
					} else {
						break;
					}
				}

				yield makeIdentifier(str);
			} else if (c >= '0' && c <= '9') {
				str = c;
				i += 1;
				for (; i < length;) {
					c = chunk[i];
					if (c < '0' || c > '9') {
						break;
					}
					i += 1;
					str += c;
				}

				yield makeNumber(+str);
			} else if (c === '"') {
				i += 1;
				str = '';
				for (; i < length;) {
					c = chunk[i];
					if (c === '"') {
						i += 1;
						yield makeString(str);
						break;
					} else if (c === '\\') {
						i += 1;
						c = chunk[i];
						switch (c) {
							case 'b':
								c = '\b';
								break;
							case 'f':
								c = '\f';
								break;
							case 'n':
								c = '\n';
								break;
							case 'r':
								c = '\r';
								break;
							case 't':
								c = '\t';
								break;
							case 'u':
								c = parseInt(chunk.substr(i + 1, 4), 16);
								if (!isFinite(c) || c < 0) {
									error("Unterminated string", {
										value: c
									});
								}
								c = String.fromCharCode(c);
								i += 4;
								break;
						}
						str += c;
						i += 1;
					} else {
						str += c;
						i += 1;
					}
				}
				/*error("Unterminated string", {
					value: c
				});*/
			} else if (operatorLength = maxOperatorLengthForFirstChar[c]) {
				c = chunk.substring(i, i + operatorLength);
				if (registeredTokens[c]) {
					yield makeOperator(c);
					i += operatorLength;
				} else {
					operatorLength -= 1;
					c = chunk.substring(i, i + operatorLength);
					if (registeredTokens[c]) {
						yield makeOperator(c);
						i += operatorLength;
					} else {
						operatorLength -= 1;
						c = chunk.substring(i, i + operatorLength);
						if (registeredTokens[c]) {
							yield makeOperator(c);
							i += operatorLength;
						} else {
							error("Unknown operator", {
								value: c
							});
						}
					}
				}

			} else {
				i += 1;
				error("Unknown char", {
					value: c
				});
			}
		}
	}; // tokenizer

	const grammar = Object.create({
		tokenizer: tokenizer,

		/**
		 * Parses the input and delivers the outermoost expression.
		 * @param {String} chunk input text
		 * @return {Object} evaluated input
		 */
		parse(chunk) {
			const tokens = tokenizer(chunk);
			let token;

			this.advance = function advance(id) {
				if (id !== undefined && token.id !== undefined && token.id !== id) {
					console.log(`Got ${token.id} expected ${id}`);
				}

				const n = tokens.next();

				if (n.done) {
					token = EOF;
					return token;
				}
				token = n.value;
				return token;
			};

			token = this.advance();

			this.expression = function (precedence) {
				let t = token;
				grammar.advance();
				let left = t.nud(grammar);

				while (precedence < token.precedence) {
					t = token;
					grammar.advance();
					left = t.led(grammar, left);
				}

				return left;
			};

			return this.expression(token.precedence);
		}
	});

	return grammar;
}

exports.createGrammar = createGrammar;
<commit_msg>fix: use const for loop variables too<commit_after>/* jslint node: true, esnext: true */

"use strict";

const rootToken = {
	precedence: 0,
	toString() {
		return `${this.type}: ${this.value} [${this.precedence}]`;
	},
	led(grammar, left) {
		return 0;
	},
	nud(grammar) {
		return this;
	},
	combine() {
		return 0;
	}
};

/**
 * Token representing "end of file"
 */
const EOF = Object.create(rootToken, {
	type: {
		value: 'EOF'
	}
});

/**
 * Creates a grammar for later parsing
 * @param {Object} options
 * @return {Object} parser
 */
function createGrammar(options) {
	const maxOperatorLengthForFirstChar = {};
	const registeredTokens = {};

	const operatorTypes = {
		"prefix": {
			nud(grammar) {
				return this.combine(grammar.expression(this.precedence));
			}
		},
		"infix": {
			led(grammar, left) {
				return this.combine(left, grammar.expression(this.precedence));
			}
		},
		"infixr": {
			led(grammar, left) {
				return this.combine(left, grammar.expression(this.precedence - 1));
			}
		}
	};

	function registerOperator(id, options, operatorType) {
		const props = {
			type: {
				value: 'operator'
			},
			value: {
				value: id
			},
			led: {
				value: operatorType.led,
				writable: true
			}
		};

		const op = Object.create(rootToken, props);
		Object.assign(op, options);

		registeredTokens[id] = op;
		return op;
	}

	for (const operatorTypeName in operatorTypes) {
		const ops = options[operatorTypeName];
		const operatorType = operatorTypes[operatorTypeName];

		for (const c in ops) {
			const firstChar = c[0];
			const maxLength = maxOperatorLengthForFirstChar[firstChar] || 0;

			if (maxLength < c.length) {
				maxOperatorLengthForFirstChar[firstChar] = c.length;
			}
			registerOperator(c, ops[c], operatorType);
		}
	}

	const tokenizer = function* (chunk) {
		let lineNumber = 1;

		function error(message, values) {
			message += "," + lineNumber;
			if (values) message += ': ' + JSON.stringify(values);
			throw new Error(message);
		}

		function makeIdentifier(value) {
			return Object.create(rootToken, {
				parseWithPrefix: {
					value: function (token) {
						console.log(`got identifier: ${token.value}`);
						return 0;
					}
				},
				type: {
					value: 'identifier'
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeOperator(value) {
			return Object.create(registeredTokens[value], {
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeNumber(value) {
			return Object.create(rootToken, {
				type: {
					value: 'number'
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeString(value) {
			return Object.create(rootToken, {
				type: {
					value: 'string'
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		function makeToken(type, value) {
			return Object.create(rootToken, {
				type: {
					value: type
				},
				value: {
					value: value
				},
				lineNumber: {
					value: lineNumber
				}
			});
		}

		let c, str;
		let firstCharInLine = 0;
		let i = 0;
		let op, operatorLength;
		const length = chunk.length;

		while (c = chunk[i]) {
			if (c <= ' ') {
				if (c === '\n') {
					lineNumber += 1;
					firstCharInLine = i;
				}
				i += 1;
				c = chunk[i];
			} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_') {
				str = c;
				i += 1;
				for (;;) {
					c = chunk[i];
					if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
						(c >= '0' && c <= '9') || c === '_') {
						str += c;
						i += 1;
					} else {
						break;
					}
				}

				yield makeIdentifier(str);
			} else if (c >= '0' && c <= '9') {
				str = c;
				i += 1;
				for (; i < length;) {
					c = chunk[i];
					if (c < '0' || c > '9') {
						break;
					}
					i += 1;
					str += c;
				}

				yield makeNumber(+str);
			} else if (c === '"') {
				i += 1;
				str = '';
				for (; i < length;) {
					c = chunk[i];
					if (c === '"') {
						i += 1;
						yield makeString(str);
						break;
					} else if (c === '\\') {
						i += 1;
						c = chunk[i];
						switch (c) {
							case 'b':
								c = '\b';
								break;
							case 'f':
								c = '\f';
								break;
							case 'n':
								c = '\n';
								break;
							case 'r':
								c = '\r';
								break;
							case 't':
								c = '\t';
								break;
							case 'u':
								c = parseInt(chunk.substr(i + 1, 4), 16);
								if (!isFinite(c) || c < 0) {
									error("Unterminated string", {
										value: c
									});
								}
								c = String.fromCharCode(c);
								i += 4;
								break;
						}
						str += c;
						i += 1;
					} else {
						str += c;
						i += 1;
					}
				}
				/*error("Unterminated string", {
					value: c
				});*/
			} else if (operatorLength = maxOperatorLengthForFirstChar[c]) {
				c = chunk.substring(i, i + operatorLength);
				if (registeredTokens[c]) {
					yield makeOperator(c);
					i += operatorLength;
				} else {
					operatorLength -= 1;
					c = chunk.substring(i, i + operatorLength);
					if (registeredTokens[c]) {
						yield makeOperator(c);
						i += operatorLength;
					} else {
						operatorLength -= 1;
						c = chunk.substring(i, i + operatorLength);
						if (registeredTokens[c]) {
							yield makeOperator(c);
							i += operatorLength;
						} else {
							error("Unknown operator", {
								value: c
							});
						}
					}
				}

			} else {
				i += 1;
				error("Unknown char", {
					value: c
				});
			}
		}
	}; // tokenizer

	const grammar = Object.create({
		tokenizer: tokenizer,

		/**
		 * Parses the input and delivers the outermoost expression.
		 * @param {String} chunk input text
		 * @return {Object} evaluated input
		 */
		parse(chunk) {
			const tokens = tokenizer(chunk);
			let token;

			this.advance = function advance(id) {
				if (id !== undefined && token.id !== undefined && token.id !== id) {
					console.log(`Got ${token.id} expected ${id}`);
				}

				const n = tokens.next();

				if (n.done) {
					token = EOF;
					return token;
				}
				token = n.value;
				return token;
			};

			token = this.advance();

			this.expression = function (precedence) {
				let t = token;
				grammar.advance();
				let left = t.nud(grammar);

				while (precedence < token.precedence) {
					t = token;
					grammar.advance();
					left = t.led(grammar, left);
				}

				return left;
			};

			return this.expression(token.precedence);
		}
	});

	return grammar;
}

exports.createGrammar = createGrammar;
<|endoftext|>


<commit_before>
/*
	-------------------------------- Grammar --------------------------------

	describe(string)                 add a description to a property group. Concatanative property.

	over([string])                   the parametres to bind random variables to.
	                                 Overriding property.

	over_(string_)                   the parametres to bind random variables to.
	                                 Overriding property.

	holdsWhen(Function, [Function])  holdsWhen a predicate is true, assert that several other
	                                     predicates are also true. Concatanative property.

	holdsWhen_(Function_)            holdsWhen_ a predicate is true, assert that several other
	                                     predicates are also true. Concatanative property.

	run(number)                          triggers the start of a test, specifies how long the test runs for.

*/

const executeTest = require('./run-test')
const fromStream  = require('./generator')
const colourise   = require('./colourise')

const restOf      = require('./flotsam').restOf

const describe = function (info) {

	const self  = joinTest(this, {
		info : info,
		type : 'describe'
	})

	return self
}

/*
Bind random variables to test over.

@param {[string]} strs. An array of strings.
*/

const over = function (strs) {

	const self  = joinTest(this, {
		params 	: strs,
		type   	: 'over'
	})

	return self
}

/*
	Variadic form of over.
*/

const over_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		params 	: args,
		type   	: 'over'
	})

	return self
}

/*
When a predicate is true of a test-case, ensure several other predicates are true too.

@param {Function}   pred:  a predicate function. This function tests if a test-case
    is suitable to be given to a set of properties.

@param {[Function]} preds: an array of predicate functions. These functions are
    properties that must hold true for an test-case that matches `pred`.

@return {object}
*/

const holdsWhen = function (pred, preds) {

	const self  = joinTest(this, {
		holdsWhenProperty : [pred].concat(preds),
		type              : 'holdsWhen'
	})

	return self
}

/*
	Variadic form of holdsWhen.
*/

const holdsWhen_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		holdsWhenProperty : args,
		type         : 'holdsWhen'
	})

	return self
}

const failsWhen = function (pred, preds) {

	const self  = joinTest(this, {
		failsWhenProperty : [pred].concat(preds),
		type              : 'failsWhen'
	})

	return self
}

const failsWhen_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		failsWhenProperty : args,
		type              : 'failsWhen'
	})

	return self
}

const worksWhen = function (pred, preds) {

	const self  = joinTest(this, {
		worksWhenProperty : [pred].concat(preds),
		type              : 'worksWhen'
	})

	return self
}

const worksWhen_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		worksWhenProperty : args,
		type              : 'worksWhen'
	})

	return self
}

/*
Run a test.

@param {number} num. The number of seconds testing should continue for.
*/

const run = function (num) {

	if (!num) {
		var num = 1
	}

	const self  = joinTest(this, {
		time : num,
		type : 'run'
	})

	return self
}

const _forallProto = {
	run         : run,

	over        : over,
	over_       : over_,

	holdsWhen   : holdsWhen,
	holdsWhen_  : holdsWhen_,

	failsWhen   : failsWhen,
	failsWhen_  : failsWhen_,

	worksWhen   : worksWhen,
	worksWhen_  : worksWhen_,

	describe    : describe
}

/*
	ensure that the test being passed to run
	can be executed properly.
*/
const validateTest = function (test) {

	// throw an error if any fields are missing.
	;['info', 'params', 'time'].forEach(function (key) {

		if (!test.hasOwnProperty(key)) {
			var message =
				"the property '" + key + "' was missing from the test object."

			throw colourise.red(message)
		}
	})

	// check each property.
	;['holdsWhenProperty', 'worksWhenProperty', 'failsWhenProperty']
	.filter(function (key) {
		const props = test[key]
		return props !== undefined
	})
	.forEach(function (key) {

		const props = test[key]

		if (toString.call(props) !== '[object Array]') {
			var message =
				'the property group "' + key + '" must be an array.'

			throw message
		}

		props.forEach(function (propGroup) {

			const pred = propGroup[0]

			if (
				toString.call(pred) !== '[object Function]' &&
				toString.call(pred) !== '[object Array]') {

				var message =
					'the predicate of the property-group "' + key +
					"' must be a single function, or an array of functions."

				throw message
			}

			restOf(propGroup).forEach(function (prop) {

				if (toString.call(prop) !== '[object Function]') {

					var message =
						'the property of the property-group "' + key +
						"' must be a function."

					throw message
				}
			})

		})

	})

	test.info.map(function (str) {

		if (toString.call(str) !== '[object String]') {
			var message =
				'the parametres of describe( ) must be a string.'

			throw message
		}

	})

	// time definitely must be a number.
	if (toString.call(test.time)!== '[object Number]') {

		var message =
			'the time-parametre passed to run( ) must be a number.'

		throw colourise.red(message)
	}

	// time mustn't be NaN
	if (test.time !== test.time) {

		var message =
			'the time-parametre passed to run( ) must not be NaN.'

		throw colourise.red(message)
	}

	// time definitely mustn't be Infinite
	if (!isFinite(test.time)) {

		var message =
			'the time-parametre passed to run( ) must not be Infinite.'

		throw colourise.red(message)
	}

	// time must be positive
	if (test.time < 0) {

		var message =
			'the time-parametre passed to run( ) must not be a positive number.'

		throw colourise.red(message)
	}

	return test
}

/*
	JoinTest

	JoinTest takes the 'this' passed to a grammar method, and some
	new data as its right argument. It then updates i
*/

const joinTest = ( function () {

	/*
		Test if an object is the global 'this' object.
	*/

	const isGlobalThis = function (obj) {
		return obj.GLOBAL || obj.window
	}

	return function (acc, right) {

		if (isGlobalThis(acc)) {
			var acc = Object.create(_forallProto)
		}

		const override = function (key) {
			return function () {

				acc[key] = right[key]
				return acc

			}
		}

		const join = function (key) {
			return function () {

				if (acc[key]) {
					acc[key] = acc[key].concat( [right[key]] )
				} else {
					acc[key] = [right[key]]
				}

				return acc
			}
		}

		const responses = {
			describe  : join('info'),
			over      : override('params'),
			holdsWhen : join('holdsWhenProperty'),
			failsWhen : join('failsWhenProperty'),
			worksWhen : join('worksWhenProperty'),
			run       : function () {

				acc.time = right.time

				executeTest(validateTest(acc))
			}
		}

		for (type in responses) {

			if (right.type === type) {
				return responses[type]()
			}
		}

	}

} )()

module.exports = {
	describe   : describe,

	over       : over,
	over_      : over_,

	holdsWhen  : holdsWhen,
	holdsWhen_ : holdsWhen_,

	run        : run
}
<commit_msg>Updated grammar<commit_after>
/*
	-------------------------------- Grammar --------------------------------

	describe(string)                 add a description to a property group. Concatanative property.

	over([string])                   the parametres to bind random variables to.
	                                 Overriding property.

	over_(string_)                   the parametres to bind random variables to.
	                                 Overriding property.

	holdsWhen(Function, [Function])  holdsWhen a predicate is true, assert that several other
	                                     predicates are also true. Concatanative property.

	holdsWhen_(Function_)            holdsWhen_ a predicate is true, assert that several other
	                                     predicates are also true. Concatanative property.

	run(number)                          triggers the start of a test, specifies how long the test runs for.

*/

const executeTest = require('./run-test')
const fromStream  = require('./generator')
const colourise   = require('./colourise')

const restOf      = require('./flotsam').restOf

const describe = function (info) {

	const self  = joinTest(this, {
		info : info,
		type : 'describe'
	})

	return self
}

/*
Bind random variables to test over.

@param {[string]} strs. An array of strings.
*/

const over = function (strs) {

	const self  = joinTest(this, {
		params 	: strs,
		type   	: 'over'
	})

	return self
}

/*
	Variadic form of over.
*/

const over_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		params 	: args,
		type   	: 'over'
	})

	return self
}

/*
When a predicate is true of a test-case, ensure several other predicates are true too.

@param {Function}   pred:  a predicate function. This function tests if a test-case
    is suitable to be given to a set of properties.

@param {[Function]} preds: an array of predicate functions. These functions are
    properties that must hold true for an test-case that matches `pred`.

@return {object}
*/

const holdsWhen = function (pred, preds) {

	const self  = joinTest(this, {
		holdsWhenProperty : [pred].concat(preds),
		type              : 'holdsWhen'
	})

	return self
}

/*
	Variadic form of holdsWhen.
*/

const holdsWhen_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		holdsWhenProperty : args,
		type         : 'holdsWhen'
	})

	return self
}

const failsWhen = function (pred, preds) {

	const self  = joinTest(this, {
		failsWhenProperty : [pred].concat(preds),
		type              : 'failsWhen'
	})

	return self
}

const failsWhen_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		failsWhenProperty : args,
		type              : 'failsWhen'
	})

	return self
}

const worksWhen = function (pred, preds) {

	const self  = joinTest(this, {
		worksWhenProperty : [pred].concat(preds),
		type              : 'worksWhen'
	})

	return self
}

const worksWhen_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		worksWhenProperty : args,
		type              : 'worksWhen'
	})

	return self
}

const holdsFor = function (preds) {

	const self  = joinTest(this, {
		worksWhenProperty : [function () {return true}].concat(preds),
		type              : 'worksWhen'
	})

	return self

}

const holdsFor_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		worksWhenProperty : [function () {return true}].concat(args),
		type              : 'worksWhen'
	})

	return self
}

const failsFor = function (preds) {

	const self  = joinTest(this, {
		failsWhenProperty : [function () {return true}].concat(preds),
		type              : 'failsWhen'
	})

	return self
}

const failsFor_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		failsWhenProperty : [function () {return true}].concat(args),
		type              : 'failsWhen'
	})

	return self
}

const worksFor = function (preds) {

	const self  = joinTest(this, {
		worksWhenProperty : [function () {return true}].concat(preds),
		type              : 'worksWhen'
	})

	return self
}

const worksFor_ = function () {

	const args = Array.prototype.slice.call(arguments)

	const self  = joinTest(this, {
		worksWhenProperty : [function () {return true}].concat(args),
		type              : 'failsWhen'
	})

	return self
}

/*
Run a test.

@param {number} num. The number of seconds testing should continue for.
*/

const run = function (num) {

	if (!num) {
		var num = 1
	}

	const self  = joinTest(this, {
		time : num,
		type : 'run'
	})

	return self
}

const _forallProto = {
	run         : run,

	over        : over,
	over_       : over_,

	holdsWhen   : holdsWhen,
	holdsWhen_  : holdsWhen_,

	failsWhen   : failsWhen,
	failsWhen_  : failsWhen_,

	worksWhen   : worksWhen,
	worksWhen_  : worksWhen_,

	describe    : describe
}

/*
	ensure that the test being passed to run
	can be executed properly.
*/
const validateTest = function (test) {

	// throw an error if any fields are missing.
	;['info', 'params', 'time'].forEach(function (key) {

		if (!test.hasOwnProperty(key)) {
			var message =
				"the property '" + key + "' was missing from the test object."

			throw colourise.red(message)
		}
	})

	// check each property.
	;['holdsWhenProperty', 'worksWhenProperty', 'failsWhenProperty']
	.filter(function (key) {
		const props = test[key]
		return props !== undefined
	})
	.forEach(function (key) {

		const props = test[key]

		if (toString.call(props) !== '[object Array]') {
			var message =
				'the property group "' + key + '" must be an array.'

			throw message
		}

		props.forEach(function (propGroup) {

			const pred = propGroup[0]

			if (
				toString.call(pred) !== '[object Function]' &&
				toString.call(pred) !== '[object Array]') {

				var message =
					'the predicate of the property-group "' + key +
					"' must be a single function, or an array of functions."

				throw message
			}

			restOf(propGroup).forEach(function (prop) {

				if (toString.call(prop) !== '[object Function]') {

					var message =
						'the property of the property-group "' + key +
						"' must be a function."

					throw message
				}
			})

		})

	})

	test.info.map(function (str) {

		if (toString.call(str) !== '[object String]') {
			var message =
				'the parametres of describe( ) must be a string.'

			throw message
		}

	})

	// time definitely must be a number.
	if (toString.call(test.time)!== '[object Number]') {

		var message =
			'the time-parametre passed to run( ) must be a number.'

		throw colourise.red(message)
	}

	// time mustn't be NaN
	if (test.time !== test.time) {

		var message =
			'the time-parametre passed to run( ) must not be NaN.'

		throw colourise.red(message)
	}

	// time definitely mustn't be Infinite
	if (!isFinite(test.time)) {

		var message =
			'the time-parametre passed to run( ) must not be Infinite.'

		throw colourise.red(message)
	}

	// time must be positive
	if (test.time < 0) {

		var message =
			'the time-parametre passed to run( ) must not be a positive number.'

		throw colourise.red(message)
	}

	return test
}

/*
	JoinTest

	JoinTest takes the 'this' passed to a grammar method, and some
	new data as its right argument. It then updates i
*/

const joinTest = ( function () {

	/*
		Test if an object is the global 'this' object.
	*/

	const isGlobalThis = function (obj) {
		return obj.GLOBAL || obj.window
	}

	return function (acc, right) {

		if (isGlobalThis(acc)) {
			var acc = Object.create(_forallProto)
		}

		const override = function (key) {
			return function () {

				acc[key] = right[key]
				return acc

			}
		}

		const join = function (key) {
			return function () {

				if (acc[key]) {
					acc[key] = acc[key].concat( [right[key]] )
				} else {
					acc[key] = [right[key]]
				}

				return acc
			}
		}

		const responses = {
			describe  : join('info'),
			over      : override('params'),
			holdsWhen : join('holdsWhenProperty'),
			failsWhen : join('failsWhenProperty'),
			worksWhen : join('worksWhenProperty'),
			run       : function () {

				acc.time = right.time

				executeTest(validateTest(acc))
			}
		}

		for (type in responses) {

			if (right.type === type) {
				return responses[type]()
			}
		}

	}

} )()

module.exports = {
	describe   : describe,

	over       : over,
	over_      : over_,

	holdsWhen  : holdsWhen,
	holdsWhen_ : holdsWhen_,

	run        : run
}
<|endoftext|>


<commit_before>/*
 * Copyright 2016 resin.io
 *
 * 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.
 */

/**
 * @module Etcher
 */

'use strict';

/* eslint-disable no-var */

var angular = require('angular');

/* eslint-enable no-var */

const _ = require('lodash');
const Store = require('./models/store');

const app = angular.module('Etcher', [
  require('angular-ui-router'),
  require('angular-ui-bootstrap'),
  require('angular-if-state'),

  // Etcher modules
  require('./modules/analytics'),
  require('./modules/error'),
  require('./modules/drive-scanner'),

  // Models
  require('./models/selection-state'),
  require('./models/flash-state'),
  require('./models/drives'),

  // Components
  require('./components/svg-icon/svg-icon'),
  require('./components/update-notifier/update-notifier'),
  require('./components/drive-selector/drive-selector'),

  // Pages
  require('./pages/main/main'),
  require('./pages/finish/finish'),
  require('./pages/settings/settings'),

  // OS
  require('./os/window-progress/window-progress'),
  require('./os/open-external/open-external'),
  require('./os/dropzone/dropzone'),

  // Utils
  require('./utils/manifest-bind/manifest-bind')
]);

app.run(() => {
  console.log([
    ' _____ _       _',
    '|  ___| |     | |',
    '| |__ | |_ ___| |__   ___ _ __',
    '|  __|| __/ __| \'_ \\ / _ \\ \'__|',
    '| |___| || (__| | | |  __/ |',
    '\\____/ \\__\\___|_| |_|\\___|_|',
    '',
    'Interested in joining the Etcher team?',
    'Drop us a line at jobs@resin.io'
  ].join('\n'));
});

app.run((AnalyticsService, UpdateNotifierService, SelectionStateModel) => {
  AnalyticsService.logEvent('Application start');

  if (UpdateNotifierService.shouldCheckForUpdates() && !process.env.ETCHER_DISABLE_UPDATES) {
    AnalyticsService.logEvent('Checking for updates');

    UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {

      // In case the internet connection is not good and checking the
      // latest published version takes too long, only show notify
      // the user about the new version if he didn't start the flash
      // process (e.g: selected an image), otherwise such interruption
      // might be annoying.
      if (!isLatestVersion && !SelectionStateModel.hasImage()) {

        AnalyticsService.logEvent('Notifying update');
        UpdateNotifierService.notify();
      }
    });
  }

});

app.run((AnalyticsService, OSWindowProgressService, FlashStateModel) => {
  Store.subscribe(() => {
    const flashState = FlashStateModel.getFlashState();

    // There is usually a short time period between the `isFlashing()`
    // property being set, and the flashing actually starting, which
    // might cause some non-sense flashing state logs including
    // `undefined` values.
    //
    // We use the presence of `.eta` to determine that the actual
    // writing started.
    if (!FlashStateModel.isFlashing() || !flashState.eta) {
      return;
    }

    AnalyticsService.logDebug([
      `Progress (${flashState.type}):`,
      `${flashState.percentage}% at ${flashState.speed} MB/s`,
      `(eta ${flashState.eta}s)`
    ].join(' '));

    OSWindowProgressService.set(flashState.percentage);
  });
});

app.run(($timeout, DriveScannerService, DrivesModel, ErrorService, DriveSelectorService) => {
  DriveScannerService.on('drives', (drives) => {

    // Safely trigger a digest cycle.
    // In some cases, AngularJS doesn't aknowledge that the
    // available drives list has changed, and incorrectly
    // keeps asking the user to "Connect a drive".
    $timeout(() => {
      DrivesModel.setDrives(drives);
    });

    if (_.isEmpty(drives)) {
      DriveSelectorService.close();
    }
  });

  DriveScannerService.on('error', ErrorService.reportException);
  DriveScannerService.start();
});

app.config(($urlRouterProvider) => {
  $urlRouterProvider.otherwise('/main');
});

app.config(($provide) => {
  $provide.decorator('$exceptionHandler', ($delegate, $injector) => {
    return (exception, cause) => {
      const ErrorService = $injector.get('ErrorService');
      ErrorService.reportException(exception);
      $delegate(exception, cause);
    };
  });
});

app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalService) {

  /**
   * @summary Open help page
   * @function
   * @public
   *
   * @description
   * This application will open either the image's support url, declared
   * in the archive `manifest.json`, or the default Etcher help page.
   *
   * @example
   * HeaderController.openHelpPage();
   */
  this.openHelpPage = () => {
    const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md';
    const supportUrl = SelectionStateModel.getImageSupportUrl() || DEFAULT_SUPPORT_URL;
    OSOpenExternalService.open(supportUrl);
  };

});
<commit_msg>chore: update "join us" email to join+etcher@resin.io (#873)<commit_after>/*
 * Copyright 2016 resin.io
 *
 * 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.
 */

/**
 * @module Etcher
 */

'use strict';

/* eslint-disable no-var */

var angular = require('angular');

/* eslint-enable no-var */

const _ = require('lodash');
const Store = require('./models/store');

const app = angular.module('Etcher', [
  require('angular-ui-router'),
  require('angular-ui-bootstrap'),
  require('angular-if-state'),

  // Etcher modules
  require('./modules/analytics'),
  require('./modules/error'),
  require('./modules/drive-scanner'),

  // Models
  require('./models/selection-state'),
  require('./models/flash-state'),
  require('./models/drives'),

  // Components
  require('./components/svg-icon/svg-icon'),
  require('./components/update-notifier/update-notifier'),
  require('./components/drive-selector/drive-selector'),

  // Pages
  require('./pages/main/main'),
  require('./pages/finish/finish'),
  require('./pages/settings/settings'),

  // OS
  require('./os/window-progress/window-progress'),
  require('./os/open-external/open-external'),
  require('./os/dropzone/dropzone'),

  // Utils
  require('./utils/manifest-bind/manifest-bind')
]);

app.run(() => {
  console.log([
    ' _____ _       _',
    '|  ___| |     | |',
    '| |__ | |_ ___| |__   ___ _ __',
    '|  __|| __/ __| \'_ \\ / _ \\ \'__|',
    '| |___| || (__| | | |  __/ |',
    '\\____/ \\__\\___|_| |_|\\___|_|',
    '',
    'Interested in joining the Etcher team?',
    'Drop us a line at join+etcher@resin.io'
  ].join('\n'));
});

app.run((AnalyticsService, UpdateNotifierService, SelectionStateModel) => {
  AnalyticsService.logEvent('Application start');

  if (UpdateNotifierService.shouldCheckForUpdates() && !process.env.ETCHER_DISABLE_UPDATES) {
    AnalyticsService.logEvent('Checking for updates');

    UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {

      // In case the internet connection is not good and checking the
      // latest published version takes too long, only show notify
      // the user about the new version if he didn't start the flash
      // process (e.g: selected an image), otherwise such interruption
      // might be annoying.
      if (!isLatestVersion && !SelectionStateModel.hasImage()) {

        AnalyticsService.logEvent('Notifying update');
        UpdateNotifierService.notify();
      }
    });
  }

});

app.run((AnalyticsService, OSWindowProgressService, FlashStateModel) => {
  Store.subscribe(() => {
    const flashState = FlashStateModel.getFlashState();

    // There is usually a short time period between the `isFlashing()`
    // property being set, and the flashing actually starting, which
    // might cause some non-sense flashing state logs including
    // `undefined` values.
    //
    // We use the presence of `.eta` to determine that the actual
    // writing started.
    if (!FlashStateModel.isFlashing() || !flashState.eta) {
      return;
    }

    AnalyticsService.logDebug([
      `Progress (${flashState.type}):`,
      `${flashState.percentage}% at ${flashState.speed} MB/s`,
      `(eta ${flashState.eta}s)`
    ].join(' '));

    OSWindowProgressService.set(flashState.percentage);
  });
});

app.run(($timeout, DriveScannerService, DrivesModel, ErrorService, DriveSelectorService) => {
  DriveScannerService.on('drives', (drives) => {

    // Safely trigger a digest cycle.
    // In some cases, AngularJS doesn't aknowledge that the
    // available drives list has changed, and incorrectly
    // keeps asking the user to "Connect a drive".
    $timeout(() => {
      DrivesModel.setDrives(drives);
    });

    if (_.isEmpty(drives)) {
      DriveSelectorService.close();
    }
  });

  DriveScannerService.on('error', ErrorService.reportException);
  DriveScannerService.start();
});

app.config(($urlRouterProvider) => {
  $urlRouterProvider.otherwise('/main');
});

app.config(($provide) => {
  $provide.decorator('$exceptionHandler', ($delegate, $injector) => {
    return (exception, cause) => {
      const ErrorService = $injector.get('ErrorService');
      ErrorService.reportException(exception);
      $delegate(exception, cause);
    };
  });
});

app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalService) {

  /**
   * @summary Open help page
   * @function
   * @public
   *
   * @description
   * This application will open either the image's support url, declared
   * in the archive `manifest.json`, or the default Etcher help page.
   *
   * @example
   * HeaderController.openHelpPage();
   */
  this.openHelpPage = () => {
    const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md';
    const supportUrl = SelectionStateModel.getImageSupportUrl() || DEFAULT_SUPPORT_URL;
    OSOpenExternalService.open(supportUrl);
  };

});
<|endoftext|>


<commit_before>/**
 * MISSKEY BOOT LOADER
 * (ENTRY POINT)
 */

/**
 * ドメインに基づいて適切なスクリプトを読み込みます。
 * ユーザーの言語およびモバイル端末か否かも考慮します。
 * webpackは介さないためrequireやimportは使えません。
 */

'use strict';

// Get the current url information
const Url = new URL(location.href);

// Extarct the (sub) domain part of the current url
//
// e.g.
//   misskey.alice               => misskey
//   misskey.strawberry.pasta    => misskey
//   dev.misskey.alice.tachibana => dev
let app = Url.host.split('.')[0];

// Detect the user language
// Note: The default language is English
let lang = navigator.language.split('-')[0];
if (!/^(en|ja)$/.test(lang)) lang = 'en';

// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
const isMobile = /mobile|iphone|ipad|android/.test(ua);

// Get the <head> element
const head = document.getElementsByTagName('head')[0];

// If mobile, insert the viewport meta tag
if (isMobile) {
	const meta = document.createElement('meta');
	meta.setAttribute('name', 'viewport');
	meta.setAttribute('content', [
		['width', 'device-width'],
		['initial-scale', '1'],
		['minimum-scale', '1'],
		['maximum-scale', '1'],
		['user-scalable', 'no']
	].map(x => x.join('=')).join(','));
	head.appendChild(meta);
}

// Switch desktop or mobile version
if (app == 'misskey') {
	app = isMobile ? 'mobile' : 'desktop';
}

// Load an app script
// Note: 'async' makes can load the script asyncly.
//       'defer' makes can run the script when the dom loaded.
const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
<commit_msg>Better English<commit_after>/**
 * MISSKEY BOOT LOADER
 * (ENTRY POINT)
 */

/**
 * ドメインに基づいて適切なスクリプトを読み込みます。
 * ユーザーの言語およびモバイル端末か否かも考慮します。
 * webpackは介さないためrequireやimportは使えません。
 */

'use strict';

// Get the current url information
const Url = new URL(location.href);

// Extarct the (sub) domain part of the current url
//
// e.g.
//   misskey.alice               => misskey
//   misskey.strawberry.pasta    => misskey
//   dev.misskey.alice.tachibana => dev
let app = Url.host.split('.')[0];

// Detect the user language
// Note: The default language is English
let lang = navigator.language.split('-')[0];
if (!/^(en|ja)$/.test(lang)) lang = 'en';

// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
const isMobile = /mobile|iphone|ipad|android/.test(ua);

// Get the <head> element
const head = document.getElementsByTagName('head')[0];

// If mobile, insert the viewport meta tag
if (isMobile) {
	const meta = document.createElement('meta');
	meta.setAttribute('name', 'viewport');
	meta.setAttribute('content', [
		['width', 'device-width'],
		['initial-scale', '1'],
		['minimum-scale', '1'],
		['maximum-scale', '1'],
		['user-scalable', 'no']
	].map(x => x.join('=')).join(','));
	head.appendChild(meta);
}

// Switch desktop or mobile version
if (app == 'misskey') {
	app = isMobile ? 'mobile' : 'desktop';
}

// Load an app script
// Note: 'async' make it possible to load the script asyncly.
//       'defer' make it possible to run the script when the dom loaded.
const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
<|endoftext|>


<commit_before>/**
 * MISSKEY BOOT LOADER
 * (ENTRY POINT)
 */

/**
 * ドメインに基づいて適切なスクリプトを読み込みます。
 * ユーザーの言語およびモバイル端末か否かも考慮します。
 */

'use strict';

// Get the current url information
const Url = new URL(location.href);

// Extarct the (sub) domain part of the current url
//
// e.g.
//   misskey.alice               => misskey
//   misskey.strawberry.pasta    => misskey
//   dev.misskey.alice.tachibana => dev
let app = Url.host.split('.')[0];

// Detect the user language
// Note: The default language is English
let lang = navigator.language.split('-')[0];
if (!/^(en|ja)$/.test(lang)) lang = 'en';

// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
const isMobile = /mobile|iphone|ipad|android/.test(ua);

// Get the <head> element
const [head] = document.getElementsByTagName('head');

// If mobile, insert the viewport meta tag
if (isMobile) {
	const meta = document.createElement('meta');
	meta.setAttribute('name', 'viewport');
	meta.setAttribute('content', [
		['width', 'device-width'],
		['initial-scale', '1'],
		['minimum-scale', '1'],
		['maximum-scale', '1'],
		['user-scalable', 'no']
	].map(x => x.join('=')).join(','));
	head.appendChild(meta);
}

// Switch desktop or mobile version
if (app == 'misskey') {
	app = isMobile ? 'mobile' : 'desktop';
}

// Load an app script
// Note: 'async' makes can load the script asyncly.
//       'defer' makes can run the script when the dom loaded.
const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
<commit_msg>Check if ES6 supported<commit_after>/**
 * MISSKEY BOOT LOADER
 * (ENTRY POINT)
 */

/**
 * ドメインに基づいて適切なスクリプトを読み込みます。
 * ユーザーの言語およびモバイル端末か否かも考慮します。
 * webpackは介さないためrequireやimportは使えません。
 */

'use strict';

// Detect an old browser
if (window.fetch == undefined) {
	alert(
		'お使いのブラウザが古いためMisskeyを動作させることができません。' +
		'バージョンを最新のものに更新するか、別のブラウザをお試しください。');
}

// Get the current url information
const Url = new URL(location.href);

// Extarct the (sub) domain part of the current url
//
// e.g.
//   misskey.alice               => misskey
//   misskey.strawberry.pasta    => misskey
//   dev.misskey.alice.tachibana => dev
let app = Url.host.split('.')[0];

// Detect the user language
// Note: The default language is English
let lang = navigator.language.split('-')[0];
if (!/^(en|ja)$/.test(lang)) lang = 'en';

// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
const isMobile = /mobile|iphone|ipad|android/.test(ua);

// Get the <head> element
const [head] = document.getElementsByTagName('head');

// If mobile, insert the viewport meta tag
if (isMobile) {
	const meta = document.createElement('meta');
	meta.setAttribute('name', 'viewport');
	meta.setAttribute('content', [
		['width', 'device-width'],
		['initial-scale', '1'],
		['minimum-scale', '1'],
		['maximum-scale', '1'],
		['user-scalable', 'no']
	].map(x => x.join('=')).join(','));
	head.appendChild(meta);
}

// Switch desktop or mobile version
if (app == 'misskey') {
	app = isMobile ? 'mobile' : 'desktop';
}

// Load an app script
// Note: 'async' makes can load the script asyncly.
//       'defer' makes can run the script when the dom loaded.
const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
<|endoftext|>


<commit_before>'use strict'

const inflection = require('inflection')
const deepEqual = require('deep-equal')

const settings = require('./settings')
const reservedKeys = settings.reservedKeys
const inBrackets = settings.inBrackets
const isField = settings.isField
const isFilter = settings.isFilter
const mediaType = settings.mediaType
const pageOffset = settings.pageOffset
const pageLimit = settings.pageLimit

module.exports = {
  initializeContext, mapRecord, mapId, matchId, castId, under, parseBuffer
}

function initializeContext (contextRequest, request, response) {
  const uriTemplate = this.uriTemplate
  const methodMap = this.methodMap
  const recordTypes = this.recordTypes
  const adapter = this.adapter
  const keys = this.keys
  const methods = this.methods

  const options = this.options
  const inflectType = options.inflectType
  const inflectKeys = options.inflectKeys
  const prefix = options.prefix
  const allowLevel = options.allowLevel

  const errors = this.errors
  const NotAcceptableError = errors.NotAcceptableError
  const NotFoundError = errors.NotFoundError

  // According to the spec, if the media type is provided in the Accept
  // header, it should be included at least once without any media type
  // parameters.
  if (request.headers['accept'] &&
    ~request.headers['accept'].indexOf(mediaType)) {
    const escapedMediaType = mediaType.replace(/[\+\.]/g, '\\$&')
    const mediaTypeRegex = new RegExp(`${escapedMediaType}(?!;)`, 'g')
    if (!request.headers['accept'].match(mediaTypeRegex))
      throw new NotAcceptableError('The "Accept" header should contain ' +
        'at least one instance of the JSON media type without any ' +
        'media type parameters.')
  }

  request.meta = {}

  const meta = contextRequest.meta

  const method = contextRequest.method = request.meta.method =
    methodMap[request.method]

  // Decode URI Component only for the query string.
  const uriObject = contextRequest.uriObject = request.meta.uriObject =
    uriTemplate.fromUri(request.url)

  if (!Object.keys(uriObject).length && request.url.length > 1)
    throw new NotFoundError('Invalid URI.')

  const type = contextRequest.type = request.meta.type =
    uriObject.type ? inflectType ?
      inflection.singularize(uriObject.type) : uriObject.type : null

  // Show allow options.
  if ((!type || type in recordTypes) && !method) {
    delete uriObject.query
    const output = {
      meta: {
        headers: {
          'Allow': allowLevel[Object.keys(uriObject)
            .filter(key => uriObject[key]).length].join(', ')
        }
      }
    }
    response.statusCode = 204
    throw output
  }

  // Show the index route.
  if (!type && method === methods.find) {
    const output = { payload: { [reservedKeys.links]: {} } }

    for (let type in recordTypes) {
      if (inflectType) type = inflection.pluralize(type)
      output.payload[reservedKeys.links][type] = prefix +
        uriTemplate.fillFromObject({ type })
    }

    response.statusCode = 200
    throw output
  }

  const ids = contextRequest.ids = request.meta.ids =
    uriObject.ids ? (Array.isArray(uriObject.ids) ?
      uriObject.ids : [ uriObject.ids ]).map(castId) : null

  const fields = recordTypes[type]

  attachQueries.call(this, contextRequest)
  request.meta.options = contextRequest.options

  let relatedField = uriObject.relatedField
  const relationship = uriObject.relationship

  if (relationship) {
    if (relatedField !== reservedKeys.relationships)
      throw new NotFoundError('Invalid relationship URI.')

    // This is a little unorthodox, but POST and DELETE requests to a
    // relationship entity should be treated as updates.
    if (method === methods.create || method === methods.delete) {
      contextRequest.originalMethod = method
      contextRequest.method = methods.update
    }

    relatedField = relationship
  }

  if (relatedField && inflectKeys)
    relatedField = inflection.camelize(under(relatedField), true)

  if (relatedField && (!(relatedField in fields) ||
    !(keys.link in fields[relatedField]) ||
    fields[relatedField][keys.denormalizedInverse]))
    throw new NotFoundError(`The field "${relatedField}" is ` +
      `not a link on the type "${type}".`)

  return relatedField ? adapter.find(type, ids, {
    // We only care about getting the related field.
    fields: { [relatedField]: true }
  }, meta)
  .then(records => {
    // Reduce the related IDs from all of the records into an array of
    // unique IDs.
    const relatedIds = Array.from((records || []).reduce((ids, record) => {
      const value = record[relatedField]

      if (Array.isArray(value)) for (let id of value) ids.add(id)
      else ids.add(value)

      return ids
    }, new Set()))

    const relatedType = fields[relatedField][keys.link]

    // Copy the original type and IDs to temporary keys.
    contextRequest.relatedField = request.meta.relatedField = relatedField
    contextRequest.relationship = request.meta.relationship = relationship
    contextRequest.originalType = request.meta.originalType = type
    contextRequest.originalIds = request.meta.originalIds = ids

    // Write the related info to the request, which should take
    // precedence over the original type and IDs.
    contextRequest.type = request.meta.type = relatedType
    contextRequest.ids = request.meta.ids = relatedIds

    return contextRequest
  }) : contextRequest
}

/**
 * Internal function to map a record to JSON API format. It must be
 * called directly within the context of the serializer. Within this
 * function, IDs must be cast to strings, per the spec.
 */
function mapRecord (type, record) {
  const keys = this.keys
  const uriTemplate = this.uriTemplate
  const recordTypes = this.recordTypes
  const fields = recordTypes[type]
  const options = this.options
  const prefix = options.prefix
  const inflectType = options.inflectType
  const inflectKeys = options.inflectKeys
  const clone = {}

  const id = record[keys.primary]

  clone[reservedKeys.type] = inflectType ? inflection.pluralize(type) : type
  clone[reservedKeys.id] = id.toString()
  clone[reservedKeys.meta] = {}
  clone[reservedKeys.attributes] = {}
  clone[reservedKeys.relationships] = {}
  clone[reservedKeys.links] = {
    [reservedKeys.self]: prefix + uriTemplate.fillFromObject({
      type: inflectType ? inflection.pluralize(type) : type,
      ids: id

    })
  }

  for (let field in record) {
    if (field === keys.primary) continue

    const fieldDefinition = fields[field]
    const originalField = field

    // Per the recommendation, dasherize keys.
    if (inflectKeys)
      field = inflection.transform(field,
        [ 'underscore', 'dasherize' ])

    // Handle meta/attributes.
    if (!fieldDefinition || fieldDefinition[keys.type]) {
      const value = record[originalField]

      if (!fieldDefinition) clone[reservedKeys.meta][field] = value
      else clone[reservedKeys.attributes][field] = value

      continue
    }

    // Handle link fields.
    const ids = record[originalField]

    const linkedType = inflectType ?
      inflection.pluralize(fieldDefinition[keys.link]) :
      fieldDefinition[keys.link]

    clone[reservedKeys.relationships][field] = {
      [reservedKeys.links]: {
        [reservedKeys.self]: prefix + uriTemplate.fillFromObject({
          type: inflectType ? inflection.pluralize(type) : type,
          ids: id,
          relatedField: reservedKeys.relationships,
          relationship: inflectKeys ? inflection.transform(field,
            [ 'underscore', 'dasherize' ]) : field
        }),
        [reservedKeys.related]: prefix + uriTemplate.fillFromObject({
          type: inflectType ? inflection.pluralize(type) : type,
          ids: id,
          relatedField: inflectKeys ? inflection.transform(field,
            [ 'underscore', 'dasherize' ]) : field
        })
      },
      [reservedKeys.primary]: fieldDefinition[keys.isArray] ?
        ids.map(toIdentifier.bind(null, linkedType)) :
        (ids ? toIdentifier(linkedType, ids) : null)
    }
  }

  if (!Object.keys(clone[reservedKeys.attributes]).length)
    delete clone[reservedKeys.attributes]

  if (!Object.keys(clone[reservedKeys.meta]).length)
    delete clone[reservedKeys.meta]

  if (!Object.keys(clone[reservedKeys.relationships]).length)
    delete clone[reservedKeys.relationships]

  return clone
}

function toIdentifier (type, id) {
  return {
    [reservedKeys.type]: type,
    [reservedKeys.id]: id.toString()
  }
}

function attachQueries (request) {
  const recordTypes = this.recordTypes
  const keys = this.keys
  const castValue = this.castValue
  const BadRequestError = this.errors.BadRequestError
  const options = this.options
  const includeLimit = options.includeLimit
  const maxLimit = options.maxLimit
  const type = request.type
  const fields = recordTypes[type]
  const reduceFields = (fields, field) => {
    fields[inflect(field)] = true
    return fields
  }
  const castMap = (type, options, x) => castValue(x, type, options)

  let query = request.uriObject.query
  if (!query) query = {}
  request.options = {}

  // Iterate over dynamic query strings.
  for (let parameter of Object.keys(query)) {
    // Attach fields option.
    if (parameter.match(isField)) {
      const sparseField = Array.isArray(query[parameter]) ?
        query[parameter] : query[parameter].split(',')
      const sparseType = (parameter.match(inBrackets) || [])[1]
      const fields = sparseField.reduce(reduceFields, {})

      if (sparseType === type)
        request.options.fields = fields
      else if (sparseType) {
        if (!(sparseType in request.includeOptions))
          request.includeOptions[sparseType] = {}

        request.includeOptions[sparseType].fields = fields
      }
    }

    // Attach match option.
    if (parameter.match(isFilter)) {
      if (!('match' in request.options)) request.options.match = {}
      const field = inflect((parameter.match(inBrackets) || [])[1])

      if (!(field in fields)) throw new BadRequestError(
        `The field "${field}" is non-existent.`)

      const fieldType = fields[field][keys.type]
      const value = Array.isArray(query[parameter]) ?
        query[parameter] : query[parameter].split(',')

      request.options.match[field] =
        value.map(castMap.bind(null, fieldType, options))
    }
  }

  // Attach include option.
  if (reservedKeys.include in query) {
    request.include = (Array.isArray(query[reservedKeys.include]) ?
      query[reservedKeys.include] :
      query[reservedKeys.include].split(','))
      .map(i => i.split('.').map(x => inflect(x)).slice(0, includeLimit))

    // Manually expand nested includes.
    for (let path of request.include)
      for (let i = path.length - 1; i > 0; i--) {
        const j = path.slice(0, i)
        if (!request.include.some(deepEqual.bind(null, j)))
          request.include.push(j)
      }
  }

  // Attach sort option.
  if (reservedKeys.sort in query)
    request.options.sort = (Array.isArray(query.sort) ?
      query.sort : query.sort.split(','))
      .reduce((sort, field) => {
        if (field.charAt(0) === '-') sort[inflect(field.slice(1))] = false
        else sort[inflect(field)] = true
        return sort
      }, {})

  // Attach offset option.
  if (pageOffset in query)
    request.options.offset = Math.abs(parseInt(query[pageOffset], 10))

  // Attach limit option.
  if (pageLimit in query)
    request.options.limit = Math.abs(parseInt(query[pageLimit], 10))

  // Check limit option.
  const limit = request.options.limit
  if (!limit || limit > maxLimit) request.options.limit = maxLimit

  function inflect(input) {
    return (options.inflectKeys) ? inflection.camelize(under(input),true) : input
  }
}

function mapId (relatedType, link) {
  const ConflictError = this.errors.ConflictError

  if (link[reservedKeys.type] !== relatedType)
    throw new ConflictError('Data object field ' +
      `"${reservedKeys.type}" is invalid, it must be ` +
      `"${relatedType}", not "${link[reservedKeys.type]}".`)

  return castId(link[reservedKeys.id])
}

function matchId (object, id) {
  return id === castId(object[reservedKeys.id])
}

function castId (id) {
  // Stolen from jQuery source code:
  // https://api.jquery.com/jQuery.isNumeric/
  const float = Number.parseFloat(id)
  return id - float + 1 >= 0 ? float : id
}

function under (s) {
  return s.replace(/-/g, '_')
}

function parseBuffer (payload) {
  const BadRequestError = this.errors.BadRequestError

  if (!Buffer.isBuffer(payload)) return payload

  try {
    return JSON.parse(payload.toString())
  }
  catch (error) {
    throw new BadRequestError(`Invalid JSON: ${error.message}`)
  }
}

<commit_msg>Syntax update to match linter<commit_after>'use strict'

const inflection = require('inflection')
const deepEqual = require('deep-equal')

const settings = require('./settings')
const reservedKeys = settings.reservedKeys
const inBrackets = settings.inBrackets
const isField = settings.isField
const isFilter = settings.isFilter
const mediaType = settings.mediaType
const pageOffset = settings.pageOffset
const pageLimit = settings.pageLimit

module.exports = {
  initializeContext, mapRecord, mapId, matchId, castId, under, parseBuffer
}

function initializeContext (contextRequest, request, response) {
  const uriTemplate = this.uriTemplate
  const methodMap = this.methodMap
  const recordTypes = this.recordTypes
  const adapter = this.adapter
  const keys = this.keys
  const methods = this.methods

  const options = this.options
  const inflectType = options.inflectType
  const inflectKeys = options.inflectKeys
  const prefix = options.prefix
  const allowLevel = options.allowLevel

  const errors = this.errors
  const NotAcceptableError = errors.NotAcceptableError
  const NotFoundError = errors.NotFoundError

  // According to the spec, if the media type is provided in the Accept
  // header, it should be included at least once without any media type
  // parameters.
  if (request.headers['accept'] &&
    ~request.headers['accept'].indexOf(mediaType)) {
    const escapedMediaType = mediaType.replace(/[\+\.]/g, '\\$&')
    const mediaTypeRegex = new RegExp(`${escapedMediaType}(?!;)`, 'g')
    if (!request.headers['accept'].match(mediaTypeRegex))
      throw new NotAcceptableError('The "Accept" header should contain ' +
        'at least one instance of the JSON media type without any ' +
        'media type parameters.')
  }

  request.meta = {}

  const meta = contextRequest.meta

  const method = contextRequest.method = request.meta.method =
    methodMap[request.method]

  // Decode URI Component only for the query string.
  const uriObject = contextRequest.uriObject = request.meta.uriObject =
    uriTemplate.fromUri(request.url)

  if (!Object.keys(uriObject).length && request.url.length > 1)
    throw new NotFoundError('Invalid URI.')

  const type = contextRequest.type = request.meta.type =
    uriObject.type ? inflectType ?
      inflection.singularize(uriObject.type) : uriObject.type : null

  // Show allow options.
  if ((!type || type in recordTypes) && !method) {
    delete uriObject.query
    const output = {
      meta: {
        headers: {
          'Allow': allowLevel[Object.keys(uriObject)
            .filter(key => uriObject[key]).length].join(', ')
        }
      }
    }
    response.statusCode = 204
    throw output
  }

  // Show the index route.
  if (!type && method === methods.find) {
    const output = { payload: { [reservedKeys.links]: {} } }

    for (let type in recordTypes) {
      if (inflectType) type = inflection.pluralize(type)
      output.payload[reservedKeys.links][type] = prefix +
        uriTemplate.fillFromObject({ type })
    }

    response.statusCode = 200
    throw output
  }

  const ids = contextRequest.ids = request.meta.ids =
    uriObject.ids ? (Array.isArray(uriObject.ids) ?
      uriObject.ids : [ uriObject.ids ]).map(castId) : null

  const fields = recordTypes[type]

  attachQueries.call(this, contextRequest)
  request.meta.options = contextRequest.options

  let relatedField = uriObject.relatedField
  const relationship = uriObject.relationship

  if (relationship) {
    if (relatedField !== reservedKeys.relationships)
      throw new NotFoundError('Invalid relationship URI.')

    // This is a little unorthodox, but POST and DELETE requests to a
    // relationship entity should be treated as updates.
    if (method === methods.create || method === methods.delete) {
      contextRequest.originalMethod = method
      contextRequest.method = methods.update
    }

    relatedField = relationship
  }

  if (relatedField && inflectKeys)
    relatedField = inflection.camelize(under(relatedField), true)

  if (relatedField && (!(relatedField in fields) ||
    !(keys.link in fields[relatedField]) ||
    fields[relatedField][keys.denormalizedInverse]))
    throw new NotFoundError(`The field "${relatedField}" is ` +
      `not a link on the type "${type}".`)

  return relatedField ? adapter.find(type, ids, {
    // We only care about getting the related field.
    fields: { [relatedField]: true }
  }, meta)
  .then(records => {
    // Reduce the related IDs from all of the records into an array of
    // unique IDs.
    const relatedIds = Array.from((records || []).reduce((ids, record) => {
      const value = record[relatedField]

      if (Array.isArray(value)) for (let id of value) ids.add(id)
      else ids.add(value)

      return ids
    }, new Set()))

    const relatedType = fields[relatedField][keys.link]

    // Copy the original type and IDs to temporary keys.
    contextRequest.relatedField = request.meta.relatedField = relatedField
    contextRequest.relationship = request.meta.relationship = relationship
    contextRequest.originalType = request.meta.originalType = type
    contextRequest.originalIds = request.meta.originalIds = ids

    // Write the related info to the request, which should take
    // precedence over the original type and IDs.
    contextRequest.type = request.meta.type = relatedType
    contextRequest.ids = request.meta.ids = relatedIds

    return contextRequest
  }) : contextRequest
}

/**
 * Internal function to map a record to JSON API format. It must be
 * called directly within the context of the serializer. Within this
 * function, IDs must be cast to strings, per the spec.
 */
function mapRecord (type, record) {
  const keys = this.keys
  const uriTemplate = this.uriTemplate
  const recordTypes = this.recordTypes
  const fields = recordTypes[type]
  const options = this.options
  const prefix = options.prefix
  const inflectType = options.inflectType
  const inflectKeys = options.inflectKeys
  const clone = {}

  const id = record[keys.primary]

  clone[reservedKeys.type] = inflectType ? inflection.pluralize(type) : type
  clone[reservedKeys.id] = id.toString()
  clone[reservedKeys.meta] = {}
  clone[reservedKeys.attributes] = {}
  clone[reservedKeys.relationships] = {}
  clone[reservedKeys.links] = {
    [reservedKeys.self]: prefix + uriTemplate.fillFromObject({
      type: inflectType ? inflection.pluralize(type) : type,
      ids: id

    })
  }

  for (let field in record) {
    if (field === keys.primary) continue

    const fieldDefinition = fields[field]
    const originalField = field

    // Per the recommendation, dasherize keys.
    if (inflectKeys)
      field = inflection.transform(field,
        [ 'underscore', 'dasherize' ])

    // Handle meta/attributes.
    if (!fieldDefinition || fieldDefinition[keys.type]) {
      const value = record[originalField]

      if (!fieldDefinition) clone[reservedKeys.meta][field] = value
      else clone[reservedKeys.attributes][field] = value

      continue
    }

    // Handle link fields.
    const ids = record[originalField]

    const linkedType = inflectType ?
      inflection.pluralize(fieldDefinition[keys.link]) :
      fieldDefinition[keys.link]

    clone[reservedKeys.relationships][field] = {
      [reservedKeys.links]: {
        [reservedKeys.self]: prefix + uriTemplate.fillFromObject({
          type: inflectType ? inflection.pluralize(type) : type,
          ids: id,
          relatedField: reservedKeys.relationships,
          relationship: inflectKeys ? inflection.transform(field,
            [ 'underscore', 'dasherize' ]) : field
        }),
        [reservedKeys.related]: prefix + uriTemplate.fillFromObject({
          type: inflectType ? inflection.pluralize(type) : type,
          ids: id,
          relatedField: inflectKeys ? inflection.transform(field,
            [ 'underscore', 'dasherize' ]) : field
        })
      },
      [reservedKeys.primary]: fieldDefinition[keys.isArray] ?
        ids.map(toIdentifier.bind(null, linkedType)) :
        (ids ? toIdentifier(linkedType, ids) : null)
    }
  }

  if (!Object.keys(clone[reservedKeys.attributes]).length)
    delete clone[reservedKeys.attributes]

  if (!Object.keys(clone[reservedKeys.meta]).length)
    delete clone[reservedKeys.meta]

  if (!Object.keys(clone[reservedKeys.relationships]).length)
    delete clone[reservedKeys.relationships]

  return clone
}

function toIdentifier (type, id) {
  return {
    [reservedKeys.type]: type,
    [reservedKeys.id]: id.toString()
  }
}

function attachQueries (request) {
  const recordTypes = this.recordTypes
  const keys = this.keys
  const castValue = this.castValue
  const BadRequestError = this.errors.BadRequestError
  const options = this.options
  const includeLimit = options.includeLimit
  const maxLimit = options.maxLimit
  const type = request.type
  const fields = recordTypes[type]
  const reduceFields = (fields, field) => {
    fields[inflect(field)] = true
    return fields
  }
  const castMap = (type, options, x) => castValue(x, type, options)

  let query = request.uriObject.query
  if (!query) query = {}
  request.options = {}

  // Iterate over dynamic query strings.
  for (let parameter of Object.keys(query)) {
    // Attach fields option.
    if (parameter.match(isField)) {
      const sparseField = Array.isArray(query[parameter]) ?
        query[parameter] : query[parameter].split(',')
      const sparseType = (parameter.match(inBrackets) || [])[1]
      const fields = sparseField.reduce(reduceFields, {})

      if (sparseType === type)
        request.options.fields = fields
      else if (sparseType) {
        if (!(sparseType in request.includeOptions))
          request.includeOptions[sparseType] = {}

        request.includeOptions[sparseType].fields = fields
      }
    }

    // Attach match option.
    if (parameter.match(isFilter)) {
      if (!('match' in request.options)) request.options.match = {}
      const field = inflect((parameter.match(inBrackets) || [])[1])

      if (!(field in fields)) throw new BadRequestError(
        `The field "${field}" is non-existent.`)

      const fieldType = fields[field][keys.type]
      const value = Array.isArray(query[parameter]) ?
        query[parameter] : query[parameter].split(',')

      request.options.match[field] =
        value.map(castMap.bind(null, fieldType, options))
    }
  }

  // Attach include option.
  if (reservedKeys.include in query) {
    request.include = (Array.isArray(query[reservedKeys.include]) ?
      query[reservedKeys.include] :
      query[reservedKeys.include].split(','))
      .map(i => i.split('.').map(x => inflect(x)).slice(0, includeLimit))

    // Manually expand nested includes.
    for (let path of request.include)
      for (let i = path.length - 1; i > 0; i--) {
        const j = path.slice(0, i)
        if (!request.include.some(deepEqual.bind(null, j)))
          request.include.push(j)
      }
  }

  // Attach sort option.
  if (reservedKeys.sort in query)
    request.options.sort = (Array.isArray(query.sort) ?
      query.sort : query.sort.split(','))
      .reduce((sort, field) => {
        if (field.charAt(0) === '-') sort[inflect(field.slice(1))] = false
        else sort[inflect(field)] = true
        return sort
      }, {})

  // Attach offset option.
  if (pageOffset in query)
    request.options.offset = Math.abs(parseInt(query[pageOffset], 10))

  // Attach limit option.
  if (pageLimit in query)
    request.options.limit = Math.abs(parseInt(query[pageLimit], 10))

  // Check limit option.
  const limit = request.options.limit
  if (!limit || limit > maxLimit) request.options.limit = maxLimit

  function inflect (input) {
    return (options.inflectKeys)
        ? inflection.camelize(under(input), true) : input
  }
}

function mapId (relatedType, link) {
  const ConflictError = this.errors.ConflictError

  if (link[reservedKeys.type] !== relatedType)
    throw new ConflictError('Data object field ' +
      `"${reservedKeys.type}" is invalid, it must be ` +
      `"${relatedType}", not "${link[reservedKeys.type]}".`)

  return castId(link[reservedKeys.id])
}

function matchId (object, id) {
  return id === castId(object[reservedKeys.id])
}

function castId (id) {
  // Stolen from jQuery source code:
  // https://api.jquery.com/jQuery.isNumeric/
  const float = Number.parseFloat(id)
  return id - float + 1 >= 0 ? float : id
}

function under (s) {
  return s.replace(/-/g, '_')
}

function parseBuffer (payload) {
  const BadRequestError = this.errors.BadRequestError

  if (!Buffer.isBuffer(payload)) return payload

  try {
    return JSON.parse(payload.toString())
  }
  catch (error) {
    throw new BadRequestError(`Invalid JSON: ${error.message}`)
  }
}

<|endoftext|>