@curly-message/parser 1.0.0-next.0
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.
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/dist/index.d.ts +297 -0
- package/dist/index.js +2 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 G.A.W.Group, s.r.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# @curly-message/parser
|
|
2
|
+
|
|
3
|
+
The JavaScript implementation of the
|
|
4
|
+
[Curly Message Format](https://github.com/curly-message/spec).
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"greeting": "Hello, {{name; default:Guest;}}!",
|
|
9
|
+
"inbox": "You have {{count:number;}} {{count; 1:message; default:messages;}}."
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
greeting { name: 'Alice' } -> "Hello, Alice!"
|
|
15
|
+
greeting {} -> "Hello, Guest!"
|
|
16
|
+
inbox { count: 1 } -> "You have 1 message."
|
|
17
|
+
inbox { count: 1234 } -> "You have 1,234 messages."
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A placeholder is written on one line: a `{{` and a `}}` with a line terminator
|
|
21
|
+
anywhere between them are text rather than a placeholder, and escaping the
|
|
22
|
+
terminator does not make them one. A placeholder that names no key — `{{}}`,
|
|
23
|
+
`{{ }}` — is a placeholder still, and resolves through the fallback chain.
|
|
24
|
+
|
|
25
|
+
Placeholders may carry a modifier (`:number`, `:date`, `:ago`, `:currency`, or
|
|
26
|
+
one of the comparisons `eq`, `ne`, `lt`, `lte`, `gt`, `gte`), a set of options,
|
|
27
|
+
and a `default`. Locale-dependent formatting is delegated to `Intl`; the
|
|
28
|
+
package itself has no runtime dependencies. A modifier that cannot produce a
|
|
29
|
+
result — a locale the host rejects, a custom modifier that throws — resolves
|
|
30
|
+
the placeholder to its `default` rather than raising, and so does a value that
|
|
31
|
+
no conversion turns into text. Neither is silent: containment keeps the failure
|
|
32
|
+
out of the caller's render path, and a report is how the caller hears about it
|
|
33
|
+
anyway. A placeholder naming a modifier the parser does not know resolves to
|
|
34
|
+
its `default` and is reported as well; it is never run as a comparison instead.
|
|
35
|
+
A comparison that declares no options has been asked to select from nothing —
|
|
36
|
+
the inline `default` is the fallback itself rather than something to select, so
|
|
37
|
+
`{{v:eq; default:D}}` declares none either. It resolves to the fallback chain,
|
|
38
|
+
as it always did, and reports `missing-options`. That is a defect in how the
|
|
39
|
+
placeholder was written, so it reports whether or not the payload carries the
|
|
40
|
+
key. The six names are the format's comparisons while the format's own
|
|
41
|
+
modifier answers to them: a host that registers its own `eq` has replaced the
|
|
42
|
+
comparison, and whether its modifier needs options is that modifier's business,
|
|
43
|
+
so `{{v:eq}}` over that registration reports nothing. A placeholder naming no
|
|
44
|
+
key has nothing to compare, so `{{:eq}}` is no selection and reports nothing;
|
|
45
|
+
`{{:zz}}` still reports the modifier nobody registered.
|
|
46
|
+
Given no locale the formatting modifiers resolve to the empty string, not to
|
|
47
|
+
the fallback chain: a declared default does not stand in for a locale nobody
|
|
48
|
+
supplied. A caller that passes none and a caller that passes the empty string
|
|
49
|
+
resolve alike; one that passes a locale the host then rejects has supplied one,
|
|
50
|
+
and takes the fallback chain like any other formatting failure. The empty
|
|
51
|
+
string is reported as `missing-locale`, whose origin is the payload: a locale
|
|
52
|
+
nobody supplied is a defect in what the caller passed rather than in the
|
|
53
|
+
message that was written.
|
|
54
|
+
|
|
55
|
+
Given a locale, each formatting modifier reads its value as a particular kind
|
|
56
|
+
of number, and a value that is not one resolves the placeholder to its
|
|
57
|
+
`default` and is reported as `failed-modifier`, like any other result a
|
|
58
|
+
modifier could not produce. Its origin is the payload too: the value, the props
|
|
59
|
+
and the locale a modifier is handed are the caller's, and so is a
|
|
60
|
+
`customModifiers` entry that raised, so none of it is a defect in the message
|
|
61
|
+
that was written. The kind each reads:
|
|
62
|
+
|
|
63
|
+
| Modifier | Value |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `number` | a number |
|
|
66
|
+
| `date` | milliseconds since the Unix epoch, or text the host can parse as a date |
|
|
67
|
+
| `ago` | a signed millisecond delta relative to now, negative for the past |
|
|
68
|
+
| `currency` | a number, multiplied by the `ratio` property below |
|
|
69
|
+
|
|
70
|
+
Empty text and text that is only whitespace are none of these, whatever the
|
|
71
|
+
host's own numeric conversion makes of them: `{{v:number}}` over `{ v: '' }`
|
|
72
|
+
takes the fallback chain rather than formatting a zero, and `{{v:date}}` over
|
|
73
|
+
the same value takes it rather than formatting the epoch.
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { createParser } from '@curly-message/parser';
|
|
79
|
+
|
|
80
|
+
const { resolve } = createParser();
|
|
81
|
+
|
|
82
|
+
resolve('Hello, {{name; default:Guest;}}!', { payload: { name: 'Alice' }, locale: 'en' });
|
|
83
|
+
// -> 'Hello, Alice!'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`createParser(options?)` returns a parser whose `resolve(message, context?)`
|
|
87
|
+
takes the four inputs resolution is defined over, plus the message's own key:
|
|
88
|
+
|
|
89
|
+
| Context | Meaning |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `payload` | The values the placeholders name, or the configuration of one — see [Payload](#payload). Its `default` key is the message-wide fallback. |
|
|
92
|
+
| `props` | Per-call formatting options handed to the modifiers, keyed by modifier name. A payload entry layers over them. |
|
|
93
|
+
| `locale` | The locale the locale-dependent modifiers format for. |
|
|
94
|
+
| `key` | The message's identifier. A missing message resolves to the payload's `default`, and to this where the payload carries none, echoed as the caller spelled it. |
|
|
95
|
+
|
|
96
|
+
A message that no conversion describes is a message that does not exist:
|
|
97
|
+
resolution steps past it to the payload's `default` and then to the key, and
|
|
98
|
+
reports nothing, because a message nobody wrote is not a defect. The link it
|
|
99
|
+
steps to is a payload value like any other, so one that is read and cannot be
|
|
100
|
+
described is reported.
|
|
101
|
+
|
|
102
|
+
The key is where that chain ends, and it is not text the format resolves over.
|
|
103
|
+
A key shaped like a placeholder is echoed rather than resolved, an escape
|
|
104
|
+
sequence inside one stays as it was spelled, and nothing behind the key is read
|
|
105
|
+
a second time. It still becomes text, because `resolve` answers with text.
|
|
106
|
+
|
|
107
|
+
`options` carries `customModifiers`, `modifierDefaults` and `onReport`. Nothing
|
|
108
|
+
else is read, and the package has no runtime dependencies — locale-dependent
|
|
109
|
+
formatting is delegated to `Intl`.
|
|
110
|
+
|
|
111
|
+
`customModifiers` registers modifiers by name, over the built-in ones, so a
|
|
112
|
+
name it carries is a name a message may write, and so is a name the parser
|
|
113
|
+
holds a modifier under already. What it registers has to be a modifier: an
|
|
114
|
+
entry that cannot be called registers none, so it takes no name of its own and
|
|
115
|
+
does not shadow the built-in it names. Where nothing else answers to the name,
|
|
116
|
+
a message writing it reads it as one nobody registered — `unknown-modifier`,
|
|
117
|
+
the fallback chain; where a built-in answers to it, that built-in answers as it
|
|
118
|
+
did, so `{ eq: 'text' }` costs a caller the name it wrote and nothing further.
|
|
119
|
+
The types say as much, but a JavaScript caller reaches the table regardless.
|
|
120
|
+
|
|
121
|
+
`onReport` is where diagnostics go. The parser writes to no channel of its own,
|
|
122
|
+
so unset it reports nowhere; resolution still fails soft, it just does so
|
|
123
|
+
silently. It is called with a `Report` describing what the parser could not do
|
|
124
|
+
— `code`, the `origin` that code declares, an English `message` carrying
|
|
125
|
+
nothing from the payload, the `limit` reached where the report is about one,
|
|
126
|
+
the message's `key` where one was passed, and `text`, the source of the
|
|
127
|
+
trouble: the placeholder that named it for `unknown-modifier`,
|
|
128
|
+
`failed-modifier`, `missing-options`, `missing-locale` and
|
|
129
|
+
`unserializable-value`, the output that would not settle for `pass-limit` and
|
|
130
|
+
`output-limit`, the message as it was passed where the read that refused is of
|
|
131
|
+
the call's own structure — a context entry, an entry of the option bag — and
|
|
132
|
+
nothing at all where what could not be described is the chain the message
|
|
133
|
+
itself resolves through. The last two name no placeholder, and a message that
|
|
134
|
+
is not text carries none of itself either. `origin` says who fixes what `code`
|
|
135
|
+
names — `'message'` for a defect in the message that was written, `'payload'`
|
|
136
|
+
for one in what the caller passed, and `'limit'` for a bound this parser set.
|
|
137
|
+
Every code declares one, and it ranks nothing: a report is no graver for coming
|
|
138
|
+
from one of the three than from another. Only `text` derives from the payload.
|
|
139
|
+
It is cut to 120 UTF-16 code units of what reached it — what a string's
|
|
140
|
+
`length` counts — or one fewer where the last of them is the high half of a
|
|
141
|
+
surrogate pair, so the cut never severs a character. A cut is marked with a
|
|
142
|
+
trailing `...` of the parser's own, and the excerpt is escaped after that —
|
|
143
|
+
quotes, backslashes, and every line terminator. So a cut excerpt arrives at
|
|
144
|
+
122 code units at the shortest, one carrying something to escape arrives
|
|
145
|
+
longer still, and no payload can forge a line where a report is written.
|
|
146
|
+
|
|
147
|
+
Two guards bound resolution, and reaching either is what the two limit codes
|
|
148
|
+
report. A payload value may name another placeholder, so interpolation runs
|
|
149
|
+
again over what the last pass produced — at most **10 passes**, after which the
|
|
150
|
+
output is returned with its remaining placeholders unresolved. And the output
|
|
151
|
+
may not exceed **100000 UTF-16 code units** — what a string's `length` counts,
|
|
152
|
+
so a character outside the Basic Multilingual Plane counts twice; a pass that
|
|
153
|
+
would carry it past that stops, and the last output under the bound is what
|
|
154
|
+
resolves. Both are what make
|
|
155
|
+
a payload value that references or multiplies its own placeholder terminate
|
|
156
|
+
rather than hang the caller.
|
|
157
|
+
|
|
158
|
+
A third bound holds the conversion that feeds them. Turning a value into JSON
|
|
159
|
+
follows a shared reference again every time it meets one, so a value naming the
|
|
160
|
+
same child twice at each of twenty-four levels holds twenty-five objects and
|
|
161
|
+
describes sixteen million leaves — no cycle anywhere, and nothing an output
|
|
162
|
+
bound measured after the fact can prevent. So the walk stops after **100000
|
|
163
|
+
nodes**, and the value is read as one no conversion describes: it falls through
|
|
164
|
+
its fallback chain and reports as `unserializable-value`. That is what a single
|
|
165
|
+
conversion may spend, and a resolution converts one value once however many
|
|
166
|
+
placeholders name it, so what a resolution spends converting is bounded by the
|
|
167
|
+
values it reaches rather than by the reads it makes of them. That covers the
|
|
168
|
+
host's own conversion as well as the JSON walk, so a class instance whose
|
|
169
|
+
`toString` runs host code runs it once for the resolution rather than once for
|
|
170
|
+
each placeholder, and one that raises is not asked a second time. It covers a
|
|
171
|
+
bigint as well, which runs no host code at all but converts to digits the
|
|
172
|
+
engine works out afresh on every read. A value built afresh on each read — by
|
|
173
|
+
a payload getter, or by a custom modifier — is a new value every time and is
|
|
174
|
+
converted every time. Whether an entry is a wrapper is asked the same way:
|
|
175
|
+
recognizing one enumerates the entry's own names, work that grows with the
|
|
176
|
+
entry, so a resolution asks one entry once however many placeholders name it,
|
|
177
|
+
and an entry that refuses the question is not asked again — though it reports
|
|
178
|
+
at every placeholder that reads it.
|
|
179
|
+
|
|
180
|
+
All three bounds belong to the call rather than to the parser, and a call is
|
|
181
|
+
what a host begins by calling `resolve` again while one is running — from a
|
|
182
|
+
custom modifier, from an `onReport` handler writing its diagnostic into a
|
|
183
|
+
translated string, from a payload accessor, or from a value's own `toString`.
|
|
184
|
+
Such a resolution counts its own passes, spends its own output and keeps its
|
|
185
|
+
own record of what it has converted, so neither it nor the one around it can
|
|
186
|
+
reach a bound the other owns, each report names the message its own call was
|
|
187
|
+
resolving, and a value the two share is converted once for each of them.
|
|
188
|
+
Nothing bounds how deep that goes: a modifier resolving a message that names
|
|
189
|
+
it again recurses until the host's own stack runs out, which is contained like
|
|
190
|
+
any other failure — the placeholder takes its fallback chain and `resolve`
|
|
191
|
+
still answers with text.
|
|
192
|
+
|
|
193
|
+
## Payload
|
|
194
|
+
|
|
195
|
+
Everything the format carries is text. A payload value reaches a modifier, and
|
|
196
|
+
the output, as text whatever type it was written at: a plain object and an
|
|
197
|
+
array become JSON, and every other value becomes what the host makes of it, so
|
|
198
|
+
a `Date`, a `RegExp` or a class instance reads as its own `toString` writes it.
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
{ v: 1234.5 } -> 1234.5
|
|
202
|
+
{ v: [1, 2] } -> [1,2]
|
|
203
|
+
{ v: { a: 1 } } -> {"a":1}
|
|
204
|
+
{ v: /re/g } -> /re/g
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
That conversion costs a `Date` its sub-second precision, because `String(date)`
|
|
208
|
+
writes seconds: `{{v:date}}` over `new Date('2024-03-05T10:00:00.123Z')`
|
|
209
|
+
renders the same instant with its milliseconds zeroed. The text is not numeric
|
|
210
|
+
either, so `{{v:number}}`, `{{v:currency}}`, `{{v:ago}}`, `{{v:lt}}` and
|
|
211
|
+
`{{v:gt}}` over a `Date` resolve to the fallback chain — the three formatting
|
|
212
|
+
ones given a locale, because with none they resolve to the empty string
|
|
213
|
+
whatever the value is. Pass a timestamp or an ISO string where a placeholder
|
|
214
|
+
needs either.
|
|
215
|
+
|
|
216
|
+
A payload entry may carry the value's own configuration — a wrapper — instead
|
|
217
|
+
of the value itself:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
resolve('You have {{count:number}} points.', {
|
|
221
|
+
payload: {
|
|
222
|
+
count: { value: 1234.56, props: { number: { maximumFractionDigits: 1 } } },
|
|
223
|
+
},
|
|
224
|
+
locale: 'en',
|
|
225
|
+
});
|
|
226
|
+
// -> 'You have 1,234.6 points.'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
An entry is a wrapper when it is a plain object that owns at least one key and
|
|
230
|
+
every key it owns is `value`, `default` or `props`. An entry owning anything
|
|
231
|
+
else is a value, wrapper-shaped or not: `{ value: 1, unit: 'kg' }` and `{}` are
|
|
232
|
+
data and become JSON. Unwrapping happens once, so a wrapper's `value` is never
|
|
233
|
+
read as a wrapper of its own, and a wrapper carrying no `value` falls back like
|
|
234
|
+
a key the payload does not carry. The payload's own `default` is always a
|
|
235
|
+
value.
|
|
236
|
+
|
|
237
|
+
A placeholder resolves to its value wherever the payload carries one, and
|
|
238
|
+
otherwise to the first of these that yields text:
|
|
239
|
+
|
|
240
|
+
1. the wrapper's `default`
|
|
241
|
+
2. the payload's `default`
|
|
242
|
+
3. the `default` the placeholder declares
|
|
243
|
+
4. the empty string
|
|
244
|
+
|
|
245
|
+
A value no conversion describes — a structure that references itself, a getter
|
|
246
|
+
that raises — is read as missing and falls through this chain, and so does any
|
|
247
|
+
link in it. Anything the chain reads and could not convert is reported as
|
|
248
|
+
`unserializable-value`; a link nothing reaches is never read, so it is never
|
|
249
|
+
reported either.
|
|
250
|
+
|
|
251
|
+
Every entry the parser reads is read as an own **enumerable** property: a
|
|
252
|
+
payload key, a wrapper's, a `props` layer's, an entry of the modifier registry,
|
|
253
|
+
and the call's own `payload`, `props`, `locale` and `key`. That is the rule the
|
|
254
|
+
value conversion has always followed, so a property `Object.defineProperty`
|
|
255
|
+
left hidden is not one the format carries anywhere — after
|
|
256
|
+
`Object.defineProperty(payload, 'v', { value: 'V' })`, `{{v}}` resolves to its
|
|
257
|
+
fallback chain, and a hidden `onReport` reports nowhere. A prototype somebody
|
|
258
|
+
else wrote to supplies nothing at any of those reads either.
|
|
259
|
+
|
|
260
|
+
A read that raises is an absence and a report wherever it sits, not only at a
|
|
261
|
+
value. A `props` entry that refuses to be read leaves the layer beneath it
|
|
262
|
+
standing and reports `unserializable-value` at the placeholder that needed it;
|
|
263
|
+
`customModifiers`, `modifierDefaults` and the context's own entries are read
|
|
264
|
+
once for the call, so a report about one names the text that went looking
|
|
265
|
+
rather than a placeholder, and a `key` that is itself the entry that raised
|
|
266
|
+
leaves the report naming no key.
|
|
267
|
+
|
|
268
|
+
A modifier's answer becomes text by that same conversion, so an answer no
|
|
269
|
+
conversion describes is read as missing and reported the same way, and the
|
|
270
|
+
placeholder takes the chain. An answer that is nothing at all is an absent
|
|
271
|
+
answer rather than one that could not be described: it takes the chain too, and
|
|
272
|
+
says nothing.
|
|
273
|
+
|
|
274
|
+
A modifier reaches the chain by reading its own `defaultValue`, which resolves
|
|
275
|
+
at the moment of that read — running whatever host code the chain carries and
|
|
276
|
+
reporting a link it cannot describe. A generic copy of a modifier's config is
|
|
277
|
+
such a read, so a rest destructure, a spread or `JSON.stringify` walks a chain
|
|
278
|
+
that a modifier taking the keys it needs by name leaves alone.
|
|
279
|
+
|
|
280
|
+
Formatting options are keyed by modifier name, and their layers compose per
|
|
281
|
+
property: the parser's `modifierDefaults`, then the `props` the call passes,
|
|
282
|
+
then the wrapper's own `props`. Each layer overrides only the properties it
|
|
283
|
+
names, so a layer cannot reset an earlier one: a property set to `undefined`
|
|
284
|
+
names nothing, where one set to the host's null is named and null — a value,
|
|
285
|
+
like zero — is what the modifier is handed. Only what a layer owns composes,
|
|
286
|
+
and only under the name the placeholder wrote: what a layer holds under other
|
|
287
|
+
names is not read for it. The object a modifier is handed owns every entry it
|
|
288
|
+
is configured with and carries no prototype, so a prototype somebody else wrote
|
|
289
|
+
to configures nothing.
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
modifierDefaults { number: { maximumFractionDigits: 4, useGrouping: false } }
|
|
293
|
+
call props { number: { useGrouping: true } }
|
|
294
|
+
wrapper props { number: { maximumFractionDigits: 1 } }
|
|
295
|
+
effective { maximumFractionDigits: 1, useGrouping: true } -> 1,234.6
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
A modifier is handed that composition under its own name and nothing else — the
|
|
299
|
+
`effective` line is what `number` reads — so what one modifier is configured
|
|
300
|
+
with never reaches the next, and a modifier nobody configured is handed an empty
|
|
301
|
+
object rather than nothing. A modifier a host registers reads its properties the
|
|
302
|
+
same way, `modifierDefaults` included, and the object it holds is the parser's
|
|
303
|
+
own copy: writing into it reaches neither the next placeholder nor the caller.
|
|
304
|
+
|
|
305
|
+
`number` formats at most two fraction digits when no layer names a maximum.
|
|
306
|
+
That two is a default rather than a cap: a layer naming a
|
|
307
|
+
`minimumFractionDigits` above it widens the default to reach it, the way
|
|
308
|
+
`Intl.NumberFormat` widens its own.
|
|
309
|
+
|
|
310
|
+
`currency` formats in the currency style whatever a layer names as the style:
|
|
311
|
+
that style is the modifier rather than one of the options it layers. It
|
|
312
|
+
multiplies its value by a `ratio` property first, defaulting to 1, so a payload
|
|
313
|
+
carrying minor units renders as major ones.
|
|
314
|
+
|
|
315
|
+
`ago` takes the unit to count in from a `format` property holding a unit name,
|
|
316
|
+
in the singular or the plural: `second`, `minute`, `hour`, `day`, `week`,
|
|
317
|
+
`month` and `year` are the rungs of the ladder it climbs. Its `auto`, which is
|
|
318
|
+
what a layer naming none leaves in place, selects the unit from the magnitude
|
|
319
|
+
of the delta instead. A `format` naming anything else — a unit `Intl` knows and
|
|
320
|
+
this ladder does not climb, a rung spelled in another case — is a property the
|
|
321
|
+
modifier cannot process: the placeholder takes its fallback chain and reports
|
|
322
|
+
`failed-modifier`.
|
|
323
|
+
|
|
324
|
+
`ratio` and `format` are the format's own properties rather than `Intl`'s, and
|
|
325
|
+
both are read from the layers like every other property — a message cannot
|
|
326
|
+
write either as an option.
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
{ v: 2 } { currency: { currency: 'USD', ratio: 100 } } -> $200.00
|
|
330
|
+
{ v: -172800000 } { ago: {} } -> 2 days ago
|
|
331
|
+
{ v: -172800000 } { ago: { format: 'hour' } } -> 48 hours ago
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Escaping
|
|
335
|
+
|
|
336
|
+
The syntax reserves a colon, a semicolon, either brace, a backslash and
|
|
337
|
+
whitespace. A backslash takes the structural meaning away from the character
|
|
338
|
+
that follows it, and the rule is the same everywhere in a message — inside a
|
|
339
|
+
placeholder and in the text around it alike.
|
|
340
|
+
|
|
341
|
+
Whitespace means the twenty-five code points the specification enumerates, not
|
|
342
|
+
whatever the host calls whitespace: a host's own class is defined over a
|
|
343
|
+
Unicode category that has changed membership before.
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
Braces are written \{\{ like this \}\} -> "Braces are written {{ like this }}"
|
|
347
|
+
Hello, {{first\ name; default:Guest}}! -> names the payload key "first name"
|
|
348
|
+
{{count; 1:one\ ; default:none}} -> keeps the trailing space
|
|
349
|
+
C:\\temp -> "C:\temp"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The rule reaches the braces themselves: a brace a backslash consumed is text,
|
|
353
|
+
so it cannot be half of a delimiter. That is what lets a key end in a closing
|
|
354
|
+
brace; one that starts no pair needs no escape.
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
\{{v}} -> "{{v}}" text, whatever the payload carries
|
|
358
|
+
\\{{v}} -> a backslash, then the placeholder {{v}}
|
|
359
|
+
{{v\}} -> "{{v}}" no closing pair, so the whole run is text
|
|
360
|
+
{{v\}}} -> names the payload key "v}"
|
|
361
|
+
{{v\\}} -> names the payload key "v\"
|
|
362
|
+
{{a}b}} -> names the payload key "a}b"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Before anything the syntax does not reserve, a backslash is text itself, so a
|
|
366
|
+
regular expression or a Windows path survives as typed: `\d+` resolves to
|
|
367
|
+
`\d+`, and `C:\Users\name` to `C:\Users\name`.
|
|
368
|
+
|
|
369
|
+
A payload value is read the same way, because a value may carry a placeholder
|
|
370
|
+
of its own. A value that has to keep a backslash in front of a reserved
|
|
371
|
+
character doubles it — `\\server\share` resolves to `\server\share`.
|
|
372
|
+
|
|
373
|
+
Escape sequences are removed once, from the finished text, so the removal
|
|
374
|
+
reaches the text a conversion produced as readily as the text a message was
|
|
375
|
+
written in. The two characters JSON writes for a backslash are an escape
|
|
376
|
+
sequence, and the removal takes one of them, so the JSON a plain object
|
|
377
|
+
serializes to does not necessarily reach the output parsable as JSON.
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
{ v: { a: 'C:\U' } } serializes to {"a":"C:\\U"} and renders {"a":"C:\U"}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
A modifier is unaffected, because it reads its value before the removal runs:
|
|
384
|
+
one that parses a serialized object back reads the serialization the conversion
|
|
385
|
+
produced. A caller that needs the result itself to parse passes the text it
|
|
386
|
+
wants as an ordinary string value, with every backslash doubled.
|
|
387
|
+
|
|
388
|
+
## Status
|
|
389
|
+
|
|
390
|
+
**Unreleased, and the public surface is unstable.**
|
|
391
|
+
|
|
392
|
+
Nothing here references a host framework: `resolve` takes the format's own
|
|
393
|
+
inputs, and an adapter that presents this parser to a host library belongs in
|
|
394
|
+
that host's own repository.
|
|
395
|
+
|
|
396
|
+
This implementation satisfies every conformance level the specification
|
|
397
|
+
defines: **Core**, **Intl** and **Extensions**. Section 2 asks an
|
|
398
|
+
implementation to say so, because a level it does not satisfy changes what a
|
|
399
|
+
message resolves to rather than merely what it can do — without Intl, `number`,
|
|
400
|
+
`date`, `ago` and `currency` are modifier names nobody registered.
|
|
401
|
+
|
|
402
|
+
The specification is normative — where this implementation and the
|
|
403
|
+
specification disagree, this implementation is wrong.
|
|
404
|
+
|
|
405
|
+
## Development
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
npm install
|
|
409
|
+
npm test # builds, typechecks, lints, then runs vitest
|
|
410
|
+
npm run lint:fix # applies what the lint step only reports
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Requires Node.js 22 or newer.
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
[MIT](./LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/// <reference lib="es2020.intl" />
|
|
2
|
+
declare const eq: Modifier.T;
|
|
3
|
+
declare const ne: Modifier.T;
|
|
4
|
+
declare const lt: Modifier.T;
|
|
5
|
+
declare const gt: Modifier.T;
|
|
6
|
+
declare const lte: Modifier.T;
|
|
7
|
+
declare const gte: Modifier.T;
|
|
8
|
+
declare const number: Modifier.T<Modifier.NumberProperties>;
|
|
9
|
+
declare const date: Modifier.T<Modifier.DateProperties>;
|
|
10
|
+
declare const ago: Modifier.T<Modifier.AgoProperties>;
|
|
11
|
+
declare const currency: Modifier.T<Modifier.CurrencyProperties>;
|
|
12
|
+
|
|
13
|
+
declare const modifiers_ago: typeof ago;
|
|
14
|
+
declare const modifiers_currency: typeof currency;
|
|
15
|
+
declare const modifiers_date: typeof date;
|
|
16
|
+
declare const modifiers_eq: typeof eq;
|
|
17
|
+
declare const modifiers_gt: typeof gt;
|
|
18
|
+
declare const modifiers_gte: typeof gte;
|
|
19
|
+
declare const modifiers_lt: typeof lt;
|
|
20
|
+
declare const modifiers_lte: typeof lte;
|
|
21
|
+
declare const modifiers_ne: typeof ne;
|
|
22
|
+
declare const modifiers_number: typeof number;
|
|
23
|
+
declare namespace modifiers {
|
|
24
|
+
export { modifiers_ago as ago, modifiers_currency as currency, modifiers_date as date, modifiers_eq as eq, modifiers_gt as gt, modifiers_gte as gte, modifiers_lt as lt, modifiers_lte as lte, modifiers_ne as ne, modifiers_number as number };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare const AGO_LADDER: readonly [{
|
|
28
|
+
readonly key: "second";
|
|
29
|
+
readonly multiplier: 1000;
|
|
30
|
+
}, {
|
|
31
|
+
readonly key: "minute";
|
|
32
|
+
readonly multiplier: 60;
|
|
33
|
+
}, {
|
|
34
|
+
readonly key: "hour";
|
|
35
|
+
readonly multiplier: 60;
|
|
36
|
+
}, {
|
|
37
|
+
readonly key: "day";
|
|
38
|
+
readonly multiplier: 24;
|
|
39
|
+
}, {
|
|
40
|
+
readonly key: "week";
|
|
41
|
+
readonly multiplier: 7;
|
|
42
|
+
}, {
|
|
43
|
+
readonly key: "month";
|
|
44
|
+
readonly multiplier: number;
|
|
45
|
+
}, {
|
|
46
|
+
readonly key: "year";
|
|
47
|
+
readonly multiplier: 12;
|
|
48
|
+
}];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A locale as it reaches resolution — an opaque identifier the modifiers hand
|
|
52
|
+
* to `Intl`. The format neither parses nor validates it.
|
|
53
|
+
*/
|
|
54
|
+
type Locale = string;
|
|
55
|
+
/**
|
|
56
|
+
* Holds a position out of the inference the surrounding call does. A type
|
|
57
|
+
* parameter is decided where it is declared or where its own argument names it
|
|
58
|
+
* — never by a second position that merely has to agree with it.
|
|
59
|
+
*/
|
|
60
|
+
type Given<T> = [T][T extends any ? 0 : never];
|
|
61
|
+
/**
|
|
62
|
+
* The branches of an all-optional bag that owns at least one of its keys: one
|
|
63
|
+
* per key, each requiring that key be present and leaving the rest optional.
|
|
64
|
+
* A key holding `undefined` is owned; the empty object owns none.
|
|
65
|
+
*/
|
|
66
|
+
type AtLeastOne<T> = {
|
|
67
|
+
[Key in keyof T]-?: Record<Key, T[Key]> & Omit<T, Key>;
|
|
68
|
+
}[keyof T];
|
|
69
|
+
/**
|
|
70
|
+
* A diagnostic the parser hands to its caller. The format does not specify a
|
|
71
|
+
* channel to report through, so the parser writes nowhere itself and describes
|
|
72
|
+
* what happened instead.
|
|
73
|
+
*/
|
|
74
|
+
type Report = {
|
|
75
|
+
/** What stopped resolution. */
|
|
76
|
+
code: 'unknown-modifier' | 'failed-modifier' | 'missing-options' | 'unserializable-value' | 'missing-locale' | 'pass-limit' | 'output-limit';
|
|
77
|
+
/**
|
|
78
|
+
* Which of the three the defect belongs to, and so who fixes it: the message
|
|
79
|
+
* that was written, what the caller passed, or a limit this parser set. Every
|
|
80
|
+
* code declares one. It ranks nothing — a report is no graver for coming from
|
|
81
|
+
* one of the three than from another.
|
|
82
|
+
*/
|
|
83
|
+
origin: 'message' | 'payload' | 'limit';
|
|
84
|
+
/**
|
|
85
|
+
* The same in English. It is self-contained and carries nothing from the
|
|
86
|
+
* payload, so writing it anywhere is safe without further thought.
|
|
87
|
+
*/
|
|
88
|
+
message: string;
|
|
89
|
+
/** The message's own key, where the caller passed one. */
|
|
90
|
+
key?: Parser.Key;
|
|
91
|
+
/** The limit that was reached, where the report is about one. */
|
|
92
|
+
limit?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Where the trouble came from: the placeholder for a report about one, the
|
|
95
|
+
* output that would not settle for the two limits, the message as the caller
|
|
96
|
+
* wrote it where a read of the call's own structure refused — a context
|
|
97
|
+
* entry, an entry of the option bag — and nothing at all where what could
|
|
98
|
+
* not be described is the chain a message itself resolves through. The last
|
|
99
|
+
* two name no placeholder, and a message that is not text carries none of
|
|
100
|
+
* itself either. Truncated — a cut is marked with a trailing `...` of the
|
|
101
|
+
* parser's own — and with its line terminators escaped, so payload content
|
|
102
|
+
* cannot forge a line wherever this is written.
|
|
103
|
+
*/
|
|
104
|
+
text: string;
|
|
105
|
+
};
|
|
106
|
+
declare namespace Modifier {
|
|
107
|
+
export type Key = string;
|
|
108
|
+
export type DefaultKeys = keyof typeof modifiers;
|
|
109
|
+
type AgoStep = (typeof AGO_LADDER)[number]['key'];
|
|
110
|
+
/**
|
|
111
|
+
* A unit `ago` can resolve to, read off the ladder it climbs rather than off
|
|
112
|
+
* `Intl`'s whole vocabulary: a `format` naming a unit the ladder does not
|
|
113
|
+
* climb is one the modifier cannot format with, so the placeholder takes the
|
|
114
|
+
* fallback chain and the failure is reported. Every step is accepted in the
|
|
115
|
+
* plural too, so a layer naming one may spell it either way.
|
|
116
|
+
*/
|
|
117
|
+
export type AgoUnit = AgoStep | `${AgoStep}s`;
|
|
118
|
+
/**
|
|
119
|
+
* What a modifier of this name is handed: the properties composed under it,
|
|
120
|
+
* which is what the layers keyed by that name carry. The `*Props` type
|
|
121
|
+
* beside each is the layer itself — one name, holding those properties.
|
|
122
|
+
*/
|
|
123
|
+
export type AgoProperties = Intl.RelativeTimeFormatOptions & {
|
|
124
|
+
format?: AgoUnit | 'auto';
|
|
125
|
+
};
|
|
126
|
+
export type AgoProps = {
|
|
127
|
+
ago?: AgoProperties;
|
|
128
|
+
};
|
|
129
|
+
export type DateProperties = Intl.DateTimeFormatOptions;
|
|
130
|
+
export type DateProps = {
|
|
131
|
+
date?: DateProperties;
|
|
132
|
+
};
|
|
133
|
+
export type NumberProperties = Intl.NumberFormatOptions;
|
|
134
|
+
export type NumberProps = {
|
|
135
|
+
number?: NumberProperties;
|
|
136
|
+
};
|
|
137
|
+
export type CurrencyProperties = Intl.NumberFormatOptions & {
|
|
138
|
+
ratio?: number;
|
|
139
|
+
};
|
|
140
|
+
export type CurrencyProps = {
|
|
141
|
+
currency?: CurrencyProperties;
|
|
142
|
+
};
|
|
143
|
+
export type DefaultProps = NumberProps & AgoProps & DateProps & CurrencyProps;
|
|
144
|
+
export type Props<T = DefaultProps> = T & DefaultProps;
|
|
145
|
+
export type ModifierOption = Record<'key' | 'value', string>;
|
|
146
|
+
/**
|
|
147
|
+
* A value's own configuration, standing in the payload where the value
|
|
148
|
+
* would. An entry is a wrapper only when it is a plain object that owns at
|
|
149
|
+
* least one key and every key it owns is one of these; an entry with a
|
|
150
|
+
* prototype of its own, one owning anything else, or one owning nothing at
|
|
151
|
+
* all, is a value, wrapper-shaped or not.
|
|
152
|
+
*/
|
|
153
|
+
export type Wrapper<Value = any, CustomModifierProps = DefaultProps> = AtLeastOne<{
|
|
154
|
+
/** The value itself. A wrapper carrying none falls back like a missing key. */
|
|
155
|
+
value?: Value;
|
|
156
|
+
/** Tried before the payload's own `default` and before the inline one. */
|
|
157
|
+
default?: any;
|
|
158
|
+
/** Layered over the `props` the call passes, property by property. */
|
|
159
|
+
props?: Props<CustomModifierProps>;
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* A modifier is handed text and nothing else: a placeholder whose value is
|
|
163
|
+
* absent takes its fallback chain before any modifier is called, and that
|
|
164
|
+
* chain ends in the empty string.
|
|
165
|
+
*
|
|
166
|
+
* `OwnProps` is what this modifier's own name holds, because that
|
|
167
|
+
* composition is what it is handed; `CustomModifierProps` is the table that
|
|
168
|
+
* name sits in, which is what `parserOptions` is read under. A modifier
|
|
169
|
+
* registered through `customModifiers` is given both by the table it is
|
|
170
|
+
* registered in, so only one written down away from its table names them.
|
|
171
|
+
* The empty composition is the default, because that is what a name nobody
|
|
172
|
+
* configured holds.
|
|
173
|
+
*/
|
|
174
|
+
export type T<OwnProps = {}, CustomModifierProps = DefaultProps> = (config: {
|
|
175
|
+
value: string;
|
|
176
|
+
/**
|
|
177
|
+
* The properties composed under this modifier's own name, layered from the
|
|
178
|
+
* implementation defaults up through the wrapper's own and copied, so a
|
|
179
|
+
* modifier that writes into what it was handed reaches neither the next
|
|
180
|
+
* placeholder nor the caller. A modifier nobody configured is handed an
|
|
181
|
+
* empty object.
|
|
182
|
+
*/
|
|
183
|
+
props: OwnProps;
|
|
184
|
+
locale?: Locale;
|
|
185
|
+
parserOptions?: Parser.Options<Modifier.Key, CustomModifierProps>;
|
|
186
|
+
options: ModifierOption[];
|
|
187
|
+
/**
|
|
188
|
+
* The fallback chain, resolved by the read rather than before the modifier
|
|
189
|
+
* was called: reading it walks the wrapper's `default`, then the payload's,
|
|
190
|
+
* then the one the placeholder declared, runs whatever host code those
|
|
191
|
+
* links carry, and reports one no conversion describes. A generic copy of
|
|
192
|
+
* the config is such a read — a rest destructure, a spread,
|
|
193
|
+
* `JSON.stringify` — so a modifier with no use for the default takes the
|
|
194
|
+
* keys it needs by name.
|
|
195
|
+
*/
|
|
196
|
+
defaultValue: string;
|
|
197
|
+
}) => any;
|
|
198
|
+
export type DefaultModifiers = typeof modifiers;
|
|
199
|
+
/**
|
|
200
|
+
* Modifiers by the name each answers to. A name the table declares
|
|
201
|
+
* properties for holds a modifier reading that slice; one it declares none
|
|
202
|
+
* for holds a modifier reading none by name. The slice reaches an entry
|
|
203
|
+
* through the names the table is typed with — the ones the factory infers
|
|
204
|
+
* from the table it is given, or the ones an annotation names — so a table
|
|
205
|
+
* or an option bag annotated without them types every entry, one under a
|
|
206
|
+
* built-in name included, as reading none.
|
|
207
|
+
*/
|
|
208
|
+
export type CustomModifiers<K extends string = Key, ModifierProps = DefaultProps> = {
|
|
209
|
+
[Name in K]: Modifier.T<Name extends keyof Props<ModifierProps> ? NonNullable<Props<ModifierProps>[Name]> : {}, ModifierProps>;
|
|
210
|
+
};
|
|
211
|
+
export { };
|
|
212
|
+
}
|
|
213
|
+
declare namespace Parser {
|
|
214
|
+
type OnReport = (report: Report) => void;
|
|
215
|
+
type Options<Key extends string = Modifier.Key, Props = Modifier.DefaultProps> = {
|
|
216
|
+
/**
|
|
217
|
+
* Modifiers registered by name, over the built-in ones. A name a message
|
|
218
|
+
* may write is one the parser holds a modifier under or one a host
|
|
219
|
+
* registered one under, so an entry that is not a modifier registers none:
|
|
220
|
+
* it takes no name of its own and shadows no built-in. The name then reads
|
|
221
|
+
* as one nobody registered where nothing else answers to it, and answers
|
|
222
|
+
* as it did where a built-in does.
|
|
223
|
+
*/
|
|
224
|
+
customModifiers?: Modifier.CustomModifiers<Key, Props>;
|
|
225
|
+
/**
|
|
226
|
+
* The bottom formatting layer, keyed by modifier name. It carries the same
|
|
227
|
+
* names the call's own `props` does, host-defined modifiers included — a
|
|
228
|
+
* modifier a host can configure per call it can also give defaults.
|
|
229
|
+
*/
|
|
230
|
+
modifierDefaults?: Modifier.Props<Given<Props>>;
|
|
231
|
+
/**
|
|
232
|
+
* Where diagnostics go. Unset, the parser reports nowhere — resolution
|
|
233
|
+
* still fails soft, it just does so silently.
|
|
234
|
+
*/
|
|
235
|
+
onReport?: OnReport;
|
|
236
|
+
};
|
|
237
|
+
type PayloadDefault = {
|
|
238
|
+
[key in 'default']?: any;
|
|
239
|
+
};
|
|
240
|
+
/** What a payload carries under a key: the value, or its configuration. */
|
|
241
|
+
type PayloadEntry<Value = any, Props = Modifier.DefaultProps> = Value | Modifier.Wrapper<Value, Props>;
|
|
242
|
+
/**
|
|
243
|
+
* The values a message's placeholders name, plus `default` — the fallback
|
|
244
|
+
* for every key the payload does not carry.
|
|
245
|
+
*
|
|
246
|
+
* A value reaches a modifier as text: a plain object and an array become
|
|
247
|
+
* JSON, and anything else becomes what the host makes of it. An entry may
|
|
248
|
+
* instead be a `Modifier.Wrapper`, which configures the value it carries.
|
|
249
|
+
*
|
|
250
|
+
* A `Date` loses its sub-second precision to that conversion, and the text
|
|
251
|
+
* `String` writes for one is not numeric, so `number`, `currency`, `ago`,
|
|
252
|
+
* `lt` and `gt` over a `Date` resolve to the fallback chain — the three
|
|
253
|
+
* formatting ones given a locale, because with none they resolve to the
|
|
254
|
+
* empty string whatever the value is. A timestamp or an ISO string keeps
|
|
255
|
+
* both.
|
|
256
|
+
*
|
|
257
|
+
* A value passes through the same unescaping as the message around it: a
|
|
258
|
+
* backslash before a character the syntax reserves — `:`, `;`, `{`, `}`, a
|
|
259
|
+
* backslash, or whitespace — writes that character as text and is dropped
|
|
260
|
+
* itself, while a backslash before anything else is left as it is. So a
|
|
261
|
+
* value holding `\d+` arrives as typed, and one holding `\\server\share`
|
|
262
|
+
* resolves to `\server\share` unless each consumed backslash is doubled.
|
|
263
|
+
*/
|
|
264
|
+
type Payload<T = any, Props = Modifier.DefaultProps> = [Exclude<keyof T, keyof PayloadDefault>] extends [never] ? Record<string, PayloadEntry<any, Props>> & PayloadDefault : {
|
|
265
|
+
[Key in keyof T]: PayloadEntry<T[Key], Props>;
|
|
266
|
+
} & PayloadDefault;
|
|
267
|
+
type Key = string;
|
|
268
|
+
type Value = any;
|
|
269
|
+
/**
|
|
270
|
+
* Everything resolution reads besides the message itself. `key` is the
|
|
271
|
+
* message's own identifier where the caller has one. A missing message
|
|
272
|
+
* resolves to the payload's own `default`, and to `key` where the payload
|
|
273
|
+
* carries none — the same chain a placeholder falls through, one level up.
|
|
274
|
+
*/
|
|
275
|
+
type Context<P = PayloadDefault, M = Modifier.DefaultProps> = {
|
|
276
|
+
payload?: Payload<P, M>;
|
|
277
|
+
props?: Modifier.Props<M>;
|
|
278
|
+
locale?: Locale;
|
|
279
|
+
key?: Key;
|
|
280
|
+
};
|
|
281
|
+
type Resolve<C extends Parser.Context = Parser.Context> = (message: Value, context?: C) => string;
|
|
282
|
+
type T<C extends Parser.Context = Parser.Context> = {
|
|
283
|
+
/**
|
|
284
|
+
* Interpolates the message against the given context and returns the result.
|
|
285
|
+
*/
|
|
286
|
+
resolve: Resolve<C>;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* The payload type comes first: with no host config to carry it, the factory
|
|
290
|
+
* is where a caller declares what its messages expect.
|
|
291
|
+
*/
|
|
292
|
+
type Factory = <Payload = {}, Props = {}, Key extends string = Modifier.Key>(options?: Parser.Options<Key, Props>) => Parser.T<Parser.Context<Payload & PayloadDefault, Props & Modifier.DefaultProps>>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
declare const createParser: Parser.Factory;
|
|
296
|
+
|
|
297
|
+
export { type Locale, Modifier, Parser, type Report, createParser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var Ae=Object.defineProperty;var Oe=(e,n)=>{for(var t in n)Ae(e,t,{get:n[t],enumerable:!0})};var U={};Oe(U,{ago:()=>Ve,currency:()=>$e,date:()=>Se,eq:()=>te,gt:()=>he,gte:()=>De,lt:()=>ge,lte:()=>Ne,ne:()=>ke,number:()=>Le});var le=[`
|
|
2
|
+
`,"\r","\u2028","\u2029"],N=[{key:"second",multiplier:1e3},{key:"minute",multiplier:60},{key:"hour",multiplier:60},{key:"day",multiplier:24},{key:"week",multiplier:7},{key:"month",multiplier:4.333333333333333},{key:"year",multiplier:12}],Z=e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`,G=e=>!/[^\t\n\v\f\r\u0020\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/.test(e),d=(e,n,t)=>{try{return n!==void 0&&e&&Object.prototype.propertyIsEnumerable.call(e,n)?e[n]:void 0}catch{t?.();return}},k=(e,n)=>{if(!e)return[];try{return Object.keys(e)}catch{return n?.(),[]}},M=(e,n,t)=>{let o=Object.create(null);return k(e,t).forEach(r=>{o[r]=d(e,r,t)}),k(n,t).forEach(r=>{let i=d(n,r,t);i!==void 0&&(o[r]=i)}),o},ee=(e,n)=>{let t=Object.create(null);return k(e,n).forEach(o=>{let r=d(e,o,n);typeof r=="function"&&(t[o]=r)}),t},l=class extends Error{constructor(t){super(t);this.code=t}code},pe=e=>{try{return e instanceof l?e.code:void 0}catch{return}},P=e=>{if(typeof e=="string"&&G(e))return;let n=+e;return Number.isFinite(n)?n:void 0},me=e=>{let n=P(e);if(n!==void 0)return n;let t=Date.parse(e);return Number.isNaN(t)?void 0:t};var W=(e,n)=>e?e.value:n.defaultValue,te=e=>{let{value:n,options:t=[]}=e,o=`${n}`.toLowerCase();return W(t.find(({key:r})=>`${r}`.toLowerCase()===o),e)},ke=e=>{let{value:n,options:t=[]}=e,o=`${n}`.toLowerCase();return W(t.find(({key:r})=>`${r}`.toLowerCase()!==o),e)},ye=(e,n)=>e.filter(({key:t})=>!Number.isNaN(+t)).sort((t,o)=>n(+t.key,+o.key)),ge=e=>{let{value:n,options:t=[]}=e,o=+n;return W(ye(t,(r,i)=>r-i).find(({key:r})=>o<+r),e)},he=e=>{let{value:n,options:t=[]}=e,o=+n;return W(ye(t,(r,i)=>i-r).find(({key:r})=>o>+r),e)},Ne=e=>te({value:e.value,props:e.props,options:e.options,get defaultValue(){return ge(e)}}),De=e=>te({value:e.value,props:e.props,options:e.options,get defaultValue(){return he(e)}}),D=e=>{if(e===void 0)throw new l("failed-modifier");return e},Le=e=>{let{value:n,props:t,locale:o=""}=e;if(!o)throw new l("missing-locale");let r=D(P(n)),i=Number(d(t,"minimumFractionDigits"))||0,s=K(d(t,"maximumFractionDigits"),Math.max(i,2));return new Intl.NumberFormat(o,M(t,{maximumFractionDigits:s})).format(r)},Se=e=>{let{value:n,props:t,locale:o=""}=e;if(!o)throw new l("missing-locale");let r=D(me(n));return new Intl.DateTimeFormat(o,M(t,void 0)).format(r)},K=(e,n)=>e===void 0?n:e,ne=(e="",n="")=>new RegExp(`^${e}s?$`).test(n),je=e=>e==="auto"||N.some(({key:n})=>ne(n,e)),Ce=e=>N.indexOf(N.find(({key:n})=>ne(n,e))),_e=e=>Math.sign(e)*Math.round(Math.abs(e)),Fe=(e,n)=>N.reduce(([t,o],{key:r,multiplier:i},s)=>{if(ne(o,n))return[t,o];if(!o||s===Ce(o)+1){let a=_e(t/i);if(!o||Math.abs(a)>=1||n!=="auto")return[a,r]}return[t,o]},[e,""]),Ve=e=>{let{value:n,locale:t="",props:o}=e;if(!t)throw new l("missing-locale");let r=D(P(n)),i=K(d(o,"numeric"),"auto"),s=K(d(o,"format"),"auto");if(!je(s))throw new l("failed-modifier");let a=Fe(r,s);return new Intl.RelativeTimeFormat(t,M(o,{numeric:i})).format(...a)},$e=e=>{let{value:n,locale:t="",props:o}=e;if(!t)throw new l("missing-locale");let r=D(P(n)),i=D(P(r*K(d(o,"ratio"),1)));return new Intl.NumberFormat(t,M(o,{style:"currency"})).format(i)};var Re=`[${le.map(Z).join("")}]`,be=new RegExp(Re),ve=new RegExp(Re,"g"),ze=(e,n)=>{for(let t=n+2;t<e.length;t+=1){let o=e[t];if(o==="\\"){if(be.test(e.charAt(t+1)))return;t+=1;continue}if(be.test(o)||o==="{"&&e.charAt(t+1)==="{")return;if(o==="}"&&e.charAt(t+1)==="}")return t+2}},re=(e,n)=>{for(let t=n;t<e.length;t+=1){if(e[t]==="\\"){t+=1;continue}if(e[t]!=="{"||e.charAt(t+1)!=="{")continue;let o=ze(e,t);if(o!==void 0)return[t,o]}},Ge=e=>typeof e=="string"&&!!re(e,0),Ke=/[:;{}\\]/,q=e=>typeof e=="string"?e.replace(/\\([\s\S])/g,(n,t)=>Ke.test(t)||G(t)?t:n):e,L=e=>{let n=-1,t=0;for(let o=0;o<e.length;o+=1){let r=e[o]==="\\"&&o+1<e.length;!r&&G(e[o])||(n<0&&(n=o),o+=r?1:0,t=o+1)}return n<0?"":e.slice(n,t)},oe=(e,n)=>{let t=[],o=0;for(let r=0;r<e.length;r+=1)e[r]==="\\"?r+=1:e[r]===n&&(t.push(e.slice(o,r)),o=r+1);return[...t,e.slice(o)]},Ee=e=>{if(!e||typeof e!="object")return!1;try{let n=Object.getPrototypeOf(e);return n===Object.prototype||n===null}catch{return!1}},We=e=>{let n=I;try{let t=JSON.stringify(e,(o,r)=>{if(n-=1,n<0)throw new RangeError("The value visits more nodes than a resolvable output can hold.");return r});return typeof t=="string"?t:void 0}catch{return}},Me=e=>{try{if(!Ee(e)&&!Array.isArray(e))return String(e)}catch{return}return We(e)},B=(e,n)=>e===void 0?void 0:e!==null&&(typeof e=="object"||typeof e=="function"||typeof e=="bigint")?(n.has(e)||n.set(e,Me(e)),n.get(e)):Me(e),ie=(e,n,t)=>{let o=B(e,t);return e!==void 0&&o===void 0&&n(),o},Ue=["value","default","props"],qe=(e,n,t)=>{if(!Ee(e))return!1;if(!n.has(e)){let r=!1,i=k(e,()=>{r=!0});n.set(e,r?void 0:!!i.length&&i.every(s=>Ue.includes(s)))}let o=n.get(e);return o===void 0&&t?.(),!!o},we=e=>!!e&&(typeof e=="object"||typeof e=="function"),Xe=(e,n,t)=>e.reduce((o,r)=>{let i=we(r)?d(r,n,t):void 0;return we(i)?M(o,i,t):o},Object.create(null)),Be=["eq","ne","lt","gt","lte","gte"],He=(e,n)=>Be.includes(e)&&n[e]===U[e],Je=({value:e,props:n,payload:t,parserOptions:o,modifiers:r,modifierDefaults:i,onReport:s,locale:a,key:c,conversions:w,wrappers:R})=>{let f=Object.keys(r),E=m=>{let[A,...S]=oe(m.slice(2,-2),";"),[j,...C]=oe(A,":"),de=L(j),_=de?q(de):void 0,y=()=>h("unserializable-value",m,c,s),H=d(t,_,y),F=_!=="default"&&qe(H,R,y)?H:void 0,Te=F?d(F,"value",y):H,V=[],J;S.forEach(g=>{let[b,...ce]=oe(g,":"),z=q(L(b)),fe=ce.length?L(ce.join(":")):L(b);z&&(J===void 0&&z==="default"&&(J=fe),z!=="default"&&V.push({key:z,value:fe}))});let Y=g=>ie(g,y,w),$=Y(Te),ue,Pe=_==="default"?()=>$:()=>Y(d(t,"default",y)),O=()=>(ue??=[()=>Y(d(F,"default",y)),Pe].reduce((g,b)=>g??b(),void 0)??J??"",ue),v=q(L(C.join(":"))),Q=!!v;if(Q&&!f.includes(v))return h("unknown-modifier",m,c,s),O();if(_!==void 0&&!V.length&&He(v,r)&&h("missing-options",m,c,s),$===void 0)return O();if(!Q&&!V.length)return $;let ae=Q?v:"eq",Ie=r[ae];try{let g=Xe([i,n,d(F,"props",y)],ae,y);return ie(Ie({value:$,options:V,props:g,get defaultValue(){return O()},locale:a,parserOptions:o}),y,w)??O()}catch(g){let b=pe(g)??"failed-modifier";return h(b,m,c,s),b==="missing-locale"?"":O()}},u=`${e}`,x=[],T=0,p=0;for(let m=re(u,p);m;m=re(u,p)){let[A,S]=m;if(A+T>I)return;let j=u.slice(A,S),C=E(j);if(x.push(u.slice(p,A),C),T+=C.length-j.length,p=S,p+T>I)return}return u.length+T>I?void 0:[...x,u.slice(p)].join("")},se=10,xe=ee(U),I=1e5,X=120,Ye=e=>e.slice(0,(e.codePointAt(X-1)??0)>65535?X-1:X),Qe=e=>JSON.stringify(e.length>X?`${Ye(e)}...`:e).slice(1,-1).replace(ve,Z),Ze={"unknown-modifier":"A placeholder named a modifier this parser does not know.","failed-modifier":"A modifier could not produce a result, so the placeholder took its fallback chain.","missing-options":"A comparison was given no options to select from, so the placeholder took its fallback chain.","unserializable-value":"A value could not become text, so resolution read it as missing.","missing-locale":"A formatting modifier was given no locale, so the placeholder resolved to the empty string.","pass-limit":`Interpolation stopped after ${se} passes. A payload value probably references its own placeholder.`,"output-limit":`Interpolation stopped before exceeding ${I} characters. A payload value probably multiplies its own placeholder.`},et={"unknown-modifier":void 0,"failed-modifier":void 0,"missing-options":void 0,"unserializable-value":void 0,"missing-locale":void 0,"pass-limit":se,"output-limit":I},tt={"unknown-modifier":"message","failed-modifier":"payload","missing-options":"message","unserializable-value":"payload","missing-locale":"payload","pass-limit":"limit","output-limit":"limit"},h=(e,n,t,o)=>{if(o)try{o({code:e,origin:tt[e],message:Ze[e],key:t,limit:et[e],text:Qe(n)})}catch{}},nt=({value:e,props:n,payload:t,parserOptions:o,modifiers:r,modifierDefaults:i,onReport:s,locale:a,key:c,conversions:w,wrappers:R})=>{let f=e;for(let E=0;Ge(f);E+=1){if(E===se){h("pass-limit",f,c,s);break}let u=Je({value:f,payload:t,props:n,parserOptions:o,modifiers:r,modifierDefaults:i,onReport:s,locale:a,key:c,conversions:w,wrappers:R});if(u===void 0){h("output-limit",f,c,s);break}f=u}return B(q(f),w)??""},dt=e=>({resolve:(n,t)=>{let o=d(e,"onReport"),r=typeof n=="string"?n:"",i=d(t,"key",()=>h("unserializable-value",r,void 0,o)),s=()=>h("unserializable-value",r,i,o),a=d(t,"payload",s),c=d(t,"props",s),w=d(t,"locale",s),R=d(e,"customModifiers",s),f=d(e,"modifierDefaults",s),E=R===void 0?xe:M(xe,ee(R,s)),u=()=>h("unserializable-value","",i,o),x=new Map,T=new Map,p=B(n,x)??ie(d(a,"default",u),u,x);return p===void 0?B(i,x)??"":nt({value:p,payload:a,props:c,parserOptions:e,modifiers:E,modifierDefaults:f,onReport:o,locale:w,key:i,conversions:x,wrappers:T})}});export{dt as createParser};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@curly-message/parser",
|
|
3
|
+
"version": "1.0.0-next.0",
|
|
4
|
+
"description": "JavaScript implementation of the Curly Message Format.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
20
|
+
"pretest": "npm run build && npm run typecheck && npm run lint",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"prepack": "npm run build",
|
|
24
|
+
"lint": "eslint . --max-warnings=0",
|
|
25
|
+
"lint:fix": "eslint --fix .",
|
|
26
|
+
"prepare": "cd .. && simple-git-hooks js/.simple-git-hooks.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+ssh://git@github.com/curly-message/parsers.git",
|
|
34
|
+
"directory": "js"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"curly-message",
|
|
38
|
+
"parser",
|
|
39
|
+
"i18n",
|
|
40
|
+
"internationalization",
|
|
41
|
+
"translations",
|
|
42
|
+
"interpolation"
|
|
43
|
+
],
|
|
44
|
+
"author": "Jarda Svoboda",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/curly-message/parsers/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/curly-message/parsers/tree/main/js#readme",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^10.0.1",
|
|
52
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
53
|
+
"eslint": "^10.8.1",
|
|
54
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
55
|
+
"globals": "^17.11.0",
|
|
56
|
+
"simple-git-hooks": "^2.13.1",
|
|
57
|
+
"tsup": "^8.0.1",
|
|
58
|
+
"typescript": "^5.1.6",
|
|
59
|
+
"typescript-eslint": "^8.67.0",
|
|
60
|
+
"vitest": "^4.1.10"
|
|
61
|
+
}
|
|
62
|
+
}
|