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