adam-26/tag-messageformat

View on GitHub
src/compiler.js

Summary

Maintainability
C
7 hrs
Test Coverage
/*
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License.
See the accompanying LICENSE file for terms.
*/

/* jslint esnext: true */

import {existsIn} from './utils';
import {stringFormatFactory} from './compilerUtil';

export default Compiler;

function Compiler(locales, formats, pluralFn, opts) {
    this.locales  = locales;
    this.formats  = formats;
    this.pluralFn = pluralFn;
    this.requireOther = (opts && typeof opts.requireOther === 'boolean') ?
        opts.requireOther :
        true;
    this.stringFormatFactory = (opts && opts.stringFormatFactory) || stringFormatFactory;
}

Compiler.prototype.compile = function (ast) {
    this.pluralStack        = [];
    this.currentPlural      = null;
    this.pluralNumberFormat = null;
    this.tagNames = [];
    this.argIds = [];

    return this.compileMessage(ast);
};

Compiler.prototype.compileMessage = function (ast) {
    if (!(ast && ast.type === 'messageFormatPattern')) {
        throw new Error('Message AST is not of type: "messageFormatPattern"');
    }

    var elements = ast.elements,
        pattern  = [];

    var i, len, element;
    for (i = 0, len = elements.length; i < len; i += 1) {
        element = elements[i];

        switch (element.type) {
            case 'messageTextElement':
                pattern.push(this.compileMessageText(element));
                break;

            case 'argumentElement':
                pattern.push(this.compileArgument(element));
                break;

            case 'selfClosingTagElement':
                pattern.push(this.compileSelfClosingTag(element));
                break;

            case 'tagElement':
                pattern.push(this.compileTag(element));
                break;

            default:
                throw new Error('Message element does not have a valid type');
        }
    }

    return pattern;
};

Compiler.prototype.compileMessageText = function (element) {
    // When this `element` is part of plural sub-pattern and its value contains
    // an unescaped '#', use a `PluralOffsetString` helper to properly output
    // the number with the correct offset in the string.
    if (this.currentPlural && /(^|[^\\])#/g.test(element.value)) {
        // Create a cache a NumberFormat instance that can be reused for any
        // PluralOffsetString instance in this message.
        if (!this.pluralNumberFormat) {
            this.pluralNumberFormat = new Intl.NumberFormat(this.locales);
        }

        return new PluralOffsetString(
                this.currentPlural.id,
                this.currentPlural.format.offset,
                this.pluralNumberFormat,
                element.value);
    }

    // Unescape the escaped '#'s in the message text.
    return element.value.replace(/\\#/g, '#');
};

Compiler.prototype.compileArgument = function (element) {
    if (existsIn(this.tagNames, element.id)) {
        throw new Error('Message has conflicting argument and tag name "' + element.id + '".');
    }

    this.argIds.push(element.id);
    var format = element.format;

    if (!format) {
        return this.stringFormatFactory(element.id);
    }

    var formats  = this.formats,
        locales  = this.locales,
        pluralFn = this.pluralFn,
        options;

    switch (format.type) {
        case 'numberFormat':
            options = formats.number[format.style];
            return {
                id    : element.id,
                format: new Intl.NumberFormat(locales, options).format
            };

        case 'dateFormat':
            options = formats.date[format.style];
            return {
                id    : element.id,
                format: new Intl.DateTimeFormat(locales, options).format
            };

        case 'timeFormat':
            options = formats.time[format.style];
            return {
                id    : element.id,
                format: new Intl.DateTimeFormat(locales, options).format
            };

        case 'pluralFormat':
            options = this.compileOptions(element);
            return new PluralFormat(
                element.id, format.ordinal, format.offset, options, pluralFn
            );

        case 'selectFormat':
            options = this.compileOptions(element);
            return new SelectFormat(element.id, options);

        default:
            throw new Error('Message element does not have a valid format type');
    }
};

Compiler.prototype.compileOptions = function (element) {
    var format      = element.format,
        options     = format.options,
        optionsHash = {};

    if (this.requireOther === true && !element.hasOther) {
        throw new Error(format.type + ' requires an `other` option, this can be disabled.');
    }

    // Save the current plural element, if any, then set it to a new value when
    // compiling the options sub-patterns. This conforms the spec's algorithm
    // for handling `"#"` syntax in message text.
    this.pluralStack.push(this.currentPlural);
    this.currentPlural = format.type === 'pluralFormat' ? element : null;

    var i, len, option;

    for (i = 0, len = options.length; i < len; i += 1) {
        option = options[i];

        // Compile the sub-pattern and save it under the options's selector.
        optionsHash[option.selector] = this.compileMessage(option.value);
    }

    // Pop the plural stack to put back the original current plural value.
    this.currentPlural = this.pluralStack.pop();

    return optionsHash;
};

Compiler.prototype.createTag = function (element, pattern) {
    if (existsIn(this.argIds, element.name)) {
        throw new Error('Message has conflicting argument and tag name "' + element.name + '".');
    }

    this.tagNames.push(element.name);
    return new TagFormat(element.name, pattern);
};

Compiler.prototype.compileTag = function (element) {
    return this.createTag(element, this.compileMessage(element.value));
};

Compiler.prototype.compileSelfClosingTag = function (element) {
    return this.createTag(element);
};

// -- Compiler Helper Classes --------------------------------------------------

function PluralFormat(id, useOrdinal, offset, options, pluralFn) {
    this.id         = id;
    this.useOrdinal = useOrdinal;
    this.offset     = offset;
    this.options    = options;
    this.pluralFn   = pluralFn;
}

PluralFormat.prototype.getOption = function (value) {
    var options = this.options;

    var option = options['=' + value] ||
            options[this.pluralFn(value - this.offset, this.useOrdinal)];

    return option || options.other || [];
};

function PluralOffsetString(id, offset, numberFormat, string) {
    this.id           = id;
    this.offset       = offset;
    this.numberFormat = numberFormat;
    this.string       = string;
}

PluralOffsetString.prototype.format = function (value) {
    var number = this.numberFormat.format(value - this.offset);

    return this.string
            .replace(/(^|[^\\])#/g, '$1' + number)
            .replace(/\\#/g, '#');
};

function SelectFormat(id, options) {
    this.id      = id;
    this.options = options;
}

SelectFormat.prototype.getOption = function (value) {
    var options = this.options;
    return options[value] || options.other || [];
};

function TagFormat(id, pattern) {
  this.id = id;
  this.pattern = pattern;
}

TagFormat.prototype.format = function(value, content) {
  if (typeof value !== 'function') {
      throw new Error('tag values require a function');
  }

  if (typeof this.pattern === 'undefined') {
      return value(); // self-closing tag
  }

  return value(content);
};