var Tannin = (function () { 'use strict'; var PRECEDENCE, OPENERS, TERMINATORS, PATTERN; /** * Operator precedence mapping. * * @type {Object} */ PRECEDENCE = { '(': 9, '!': 8, '*': 7, '/': 7, '%': 7, '+': 6, '-': 6, '<': 5, '<=': 5, '>': 5, '>=': 5, '==': 4, '!=': 4, '&&': 3, '||': 2, '?': 1, '?:': 1, }; /** * Characters which signal pair opening, to be terminated by terminators. * * @type {string[]} */ OPENERS = [ '(', '?' ]; /** * Characters which signal pair termination, the value an array with the * opener as its first member. The second member is an optional operator * replacement to push to the stack. * * @type {string[]} */ TERMINATORS = { ')': [ '(' ], ':': [ '?', '?:' ], }; /** * Pattern matching operators and openers. * * @type {RegExp} */ PATTERN = /<=|>=|==|!=|&&|\|\||\?:|\(|!|\*|\/|%|\+|-|<|>|\?|\)|:/; /** * Given a C expression, returns the equivalent postfix (Reverse Polish) * notation terms as an array. * * If a postfix string is desired, simply `.join( ' ' )` the result. * * @example * * ```js * import postfix from '@tannin/postfix'; * * postfix( 'n > 1' ); * // ⇒ [ 'n', '1', '>' ] * ``` * * @param {string} expression C expression. * * @return {string[]} Postfix terms. */ function postfix( expression ) { var terms = [], stack = [], match, operator, term, element; while ( ( match = expression.match( PATTERN ) ) ) { operator = match[ 0 ]; // Term is the string preceding the operator match. It may contain // whitespace, and may be empty (if operator is at beginning). term = expression.substr( 0, match.index ).trim(); if ( term ) { terms.push( term ); } while ( ( element = stack.pop() ) ) { if ( TERMINATORS[ operator ] ) { if ( TERMINATORS[ operator ][ 0 ] === element ) { // Substitution works here under assumption that because // the assigned operator will no longer be a terminator, it // will be pushed to the stack during the condition below. operator = TERMINATORS[ operator ][ 1 ] || operator; break; } } else if ( OPENERS.indexOf( element ) >= 0 || PRECEDENCE[ element ] < PRECEDENCE[ operator ] ) { // Push to stack if either an opener or when pop reveals an // element of lower precedence. stack.push( element ); break; } // For each popped from stack, push to terms. terms.push( element ); } if ( ! TERMINATORS[ operator ] ) { stack.push( operator ); } // Slice matched fragment from expression to continue match. expression = expression.substr( match.index + operator.length ); } // Push remainder of operand, if exists, to terms. expression = expression.trim(); if ( expression ) { terms.push( expression ); } // Pop remaining items from stack into terms. return terms.concat( stack.reverse() ); } /** * Operator callback functions. * * @type {Object} */ var OPERATORS = { '!': function( a ) { return ! a; }, '*': function( a, b ) { return a * b; }, '/': function( a, b ) { return a / b; }, '%': function( a, b ) { return a % b; }, '+': function( a, b ) { return a + b; }, '-': function( a, b ) { return a - b; }, '<': function( a, b ) { return a < b; }, '<=': function( a, b ) { return a <= b; }, '>': function( a, b ) { return a > b; }, '>=': function( a, b ) { return a >= b; }, '==': function( a, b ) { return a === b; }, '!=': function( a, b ) { return a !== b; }, '&&': function( a, b ) { return a && b; }, '||': function( a, b ) { return a || b; }, '?:': function( a, b, c ) { if ( a ) { throw b; } return c; }, }; /** * Given an array of postfix terms and operand variables, returns the result of * the postfix evaluation. * * @example * * ```js * import evaluate from '@tannin/evaluate'; * * // 3 + 4 * 5 / 6 ⇒ '3 4 5 * 6 / +' * const terms = [ '3', '4', '5', '*', '6', '/', '+' ]; * * evaluate( terms, {} ); * // ⇒ 6.333333333333334 * ``` * * @param {string[]} postfix Postfix terms. * @param {Object} variables Operand variables. * * @return {*} Result of evaluation. */ function evaluate( postfix, variables ) { var stack = [], i, j, args, getOperatorResult, term, value; for ( i = 0; i < postfix.length; i++ ) { term = postfix[ i ]; getOperatorResult = OPERATORS[ term ]; if ( getOperatorResult ) { // Pop from stack by number of function arguments. j = getOperatorResult.length; args = Array( j ); while ( j-- ) { args[ j ] = stack.pop(); } try { value = getOperatorResult.apply( null, args ); } catch ( earlyReturn ) { return earlyReturn; } } else if ( variables.hasOwnProperty( term ) ) { value = variables[ term ]; } else { value = +term; } stack.push( value ); } return stack[ 0 ]; } /** * Given a C expression, returns a function which can be called to evaluate its * result. * * @example * * ```js * import compile from '@tannin/compile'; * * const evaluate = compile( 'n > 1' ); * * evaluate( { n: 2 } ); * // ⇒ true * ``` * * @param {string} expression C expression. * * @return {(variables?:{[variable:string]:*})=>*} Compiled evaluator. */ function compile( expression ) { var terms = postfix( expression ); return function( variables ) { return evaluate( terms, variables ); }; } /** * Given a C expression, returns a function which, when called with a value, * evaluates the result with the value assumed to be the "n" variable of the * expression. The result will be coerced to its numeric equivalent. * * @param {string} expression C expression. * * @return {Function} Evaluator function. */ function pluralForms( expression ) { var evaluate = compile( expression ); return function( n ) { return +evaluate( { n: n } ); }; } /** * Tannin constructor options. * * @typedef {Object} TanninOptions * * @property {string} [contextDelimiter] Joiner in string lookup with context. * @property {Function} [onMissingKey] Callback to invoke when key missing. */ /** * Domain metadata. * * @typedef {Object} TanninDomainMetadata * * @property {string} [domain] Domain name. * @property {string} [lang] Language code. * @property {(string|Function)} [plural_forms] Plural forms expression or * function evaluator. */ /** * Domain translation pair respectively representing the singular and plural * translation. * * @typedef {[string,string]} TanninTranslation */ /** * Locale data domain. The key is used as reference for lookup, the value an * array of two string entries respectively representing the singular and plural * translation. * * @typedef {{[key:string]:TanninDomainMetadata|TanninTranslation,'':TanninDomainMetadata|TanninTranslation}} TanninLocaleDomain */ /** * Jed-formatted locale data. * * @see http://messageformat.github.io/Jed/ * * @typedef {{[domain:string]:TanninLocaleDomain}} TanninLocaleData */ /** * Default Tannin constructor options. * * @type {TanninOptions} */ var DEFAULT_OPTIONS = { contextDelimiter: '\u0004', onMissingKey: null, }; /** * Given a specific locale data's config `plural_forms` value, returns the * expression. * * @example * * ``` * getPluralExpression( 'nplurals=2; plural=(n != 1);' ) === '(n != 1)' * ``` * * @param {string} pf Locale data plural forms. * * @return {string} Plural forms expression. */ function getPluralExpression( pf ) { var parts, i, part; parts = pf.split( ';' ); for ( i = 0; i < parts.length; i++ ) { part = parts[ i ].trim(); if ( part.indexOf( 'plural=' ) === 0 ) { return part.substr( 7 ); } } } /** * Tannin constructor. * * @class * * @param {TanninLocaleData} data Jed-formatted locale data. * @param {TanninOptions} [options] Tannin options. */ function Tannin( data, options ) { var key; /** * Jed-formatted locale data. * * @name Tannin#data * @type {TanninLocaleData} */ this.data = data; /** * Plural forms function cache, keyed by plural forms string. * * @name Tannin#pluralForms * @type {Object} */ this.pluralForms = {}; /** * Effective options for instance, including defaults. * * @name Tannin#options * @type {TanninOptions} */ this.options = {}; for ( key in DEFAULT_OPTIONS ) { this.options[ key ] = options !== undefined && key in options ? options[ key ] : DEFAULT_OPTIONS[ key ]; } } /** * Returns the plural form index for the given domain and value. * * @param {string} domain Domain on which to calculate plural form. * @param {number} n Value for which plural form is to be calculated. * * @return {number} Plural form index. */ Tannin.prototype.getPluralForm = function( domain, n ) { var getPluralForm = this.pluralForms[ domain ], config, plural, pf; if ( ! getPluralForm ) { config = this.data[ domain ][ '' ]; pf = ( config[ 'Plural-Forms' ] || config[ 'plural-forms' ] || // Ignore reason: As known, there's no way to document the empty // string property on a key to guarantee this as metadata. // @ts-ignore config.plural_forms ); if ( typeof pf !== 'function' ) { plural = getPluralExpression( config[ 'Plural-Forms' ] || config[ 'plural-forms' ] || // Ignore reason: As known, there's no way to document the empty // string property on a key to guarantee this as metadata. // @ts-ignore config.plural_forms ); pf = pluralForms( plural ); } getPluralForm = this.pluralForms[ domain ] = pf; } return getPluralForm( n ); }; /** * Translate a string. * * @param {string} domain Translation domain. * @param {string|void} context Context distinguishing terms of the same name. * @param {string} singular Primary key for translation lookup. * @param {string=} plural Fallback value used for non-zero plural * form index. * @param {number=} n Value to use in calculating plural form. * * @return {string} Translated string. */ Tannin.prototype.dcnpgettext = function( domain, context, singular, plural, n ) { var index, key, entry; if ( n === undefined ) { // Default to singular. index = 0; } else { // Find index by evaluating plural form for value. index = this.getPluralForm( domain, n ); } key = singular; // If provided, context is prepended to key with delimiter. if ( context ) { key = context + this.options.contextDelimiter + singular; } entry = this.data[ domain ][ key ]; // Verify not only that entry exists, but that the intended index is within // range and non-empty. if ( entry && entry[ index ] ) { return entry[ index ]; } if ( this.options.onMissingKey ) { this.options.onMissingKey( singular, domain ); } // If entry not found, fall back to singular vs. plural with zero index // representing the singular value. return index === 0 ? singular : plural; }; return Tannin; }());