var reserved = require('reserved-words'); var parse = require('messageformat-parser').parse; /** Creates a new message compiler. Called internally from {@link MessageFormat#compile}. * * @class * @private * @param {MessageFormat} mf - A MessageFormat instance * @property {object} locales - The locale identifiers that are used by the compiled functions * @property {object} runtime - Names of the core runtime functions that are used by the compiled functions * @property {object} formatters - The formatter functions that are used by the compiled functions */ function Compiler(mf) { this.mf = mf; this.lc = null; this.locales = {}; this.runtime = {}; this.formatters = {}; } module.exports = Compiler; /** Utility function for quoting an Object's key value if required * * Quotes the key if it contains invalid characters or is an * ECMAScript 3rd Edition reserved word (for IE8). */ Compiler.propname = function(key, obj) { if ( /^[A-Z_$][0-9A-Z_$]*$/i.test(key) && [ 'break', 'continue', 'delete', 'else', 'for', 'function', 'if', 'in', 'new', 'return', 'this', 'typeof', 'var', 'void', 'while', 'with', 'case', 'catch', 'default', 'do', 'finally', 'instanceof', 'switch', 'throw', 'try' ].indexOf(key) < 0 ) { return obj ? obj + '.' + key : key; } else { var jkey = JSON.stringify(key); return obj ? obj + '[' + jkey + ']' : jkey; } }; /** Utility function for escaping a function name if required */ Compiler.funcname = function(key) { var fn = key.trim().replace(/\W+/g, '_'); return reserved.check(fn, 'es2015', true) || /^\d/.test(fn) ? '_' + fn : fn; }; /** Utility formatter function for enforcing Bidi Structured Text by using UCC * * List inlined from data extracted from CLDR v27 & v28 * To verify/recreate, use the following: * * git clone https://github.com/unicode-cldr/cldr-misc-full.git * cd cldr-misc-full/main/ * grep characterOrder -r . | tr '"/' '\t' | cut -f2,6 | grep -C4 right-to-left */ Compiler.bidiMarkText = function(text, locale) { function isLocaleRTL(locale) { var rtlLanguages = [ 'ar', 'ckb', 'fa', 'he', 'ks($|[^bfh])', 'lrc', 'mzn', 'pa-Arab', 'ps', 'ug', 'ur', 'uz-Arab', 'yi' ]; return new RegExp('^' + rtlLanguages.join('|^')).test(locale); } var mark = JSON.stringify(isLocaleRTL(locale) ? '\u200F' : '\u200E'); return mark + ' + ' + text + ' + ' + mark; }; /** @private */ Compiler.prototype.cases = function(token, plural) { var needOther = token.type === 'select' || !this.mf.hasCustomPluralFuncs; var r = token.cases.map(function(c) { if (c.key === 'other') needOther = false; var s = c.tokens.map(function(tok) { return this.token(tok, plural); }, this); return Compiler.propname(c.key) + ': ' + (s.join(' + ') || '""'); }, this); if (needOther) throw new Error("No 'other' form found in " + JSON.stringify(token)); return '{ ' + r.join(', ') + ' }'; }; /** @private */ Compiler.prototype.token = function(token, plural) { if (typeof token == 'string') return JSON.stringify(token); var fn, args = [Compiler.propname(token.arg, 'd')]; switch (token.type) { case 'argument': return this.mf.bidiSupport ? Compiler.bidiMarkText(args[0], this.lc) : args[0]; case 'select': fn = 'select'; if (plural && this.mf.strictNumberSign) plural = null; args.push(this.cases(token, plural)); this.runtime.select = true; break; case 'selectordinal': fn = 'plural'; args.push(0, Compiler.funcname(this.lc), this.cases(token, token), 1); this.locales[this.lc] = true; this.runtime.plural = true; break; case 'plural': fn = 'plural'; args.push( token.offset || 0, Compiler.funcname(this.lc), this.cases(token, token) ); this.locales[this.lc] = true; this.runtime.plural = true; break; case 'function': if ( !(token.key in this.mf.fmt) && token.key in this.mf.constructor.formatters ) { var fmt = this.mf.constructor.formatters[token.key]; this.mf.fmt[token.key] = fmt(this.mf); } if (!this.mf.fmt[token.key]) throw new Error( 'Formatting function ' + JSON.stringify(token.key) + ' not found!' ); args.push(JSON.stringify(this.lc)); if (token.param) { if (plural && this.mf.strictNumberSign) plural = null; var s = token.param.tokens.map(function(tok) { return this.token(tok, plural); }, this); args.push('(' + (s.join(' + ') || '""') + ').trim()'); } fn = Compiler.propname(token.key, 'fmt'); this.formatters[token.key] = true; break; case 'octothorpe': if (!plural) return '"#"'; fn = 'number'; args = [Compiler.propname(plural.arg, 'd'), JSON.stringify(plural.arg)]; if (plural.offset) args.push(plural.offset); this.runtime.number = true; break; } if (!fn) throw new Error('Parser error for token ' + JSON.stringify(token)); return fn + '(' + args.join(', ') + ')'; }; /** Recursively compile a string or a tree of strings to JavaScript function sources * * If `src` is an object with a key that is also present in `plurals`, the key * in question will be used as the locale identifier for its value. To disable * the compile-time checks for plural & selectordinal keys while maintaining * multi-locale support, use falsy values in `plurals`. * * @param {string|object} src - the source for which the JS code should be generated * @param {string} lc - the default locale * @param {object} plurals - a map of pluralization keys for all available locales */ Compiler.prototype.compile = function(src, lc, plurals) { if (typeof src != 'object') { this.lc = lc; var pc = plurals[lc] || { cardinal: [], ordinal: [] }; pc.strict = !!this.mf.strictNumberSign; var r = parse(src, pc).map(function(token) { return this.token(token); }, this); return 'function(d) { return ' + (r.join(' + ') || '""') + '; }'; } else { var result = {}; for (var key in src) { var lcKey = plurals.hasOwnProperty(key) ? key : lc; result[key] = this.compile(src[key], lcKey, plurals); } return result; } };