UNPKG

6.12 kBJavaScriptView Raw
1var reserved = require('reserved-words');
2var parse = require('messageformat-parser').parse;
3
4
5/** Creates a new message compiler. Called internally from {@link MessageFormat#compile}.
6 *
7 * @class
8 * @param {MessageFormat} mf - A MessageFormat instance
9 * @property {object} locales - The locale identifiers that are used by the compiled functions
10 * @property {object} runtime - Names of the core runtime functions that are used by the compiled functions
11 * @property {object} formatters - The formatter functions that are used by the compiled functions
12 */
13function Compiler(mf) {
14 this.mf = mf;
15 this.lc = null;
16 this.locales = {};
17 this.runtime = {};
18 this.formatters = {};
19}
20
21module.exports = Compiler;
22
23
24/** Utility function for quoting an Object's key value iff required
25 *
26 * Quotes the key if it contains invalid characters or is an
27 * ECMAScript 3rd Edition reserved word (for IE8).
28 */
29Compiler.propname = function(key, obj) {
30 if (/^[A-Z_$][0-9A-Z_$]*$/i.test(key) &&
31 ['break', 'continue', 'delete', 'else', 'for', 'function', 'if', 'in', 'new',
32 'return', 'this', 'typeof', 'var', 'void', 'while', 'with', 'case', 'catch',
33 'default', 'do', 'finally', 'instanceof', 'switch', 'throw', 'try'].indexOf(key) < 0) {
34 return obj ? obj + '.' + key : key;
35 } else {
36 var jkey = JSON.stringify(key);
37 return obj ? obj + '[' + jkey + ']' : jkey;
38 }
39}
40
41
42/** Utility function for escaping a function name iff required
43 */
44Compiler.funcname = function(key) {
45 var fn = key.trim().replace(/\W+/g, '_');
46 return reserved.check(fn, 'es2015', true) || /^\d/.test(fn) ? '_' + fn : fn;
47}
48
49
50/** Utility formatter function for enforcing Bidi Structured Text by using UCC
51 *
52 * List inlined from data extracted from CLDR v27 & v28
53 * To verify/recreate, use the following:
54 *
55 * git clone https://github.com/unicode-cldr/cldr-misc-full.git
56 * cd cldr-misc-full/main/
57 * grep characterOrder -r . | tr '"/' '\t' | cut -f2,6 | grep -C4 right-to-left
58 */
59Compiler.bidiMarkText = function(text, locale) {
60 function isLocaleRTL(locale) {
61 var rtlLanguages = ['ar', 'ckb', 'fa', 'he', 'ks($|[^bfh])', 'lrc', 'mzn',
62 'pa-Arab', 'ps', 'ug', 'ur', 'uz-Arab', 'yi'];
63 return new RegExp('^' + rtlLanguages.join('|^')).test(locale);
64 }
65 var mark = JSON.stringify(isLocaleRTL(locale) ? '\u200F' : '\u200E');
66 return mark + ' + ' + text + ' + ' + mark;
67}
68
69
70/** @private */
71Compiler.prototype.cases = function(token, plural) {
72 var needOther = true;
73 var r = token.cases.map(function(c) {
74 if (c.key === 'other') needOther = false;
75 var s = c.tokens.map(function(tok) { return this.token(tok, plural); }, this);
76 return Compiler.propname(c.key) + ': ' + (s.join(' + ') || '""');
77 }, this);
78 if (needOther) throw new Error("No 'other' form found in " + JSON.stringify(token));
79 return '{ ' + r.join(', ') + ' }';
80}
81
82
83/** @private */
84Compiler.prototype.token = function(token, plural) {
85 if (typeof token == 'string') return JSON.stringify(token);
86
87 var fn, args = [ Compiler.propname(token.arg, 'd') ];
88 switch (token.type) {
89 case 'argument':
90 return this.mf.bidiSupport ? Compiler.bidiMarkText(args[0], this.lc) : args[0];
91
92 case 'select':
93 fn = 'select';
94 args.push(this.cases(token, this.mf.strictNumberSign ? null : plural));
95 this.runtime.select = true;
96 break;
97
98 case 'selectordinal':
99 fn = 'plural';
100 args.push(0, Compiler.funcname(this.lc), this.cases(token, token), 1);
101 this.locales[this.lc] = true;
102 this.runtime.plural = true;
103 break;
104
105 case 'plural':
106 fn = 'plural';
107 args.push(token.offset || 0, Compiler.funcname(this.lc), this.cases(token, token));
108 this.locales[this.lc] = true;
109 this.runtime.plural = true;
110 break;
111
112 case 'function':
113 if (this.mf.intlSupport && !(token.key in this.mf.fmt) && (token.key in this.mf.constructor.formatters)) {
114 var fmt = this.mf.constructor.formatters[token.key];
115 this.mf.fmt[token.key] = (typeof fmt(this.mf) == 'function') ? fmt(this.mf) : fmt;
116 }
117 if (!this.mf.fmt[token.key]) throw new Error('Formatting function ' + JSON.stringify(token.key) + ' not found!');
118 args.push(JSON.stringify(this.lc));
119 if (token.params) switch (token.params.length) {
120 case 0: break;
121 case 1: args.push(JSON.stringify(token.params[0])); break;
122 default: args.push(JSON.stringify(token.params)); break;
123 }
124 fn = Compiler.propname(token.key, 'fmt');
125 this.formatters[token.key] = true;
126 break;
127
128 case 'octothorpe':
129 if (!plural) return '"#"';
130 fn = 'number';
131 args = [ Compiler.propname(plural.arg, 'd'), JSON.stringify(plural.arg) ];
132 if (plural.offset) args.push(plural.offset);
133 this.runtime.number = true;
134 break;
135 }
136
137 if (!fn) throw new Error('Parser error for token ' + JSON.stringify(token));
138 return fn + '(' + args.join(', ') + ')';
139};
140
141
142/** Recursively compile a string or a tree of strings to JavaScript function sources
143 *
144 * If `src` is an object with a key that is also present in `plurals`, the key
145 * in question will be used as the locale identifier for its value. To disable
146 * the compile-time checks for plural & selectordinal keys while maintaining
147 * multi-locale support, use falsy values in `plurals`.
148 *
149 * @param {string|object} src - the source for which the JS code should be generated
150 * @param {string} lc - the default locale
151 * @param {object} plurals - a map of pluralization keys for all available locales
152 */
153Compiler.prototype.compile = function(src, lc, plurals) {
154 if (typeof src != 'object') {
155 this.lc = lc;
156 var pc = plurals[lc] || { cardinal: [], ordinal: [] };
157 var r = parse(src, pc).map(function(token) { return this.token(token); }, this);
158 return 'function(d) { return ' + (r.join(' + ') || '""') + '; }';
159 } else {
160 var result = {};
161 for (var key in src) {
162 var lcKey = plurals.hasOwnProperty(key) ? key : lc;
163 result[key] = this.compile(src[key], lcKey, plurals);
164 }
165 return result;
166 }
167}