@ultraq/icu-message-formatter 0.14.1 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,63 +39,72 @@ var functionUtils = require('@ultraq/function-utils');
39
39
  * @param {string} string
40
40
  * @return {ParseCasesResult}
41
41
  */
42
- function parseCases() {
43
- let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
44
- const isWhitespace = ch => /\s/.test(ch);
45
- const args = [];
46
- const cases = {};
47
- let currTermStart = 0;
48
- let latestTerm = null;
49
- let inTerm = false;
50
- let i = 0;
51
- while (i < string.length) {
52
- // Term ended
53
- if (inTerm && (isWhitespace(string[i]) || string[i] === '{')) {
54
- inTerm = false;
55
- latestTerm = string.slice(currTermStart, i);
56
-
57
- // We want to process the opening char again so the case will be properly registered.
58
- if (string[i] === '{') {
59
- i--;
60
- }
61
- }
62
-
63
- // New term
64
- else if (!inTerm && !isWhitespace(string[i])) {
65
- const caseBody = string[i] === '{';
66
-
67
- // If there's a previous term, we can either handle a whole
68
- // case, or add that as an argument.
69
- if (latestTerm && caseBody) {
70
- const branchEndIndex = findClosingBracket(string, i);
71
- if (branchEndIndex === -1) {
72
- throw new Error(`Unbalanced curly braces in string: "${string}"`);
73
- }
74
- cases[latestTerm] = string.slice(i + 1, branchEndIndex); // Don't include the braces
75
-
76
- i = branchEndIndex; // Will be moved up where needed at end of loop.
77
- latestTerm = null;
78
- } else {
79
- if (latestTerm) {
80
- args.push(latestTerm);
81
- latestTerm = null;
82
- }
83
- inTerm = true;
84
- currTermStart = i;
85
- }
86
- }
87
- i++;
88
- }
89
- if (inTerm) {
90
- latestTerm = string.slice(currTermStart);
91
- }
92
- if (latestTerm) {
93
- args.push(latestTerm);
94
- }
95
- return {
96
- args,
97
- cases
98
- };
42
+ function parseCases(string = '') {
43
+ const isWhitespace = ch => /\s/.test(ch);
44
+
45
+ const args = [];
46
+ const cases = {};
47
+
48
+ let currTermStart = 0;
49
+ let latestTerm = null;
50
+ let inTerm = false;
51
+
52
+ let i = 0;
53
+ while (i < string.length) {
54
+ // Term ended
55
+ if (inTerm && (isWhitespace(string[i]) || string[i] === '{')) {
56
+ inTerm = false;
57
+ latestTerm = string.slice(currTermStart, i);
58
+
59
+ // We want to process the opening char again so the case will be properly registered.
60
+ if (string[i] === '{') {
61
+ i--;
62
+ }
63
+ }
64
+
65
+ // New term
66
+ else if (!inTerm && !isWhitespace(string[i])) {
67
+ const caseBody = string[i] === '{';
68
+
69
+ // If there's a previous term, we can either handle a whole
70
+ // case, or add that as an argument.
71
+ if (latestTerm && caseBody) {
72
+ const branchEndIndex = findClosingBracket(string, i);
73
+
74
+ if (branchEndIndex === -1) {
75
+ throw new Error(`Unbalanced curly braces in string: "${string}"`);
76
+ }
77
+
78
+ cases[latestTerm] = string.slice(i + 1, branchEndIndex); // Don't include the braces
79
+
80
+ i = branchEndIndex; // Will be moved up where needed at end of loop.
81
+ latestTerm = null;
82
+ }
83
+ else {
84
+ if (latestTerm) {
85
+ args.push(latestTerm);
86
+ latestTerm = null;
87
+ }
88
+
89
+ inTerm = true;
90
+ currTermStart = i;
91
+ }
92
+ }
93
+ i++;
94
+ }
95
+
96
+ if (inTerm) {
97
+ latestTerm = string.slice(currTermStart);
98
+ }
99
+
100
+ if (latestTerm) {
101
+ args.push(latestTerm);
102
+ }
103
+
104
+ return {
105
+ args,
106
+ cases
107
+ };
99
108
  }
100
109
 
101
110
  /**
@@ -109,19 +118,20 @@ function parseCases() {
109
118
  * could be found.
110
119
  */
111
120
  function findClosingBracket(string, fromIndex) {
112
- let depth = 0;
113
- for (let i = fromIndex + 1; i < string.length; i++) {
114
- let char = string.charAt(i);
115
- if (char === '}') {
116
- if (depth === 0) {
117
- return i;
118
- }
119
- depth--;
120
- } else if (char === '{') {
121
- depth++;
122
- }
123
- }
124
- return -1;
121
+ let depth = 0;
122
+ for (let i = fromIndex + 1; i < string.length; i++) {
123
+ let char = string.charAt(i);
124
+ if (char === '}') {
125
+ if (depth === 0) {
126
+ return i;
127
+ }
128
+ depth--;
129
+ }
130
+ else if (char === '{') {
131
+ depth++;
132
+ }
133
+ }
134
+ return -1;
125
135
  }
126
136
 
127
137
  /**
@@ -134,7 +144,7 @@ function findClosingBracket(string, fromIndex) {
134
144
  * in the formatted argument block.
135
145
  */
136
146
  function splitFormattedArgument(block) {
137
- return split(block.slice(1, -1), ',', 3);
147
+ return split(block.slice(1, -1), ',', 3);
138
148
  }
139
149
 
140
150
  /**
@@ -148,24 +158,23 @@ function splitFormattedArgument(block) {
148
158
  * @param {string[]} accumulator
149
159
  * @return {string[]}
150
160
  */
151
- function split(string, separator, limit) {
152
- let accumulator = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
153
- if (!string) {
154
- return accumulator;
155
- }
156
- if (limit === 1) {
157
- accumulator.push(string);
158
- return accumulator;
159
- }
160
- let indexOfDelimiter = string.indexOf(separator);
161
- if (indexOfDelimiter === -1) {
162
- accumulator.push(string);
163
- return accumulator;
164
- }
165
- let head = string.substring(0, indexOfDelimiter).trim();
166
- let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
167
- accumulator.push(head);
168
- return split(tail, separator, limit - 1, accumulator);
161
+ function split(string, separator, limit, accumulator = []) {
162
+ if (!string) {
163
+ return accumulator;
164
+ }
165
+ if (limit === 1) {
166
+ accumulator.push(string);
167
+ return accumulator;
168
+ }
169
+ let indexOfDelimiter = string.indexOf(separator);
170
+ if (indexOfDelimiter === -1) {
171
+ accumulator.push(string);
172
+ return accumulator;
173
+ }
174
+ let head = string.substring(0, indexOfDelimiter).trim();
175
+ let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
176
+ accumulator.push(head);
177
+ return split(tail, separator, limit - 1, accumulator);
169
178
  }
170
179
 
171
180
  /*
@@ -219,86 +228,88 @@ function split(string, separator, limit) {
219
228
  * @author Emanuel Rabina
220
229
  */
221
230
  class MessageFormatter {
222
- /**
223
- * Creates a new formatter that can work using any of the custom type handlers
224
- * you register.
225
- *
226
- * @param {string} locale
227
- * @param {Record<string,TypeHandler>} [typeHandlers]
228
- * Optional object where the keys are the names of the types to register,
229
- * their values being the functions that will return a nicely formatted
230
- * string for the data and locale they are given.
231
- */
232
- constructor(locale) {
233
- let typeHandlers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
234
- this.locale = locale;
235
- this.typeHandlers = typeHandlers;
236
- }
237
-
238
- /**
239
- * Formats an ICU message syntax string using `values` for placeholder data
240
- * and any currently-registered type handlers.
241
- *
242
- * @type {(message: string, values?: FormatValues) => string}
243
- */
244
- format = functionUtils.memoize((() => {
245
- var _this = this;
246
- return function (message) {
247
- let values = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
248
- return _this.process(message, values).flat(Infinity).join('');
249
- };
250
- })());
251
-
252
- /**
253
- * Process an ICU message syntax string using `values` for placeholder data
254
- * and any currently-registered type handlers. The result of this method is
255
- * an array of the component parts after they have been processed in turn by
256
- * their own type handlers. This raw output is useful for other renderers,
257
- * eg: React where components can be used instead of being forced to return
258
- * raw strings.
259
- *
260
- * This method is used by {@link MessageFormatter#format} where it acts as a
261
- * string renderer.
262
- *
263
- * @param {string} message
264
- * @param {FormatValues} [values]
265
- * @return {any[]}
266
- */
267
- process(message) {
268
- let values = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
269
- if (!message) {
270
- return [];
271
- }
272
- let blockStartIndex = message.indexOf('{');
273
- if (blockStartIndex !== -1) {
274
- let blockEndIndex = findClosingBracket(message, blockStartIndex);
275
- if (blockEndIndex !== -1) {
276
- let block = message.substring(blockStartIndex, blockEndIndex + 1);
277
- if (block) {
278
- let result = [];
279
- let head = message.substring(0, blockStartIndex);
280
- if (head) {
281
- result.push(head);
282
- }
283
- let [key, type, format] = splitFormattedArgument(block);
284
- let body = values[key];
285
- if (body === null || body === undefined) {
286
- body = '';
287
- }
288
- let typeHandler = type && this.typeHandlers[type];
289
- result.push(typeHandler ? typeHandler(body, format, this.locale, values, this.process.bind(this)) : body);
290
- let tail = message.substring(blockEndIndex + 1);
291
- if (tail) {
292
- result.push(this.process(tail, values));
293
- }
294
- return result;
295
- }
296
- } else {
297
- throw new Error(`Unbalanced curly braces in string: "${message}"`);
298
- }
299
- }
300
- return [message];
301
- }
231
+
232
+ /**
233
+ * Creates a new formatter that can work using any of the custom type handlers
234
+ * you register.
235
+ *
236
+ * @param {string} locale
237
+ * @param {Record<string,TypeHandler>} [typeHandlers]
238
+ * Optional object where the keys are the names of the types to register,
239
+ * their values being the functions that will return a nicely formatted
240
+ * string for the data and locale they are given.
241
+ */
242
+ constructor(locale, typeHandlers = {}) {
243
+
244
+ this.locale = locale;
245
+ this.typeHandlers = typeHandlers;
246
+ }
247
+
248
+ /**
249
+ * Formats an ICU message syntax string using `values` for placeholder data
250
+ * and any currently-registered type handlers.
251
+ *
252
+ * @type {(message: string, values?: FormatValues) => string}
253
+ */
254
+ format = functionUtils.memoize((message, values = {}) => {
255
+
256
+ return this.process(message, values).flat(Infinity).join('');
257
+ });
258
+
259
+ /**
260
+ * Process an ICU message syntax string using `values` for placeholder data
261
+ * and any currently-registered type handlers. The result of this method is
262
+ * an array of the component parts after they have been processed in turn by
263
+ * their own type handlers. This raw output is useful for other renderers,
264
+ * eg: React where components can be used instead of being forced to return
265
+ * raw strings.
266
+ *
267
+ * This method is used by {@link MessageFormatter#format} where it acts as a
268
+ * string renderer.
269
+ *
270
+ * @param {string} message
271
+ * @param {FormatValues} [values]
272
+ * @return {any[]}
273
+ */
274
+ process(message, values = {}) {
275
+
276
+ if (!message) {
277
+ return [];
278
+ }
279
+
280
+ let blockStartIndex = message.indexOf('{');
281
+ if (blockStartIndex !== -1) {
282
+ let blockEndIndex = findClosingBracket(message, blockStartIndex);
283
+ if (blockEndIndex !== -1) {
284
+ let block = message.substring(blockStartIndex, blockEndIndex + 1);
285
+ if (block) {
286
+ let result = [];
287
+ let head = message.substring(0, blockStartIndex);
288
+ if (head) {
289
+ result.push(head);
290
+ }
291
+ let [key, type, format] = splitFormattedArgument(block);
292
+ let body = values[key];
293
+ if (body === null || body === undefined) {
294
+ body = '';
295
+ }
296
+ let typeHandler = type && this.typeHandlers[type];
297
+ result.push(typeHandler ?
298
+ typeHandler(body, format, this.locale, values, this.process.bind(this)) :
299
+ body);
300
+ let tail = message.substring(blockEndIndex + 1);
301
+ if (tail) {
302
+ result.push(this.process(tail, values));
303
+ }
304
+ return result;
305
+ }
306
+ }
307
+ else {
308
+ throw new Error(`Unbalanced curly braces in string: "${message}"`);
309
+ }
310
+ }
311
+ return [message];
312
+ }
302
313
  }
303
314
 
304
315
  /*
@@ -317,11 +328,13 @@ class MessageFormatter {
317
328
  * limitations under the License.
318
329
  */
319
330
 
331
+
320
332
  let pluralFormatter;
333
+
321
334
  let keyCounter = 0;
322
335
 
323
336
  // All the special keywords that can be used in `plural` blocks for the various branches
324
- const ONE = 'one';
337
+ const ONE = 'one';
325
338
  const OTHER$1 = 'other';
326
339
 
327
340
  /**
@@ -331,29 +344,35 @@ const OTHER$1 = 'other';
331
344
  * @return {{caseBody: string, numberValues: object}}
332
345
  */
333
346
  function replaceNumberSign(caseBody, value) {
334
- let i = 0;
335
- let output = '';
336
- let numBraces = 0;
337
- const numberValues = {};
338
- while (i < caseBody.length) {
339
- if (caseBody[i] === '#' && !numBraces) {
340
- let keyParam = `__hashToken${keyCounter++}`;
341
- output += `{${keyParam}, number}`;
342
- numberValues[keyParam] = value;
343
- } else {
344
- output += caseBody[i];
345
- }
346
- if (caseBody[i] === '{') {
347
- numBraces++;
348
- } else if (caseBody[i] === '}') {
349
- numBraces--;
350
- }
351
- i++;
352
- }
353
- return {
354
- caseBody: output,
355
- numberValues
356
- };
347
+ let i = 0;
348
+ let output = '';
349
+ let numBraces = 0;
350
+ const numberValues = {};
351
+
352
+ while (i < caseBody.length) {
353
+ if (caseBody[i] === '#' && !numBraces) {
354
+ let keyParam = `__hashToken${keyCounter++}`;
355
+ output += `{${keyParam}, number}`;
356
+ numberValues[keyParam] = value;
357
+ }
358
+ else {
359
+ output += caseBody[i];
360
+ }
361
+
362
+ if (caseBody[i] === '{') {
363
+ numBraces++;
364
+ }
365
+ else if (caseBody[i] === '}') {
366
+ numBraces--;
367
+ }
368
+
369
+ i++;
370
+ }
371
+
372
+ return {
373
+ caseBody: output,
374
+ numberValues
375
+ };
357
376
  }
358
377
 
359
378
  /**
@@ -371,47 +390,48 @@ function replaceNumberSign(caseBody, value) {
371
390
  * @return {any | any[]}
372
391
  */
373
392
  function pluralTypeHandler(value, matches, locale, values, process) {
374
- const {
375
- args,
376
- cases
377
- } = parseCases(matches);
378
- let intValue = parseInt(value);
379
- args.forEach(arg => {
380
- if (arg.startsWith('offset:')) {
381
- intValue -= parseInt(arg.slice('offset:'.length));
382
- }
383
- });
384
- const keywordPossibilities = [];
385
- if ('PluralRules' in Intl) {
386
- // Effectively memoize because instantiation of `Int.*` objects is expensive.
387
- if (pluralFormatter === undefined || pluralFormatter.resolvedOptions().locale !== locale) {
388
- pluralFormatter = new Intl.PluralRules(locale);
389
- }
390
- const pluralKeyword = pluralFormatter.select(intValue);
391
-
392
- // Other is always added last with least priority, so we don't want to add it here.
393
- if (pluralKeyword !== OTHER$1) {
394
- keywordPossibilities.push(pluralKeyword);
395
- }
396
- }
397
- if (intValue === 1) {
398
- keywordPossibilities.push(ONE);
399
- }
400
- keywordPossibilities.push(`=${intValue}`, OTHER$1);
401
- for (let i = 0; i < keywordPossibilities.length; i++) {
402
- const keyword = keywordPossibilities[i];
403
- if (keyword in cases) {
404
- const {
405
- caseBody,
406
- numberValues
407
- } = replaceNumberSign(cases[keyword], intValue);
408
- return process(caseBody, {
409
- ...values,
410
- ...numberValues
411
- });
412
- }
413
- }
414
- return value;
393
+ const {args, cases} = parseCases(matches);
394
+
395
+ let intValue = parseInt(value);
396
+
397
+ args.forEach((arg) => {
398
+ if (arg.startsWith('offset:')) {
399
+ intValue -= parseInt(arg.slice('offset:'.length));
400
+ }
401
+ });
402
+
403
+ const keywordPossibilities = [];
404
+
405
+ if ('PluralRules' in Intl) {
406
+ // Effectively memoize because instantiation of `Int.*` objects is expensive.
407
+ if (pluralFormatter === undefined || pluralFormatter.resolvedOptions().locale !== locale) {
408
+ pluralFormatter = new Intl.PluralRules(locale);
409
+ }
410
+
411
+ const pluralKeyword = pluralFormatter.select(intValue);
412
+
413
+ // Other is always added last with least priority, so we don't want to add it here.
414
+ if (pluralKeyword !== OTHER$1) {
415
+ keywordPossibilities.push(pluralKeyword);
416
+ }
417
+ }
418
+ if (intValue === 1) {
419
+ keywordPossibilities.push(ONE);
420
+ }
421
+ keywordPossibilities.push(`=${intValue}`, OTHER$1);
422
+
423
+ for (let i = 0; i < keywordPossibilities.length; i++) {
424
+ const keyword = keywordPossibilities[i];
425
+ if (keyword in cases) {
426
+ const {caseBody, numberValues} = replaceNumberSign(cases[keyword], intValue);
427
+ return process(caseBody, {
428
+ ...values,
429
+ ...numberValues
430
+ });
431
+ }
432
+ }
433
+
434
+ return value;
415
435
  }
416
436
 
417
437
  /*
@@ -430,6 +450,7 @@ function pluralTypeHandler(value, matches, locale, values, process) {
430
450
  * limitations under the License.
431
451
  */
432
452
 
453
+
433
454
  const OTHER = 'other';
434
455
 
435
456
  /**
@@ -447,15 +468,16 @@ const OTHER = 'other';
447
468
  * @return {any | any[]}
448
469
  */
449
470
  function selectTypeHandler(value, matches, locale, values, process) {
450
- const {
451
- cases
452
- } = parseCases(matches);
453
- if (value in cases) {
454
- return process(cases[value], values);
455
- } else if (OTHER in cases) {
456
- return process(cases[OTHER], values);
457
- }
458
- return value;
471
+ const {cases} = parseCases(matches);
472
+
473
+ if (value in cases) {
474
+ return process(cases[value], values);
475
+ }
476
+ else if (OTHER in cases) {
477
+ return process(cases[OTHER], values);
478
+ }
479
+
480
+ return value;
459
481
  }
460
482
 
461
483
  exports.MessageFormatter = MessageFormatter;