soml-lang 0.0.1 → 0.0.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.
- package/distribution/edit.d.ts +56 -0
- package/distribution/edit.js +635 -0
- package/distribution/error.d.ts +49 -0
- package/distribution/error.js +156 -0
- package/distribution/format.d.ts +62 -0
- package/distribution/format.js +403 -0
- package/distribution/index.d.ts +8 -0
- package/distribution/index.js +10 -0
- package/distribution/parse.d.ts +131 -0
- package/distribution/parse.js +1587 -0
- package/distribution/shared.d.ts +102 -0
- package/distribution/shared.js +285 -0
- package/distribution/stringify.d.ts +115 -0
- package/distribution/stringify.js +473 -0
- package/distribution/tree.d.ts +264 -0
- package/distribution/tree.js +468 -0
- package/package.json +34 -34
- package/readme.md +326 -39
- package/index.d.ts +0 -151
- package/index.js +0 -3
- package/source/error.js +0 -130
- package/source/parse.js +0 -1574
- package/source/shared.js +0 -166
- package/source/stringify.js +0 -439
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { MAX_DEPTH, INT64_MIN, INT64_MAX, MIN_INSTANT, MAX_INSTANT, DURATION_UNITS, trimTrailingZeros, getIntegersOption, requireTemporal, isBareKey, abbreviate, } from "./shared.js";
|
|
2
|
+
const dateTime = Date.prototype.getTime;
|
|
3
|
+
const durationField = (name) => Object.getOwnPropertyDescriptor(Temporal.Duration.prototype, name).get;
|
|
4
|
+
const durationSizes = DURATION_UNITS.values().toArray();
|
|
5
|
+
// Undefined without `Temporal`, and then `requireTemporal()` throws before it is used.
|
|
6
|
+
const temporal = typeof Temporal === 'undefined'
|
|
7
|
+
? undefined
|
|
8
|
+
: {
|
|
9
|
+
instantNanoseconds: Object.getOwnPropertyDescriptor(Temporal.Instant.prototype, 'epochNanoseconds').get,
|
|
10
|
+
durationCalendarFields: ['years', 'months', 'weeks', 'days'].map(name => durationField(name)),
|
|
11
|
+
// In the order of `DURATION_UNITS`.
|
|
12
|
+
durationTimeFields: ['hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'].map(name => durationField(name)),
|
|
13
|
+
durationToString: Temporal.Duration.prototype.toString,
|
|
14
|
+
};
|
|
15
|
+
const HIGH_CODE_UNIT = /[\u{D800}-\u{10FFFF}]/v;
|
|
16
|
+
// eslint-disable-next-line no-control-regex, regexp/no-control-character -- Control characters are exactly what must be found.
|
|
17
|
+
const NEEDS_ESCAPED_STRING = /[\u{0}-\u{1F}'\u{7F}]/v;
|
|
18
|
+
// eslint-disable-next-line no-control-regex, regexp/no-control-character -- As above, plus a lone surrogate, which the `v` flag matches only when it is unpaired.
|
|
19
|
+
const NEEDS_ATTENTION = /[\u{0}-\u{1F}'\u{7F}\u{D800}-\u{DFFF}]/v;
|
|
20
|
+
// eslint-disable-next-line no-control-regex, regexp/no-control-character -- Control characters are exactly what must be escaped.
|
|
21
|
+
const ESCAPED_CHARACTER = /[\u{0}-\u{1F}"\\\u{7F}]/gv;
|
|
22
|
+
// A map rather than an object, so that a property added to `Object.prototype` is never written as an escape.
|
|
23
|
+
const ESCAPES = new Map([
|
|
24
|
+
['\\', '\\\\'],
|
|
25
|
+
['"', String.raw `\"`],
|
|
26
|
+
['\n', String.raw `\n`],
|
|
27
|
+
['\t', String.raw `\t`],
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
Serialize an object or an array to SOML. Nesting is written with braces and tabs, and the output ends with one line feed. Comments, dotted keys, and block strings are never written.
|
|
31
|
+
|
|
32
|
+
Members keep the order of `value`, which reads better in a file for people, and every other rule of canonical form is followed. With the `canonical: true` option, members are sorted by key, and the output is canonical form: two equal values produce the same bytes, so it can be hashed, signed, or compared.
|
|
33
|
+
|
|
34
|
+
JavaScript puts integer-like keys, such as `404` or `10`, before every other key, in numeric order, so their order in `value` cannot be kept.
|
|
35
|
+
|
|
36
|
+
A `bigint` is written as an int and a `number` as a float, so `{port: 8080}` becomes `port: 8080.0`. Use `8080n`, or the `integers: 'number'` option.
|
|
37
|
+
|
|
38
|
+
Besides the types that `parse()` returns, a `Date` is accepted and written as an instant, a `Temporal.Duration` is accepted when it has no years, months, weeks, or days, because those are not a fixed length, and a member whose value is `undefined` is left out, as `JSON.stringify` does.
|
|
39
|
+
|
|
40
|
+
@param value - A plain object or an array.
|
|
41
|
+
@param options - How an int is represented in `value`, and whether to sort members.
|
|
42
|
+
@returns The document, ending with one line feed.
|
|
43
|
+
@throws {TypeError} For a value that cannot be represented: `NaN`, a function, a symbol value, an object that is neither a plain object nor an array (such as a class instance or a `Map`), `undefined` in an array, a circular reference, a non-collection at the top level, an invalid `Date`, a `Temporal.Duration` with years, months, weeks, or days, an object that only claims to be a `Date`, a `Temporal.Instant`, or a `Temporal.Duration`, and a string or key with a lone surrogate or a carriage return. Also when `options` is not an object, its `integers` is not `'bigint'` or `'number'`, or its `canonical` is not a boolean.
|
|
44
|
+
@throws {RangeError} For an int outside the 64-bit range, an instant outside the years 0001 to 9999, a duration outside the 64-bit range of nanoseconds, and nesting deeper than 100 levels.
|
|
45
|
+
|
|
46
|
+
@example
|
|
47
|
+
```
|
|
48
|
+
import {stringify} from 'soml-lang';
|
|
49
|
+
|
|
50
|
+
stringify({name: 'api-gateway', replicas: 3n, timeout: 30});
|
|
51
|
+
//=> "name: 'api-gateway'\nreplicas: 3\ntimeout: 30.0\n"
|
|
52
|
+
|
|
53
|
+
stringify({port: 8080}, {integers: 'number'});
|
|
54
|
+
//=> 'port: 8080\n'
|
|
55
|
+
|
|
56
|
+
stringify({name: 'api', description: 'The edge service'}, {canonical: true});
|
|
57
|
+
//=> "description: 'The edge service'\nname: 'api'\n"
|
|
58
|
+
```
|
|
59
|
+
*/
|
|
60
|
+
// `object` rather than `Record<string, unknown>`, so that a value typed with an interface is accepted too. The value is checked at runtime.
|
|
61
|
+
export function stringify(value, options) {
|
|
62
|
+
if (!Array.isArray(value) && !isPlainObject(value)) {
|
|
63
|
+
throw new TypeError(`The top-level value must be an object or an array, because a document is a collection. Got ${describeType(value)}`);
|
|
64
|
+
}
|
|
65
|
+
return new Writer(getStringifyOptions(options)).writeDocument(value);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
Serialize one value as it is written in a document: a scalar on one line, and an object or an array in braces or brackets over several lines, with each level indented by one more tab. There is no line feed at the end.
|
|
69
|
+
|
|
70
|
+
For showing a value, as in a message, or for writing it into a template. Use `stringify()` to write a whole document, and `edit()` to change a value in one.
|
|
71
|
+
|
|
72
|
+
It takes the same values and options as `stringify()`, and any of them at the top level, so `1.5` gives `'1.5'` and `'it\'s'` gives `"it's"`. Members keep their order, as in `stringify()`, unless the `canonical: true` option sorts them.
|
|
73
|
+
|
|
74
|
+
@param value - A value of any type that `stringify()` accepts in a document.
|
|
75
|
+
@param options - How an int is represented in `value`, and whether to sort members.
|
|
76
|
+
@returns The value as it is written in a document.
|
|
77
|
+
@throws {TypeError} For a value that cannot be represented, as for `stringify()`, and for `undefined`.
|
|
78
|
+
@throws {RangeError} For a value out of range, as for `stringify()`.
|
|
79
|
+
|
|
80
|
+
@example
|
|
81
|
+
```
|
|
82
|
+
import {stringifyValue} from 'soml-lang';
|
|
83
|
+
|
|
84
|
+
stringifyValue(0.1 + 0.2);
|
|
85
|
+
//=> '0.30000000000000004'
|
|
86
|
+
|
|
87
|
+
stringifyValue(8080n);
|
|
88
|
+
//=> '8080'
|
|
89
|
+
|
|
90
|
+
stringifyValue({b: 1n, a: [true]});
|
|
91
|
+
//=> '{\n\tb: 1\n\ta: [\n\t\ttrue\n\t]\n}'
|
|
92
|
+
|
|
93
|
+
stringifyValue({b: 1n, a: [true]}, {canonical: true});
|
|
94
|
+
//=> '{\n\ta: [\n\t\ttrue\n\t]\n\tb: 1\n}'
|
|
95
|
+
```
|
|
96
|
+
*/
|
|
97
|
+
export function stringifyValue(value, options) {
|
|
98
|
+
return new Writer(getStringifyOptions(options)).writeValue(value, 1, '');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
One value as `stringifyValue()` writes it, for `edit()`. A container's own depth is `depth`, and its lines after the first are indented by `indent`, so the value fits where it is written. With `isOneLine`, a container is written on one line instead, as in `[1, {a: 2}]`, for a value inside a container that is on one line.
|
|
102
|
+
|
|
103
|
+
@internal
|
|
104
|
+
*/
|
|
105
|
+
export function stringifyValueAt(value, { depth, indent, isOneLine, ...options }) {
|
|
106
|
+
return new Writer(options, isOneLine).writeValue(value, depth, indent);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
The options of `stringify()`, `stringifyValue()`, and `edit()`, checked, with the defaults for those that are left out.
|
|
110
|
+
|
|
111
|
+
@internal
|
|
112
|
+
*/
|
|
113
|
+
export function getStringifyOptions(options) {
|
|
114
|
+
const integers = getIntegersOption(options);
|
|
115
|
+
const { canonical = false } = (options ?? {});
|
|
116
|
+
if (typeof canonical !== 'boolean') {
|
|
117
|
+
throw new TypeError(`The \`canonical\` option must be a boolean, got ${canonical === null ? 'null' : typeof canonical}`);
|
|
118
|
+
}
|
|
119
|
+
return { integers, canonical };
|
|
120
|
+
}
|
|
121
|
+
/*
|
|
122
|
+
Appends to one array of parts that is joined once at the end, so the time is linear in the output whatever the nesting. A scalar and its line are one part.
|
|
123
|
+
*/
|
|
124
|
+
class Writer {
|
|
125
|
+
#integers;
|
|
126
|
+
#isCanonical;
|
|
127
|
+
#isOneLine;
|
|
128
|
+
#ancestors = new Set();
|
|
129
|
+
#parts = [];
|
|
130
|
+
constructor({ integers, canonical }, isOneLine = false) {
|
|
131
|
+
this.#integers = integers;
|
|
132
|
+
this.#isCanonical = canonical;
|
|
133
|
+
this.#isOneLine = isOneLine;
|
|
134
|
+
}
|
|
135
|
+
/*
|
|
136
|
+
What comes before the item at `index` in a container whose items are indented by `indent`: a comma and a space after the first item on one line, and otherwise the indentation of a new line.
|
|
137
|
+
*/
|
|
138
|
+
#itemPrefix(index, indent) {
|
|
139
|
+
if (this.#isOneLine) {
|
|
140
|
+
return index === 0 ? '' : ', ';
|
|
141
|
+
}
|
|
142
|
+
return indent;
|
|
143
|
+
}
|
|
144
|
+
/*
|
|
145
|
+
Writes `prefix`, the value, and `suffix`. A container's own depth is `depth`, and its lines are indented by `indent`.
|
|
146
|
+
*/
|
|
147
|
+
#writeItem(prefix, value, depth, indent, suffix, key) {
|
|
148
|
+
const scalar = this.#formatScalar(value, key);
|
|
149
|
+
if (scalar === undefined) {
|
|
150
|
+
this.#parts.push(prefix);
|
|
151
|
+
this.#writeContainer(value, depth, indent);
|
|
152
|
+
this.#parts.push(suffix);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.#parts.push(`${prefix}${scalar}${suffix}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/*
|
|
159
|
+
Formats a scalar, or returns `undefined` for an array or a plain object.
|
|
160
|
+
*/
|
|
161
|
+
#formatScalar(value, key) {
|
|
162
|
+
switch (typeof value) {
|
|
163
|
+
case 'string': {
|
|
164
|
+
return formatString(value, 'string');
|
|
165
|
+
}
|
|
166
|
+
case 'bigint': {
|
|
167
|
+
return formatInteger(value);
|
|
168
|
+
}
|
|
169
|
+
case 'number': {
|
|
170
|
+
return this.#integers === 'number' && Number.isSafeInteger(value) ? formatInteger(BigInt(value)) : formatFloat(value);
|
|
171
|
+
}
|
|
172
|
+
case 'boolean': {
|
|
173
|
+
return value ? 'true' : 'false';
|
|
174
|
+
}
|
|
175
|
+
case 'object': {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
default: {
|
|
179
|
+
throw new TypeError(`Cannot serialize ${describeType(value)}${key === undefined ? '' : ` at key “${abbreviate(key)}”`}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (value === null) {
|
|
183
|
+
return 'null';
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value) || isPlainObject(value)) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
// A brand check rather than `instanceof`, so that an instant or a date from another realm, such as a `vm` context, is accepted too. The tag is only a cheap filter, since `Symbol.toStringTag` can fake it. The getters read internal slots, so they throw for anything else.
|
|
189
|
+
const brand = Object.prototype.toString.call(value);
|
|
190
|
+
if (brand === '[object Temporal.Instant]') {
|
|
191
|
+
requireTemporal();
|
|
192
|
+
return formatInstant(readBranded(temporal.instantNanoseconds, value, 'Temporal.Instant'));
|
|
193
|
+
}
|
|
194
|
+
if (brand === '[object Temporal.Duration]') {
|
|
195
|
+
return formatDuration(value);
|
|
196
|
+
}
|
|
197
|
+
if (brand === '[object Date]') {
|
|
198
|
+
const milliseconds = readBranded(dateTime, value, 'Date');
|
|
199
|
+
if (Number.isNaN(milliseconds)) {
|
|
200
|
+
throw new TypeError('Cannot serialize an invalid Date');
|
|
201
|
+
}
|
|
202
|
+
return formatInstant(BigInt(milliseconds) * 1000000n);
|
|
203
|
+
}
|
|
204
|
+
throw new TypeError(`Cannot serialize ${describeType(value)}. Only plain objects, arrays, and the scalar types can be serialized`);
|
|
205
|
+
}
|
|
206
|
+
#enter(value, depth) {
|
|
207
|
+
if (depth > MAX_DEPTH) {
|
|
208
|
+
throw new RangeError(`Cannot serialize a value nested more than ${MAX_DEPTH} levels deep`);
|
|
209
|
+
}
|
|
210
|
+
if (this.#ancestors.has(value)) {
|
|
211
|
+
throw new TypeError('Cannot serialize a circular structure');
|
|
212
|
+
}
|
|
213
|
+
this.#ancestors.add(value);
|
|
214
|
+
}
|
|
215
|
+
#writeContainer(value, depth, indent) {
|
|
216
|
+
this.#enter(value, depth);
|
|
217
|
+
const parts = this.#parts;
|
|
218
|
+
const innerIndent = `${indent}\t`;
|
|
219
|
+
const lineEnd = this.#isOneLine ? '' : '\n';
|
|
220
|
+
const closingIndent = this.#isOneLine ? '' : indent;
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
// The length is read once, as `JSON.stringify()` does, so a getter that adds items cannot make it go on forever.
|
|
223
|
+
const { length } = value;
|
|
224
|
+
if (length === 0) {
|
|
225
|
+
parts.push('[]');
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
parts.push(`[${lineEnd}`);
|
|
229
|
+
for (let index = 0; index < length; index++) {
|
|
230
|
+
const item = value[index];
|
|
231
|
+
// A hole reads as `undefined`, and both would otherwise have to be guessed into something.
|
|
232
|
+
if (item === undefined) {
|
|
233
|
+
throw new TypeError(`Cannot serialize undefined in an array, at index ${index}`);
|
|
234
|
+
}
|
|
235
|
+
this.#writeItem(this.#itemPrefix(index, innerIndent), item, depth + 1, innerIndent, lineEnd);
|
|
236
|
+
}
|
|
237
|
+
parts.push(`${closingIndent}]`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const members = readMembers(value, this.#isCanonical);
|
|
242
|
+
if (members.length === 0) {
|
|
243
|
+
parts.push('{}');
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
parts.push(`{${lineEnd}`);
|
|
247
|
+
this.#writeMembers(members, depth + 1, innerIndent);
|
|
248
|
+
parts.push(`${closingIndent}}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.#ancestors.delete(value);
|
|
252
|
+
}
|
|
253
|
+
#writeMembers(members, depth, indent) {
|
|
254
|
+
const lineEnd = this.#isOneLine ? '' : '\n';
|
|
255
|
+
for (const [index, [key, member]] of members.entries()) {
|
|
256
|
+
this.#writeItem(`${this.#itemPrefix(index, indent)}${formatKey(key)}: `, member, depth, indent, lineEnd, key);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
writeValue(value, depth, indent) {
|
|
260
|
+
this.#writeItem('', value, depth, indent, '');
|
|
261
|
+
return this.#parts.join('');
|
|
262
|
+
}
|
|
263
|
+
writeDocument(value) {
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
this.#writeContainer(value, 1, '');
|
|
266
|
+
this.#parts.push('\n');
|
|
267
|
+
return this.#parts.join('');
|
|
268
|
+
}
|
|
269
|
+
const members = readMembers(value, this.#isCanonical);
|
|
270
|
+
// A brace-less object needs at least one entry, so the empty object keeps its braces, like an array. It is written from the members already read, because reading them again would call each getter twice, and one that returns a value the second time would give a braced object that is not canonical.
|
|
271
|
+
if (members.length === 0) {
|
|
272
|
+
this.#parts.push('{}\n');
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
this.#enter(value, 1);
|
|
276
|
+
this.#writeMembers(members, 2, '');
|
|
277
|
+
}
|
|
278
|
+
return this.#parts.join('');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/*
|
|
282
|
+
The members to write, with each value read once, sorted by key when `isCanonical` is true and otherwise in the object's own order. A member whose value is `undefined` is left out, as `JSON.stringify` does.
|
|
283
|
+
*/
|
|
284
|
+
function readMembers(object, isCanonical) {
|
|
285
|
+
const keys = Object.keys(object);
|
|
286
|
+
if (isCanonical) {
|
|
287
|
+
// The default sort compares UTF-16 code units, which matches code point order unless a key has a code unit from U+D800 up.
|
|
288
|
+
if (keys.some(key => HIGH_CODE_UNIT.test(key))) {
|
|
289
|
+
keys.sort(compareKeys);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
keys.sort(); // The native string order is the point, and it is much faster than a comparator.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const members = [];
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
const value = object[key];
|
|
298
|
+
if (value !== undefined) {
|
|
299
|
+
members.push([key, value]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return members;
|
|
303
|
+
}
|
|
304
|
+
function isPlainObject(value) {
|
|
305
|
+
if (typeof value !== 'object' || value === null) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const prototype = Object.getPrototypeOf(value);
|
|
309
|
+
return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null;
|
|
310
|
+
}
|
|
311
|
+
function describeType(value) {
|
|
312
|
+
if (value === null) {
|
|
313
|
+
return 'null';
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === 'function') {
|
|
316
|
+
return 'a function';
|
|
317
|
+
}
|
|
318
|
+
if (typeof value === 'object') {
|
|
319
|
+
const name = value.constructor?.name;
|
|
320
|
+
return Array.isArray(value) ? 'an array' : (name === undefined || name === '' ? 'an object' : `an instance of ${name}`);
|
|
321
|
+
}
|
|
322
|
+
return value === undefined ? 'undefined' : `a ${typeof value}`;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
Compare two keys in canonical order, which is by their Unicode scalar values, as `stringify()` sorts members with the `canonical: true` option. For a sort, such as in a lint rule that keeps the members of an object in canonical order.
|
|
326
|
+
|
|
327
|
+
The default `Array#sort()` compares UTF-16 code units, which puts U+E000 to U+FFFF after every character above U+FFFF, so it differs from canonical order for keys that hold such characters.
|
|
328
|
+
|
|
329
|
+
@param left - A key.
|
|
330
|
+
@param right - Another key.
|
|
331
|
+
@returns A negative number when `left` comes first, a positive number when `right` comes first, and `0` when they are equal.
|
|
332
|
+
|
|
333
|
+
@example
|
|
334
|
+
```
|
|
335
|
+
import {compareKeys} from 'soml-lang';
|
|
336
|
+
|
|
337
|
+
['b', 'a', '😀', 'ff'].toSorted(compareKeys);
|
|
338
|
+
//=> ['a', 'b', 'ff', '😀']
|
|
339
|
+
```
|
|
340
|
+
*/
|
|
341
|
+
export function compareKeys(left, right) {
|
|
342
|
+
// The code units are compared with the surrogates moved above U+E000 to U+FFFF, which gives code point order.
|
|
343
|
+
const length = Math.min(left.length, right.length);
|
|
344
|
+
for (let index = 0; index < length; index++) {
|
|
345
|
+
let leftCode = left.charCodeAt(index);
|
|
346
|
+
let rightCode = right.charCodeAt(index);
|
|
347
|
+
if (leftCode !== rightCode) {
|
|
348
|
+
if (leftCode >= 0xD8_00 && rightCode >= 0xD8_00) {
|
|
349
|
+
leftCode = toCodePointOrder(leftCode);
|
|
350
|
+
rightCode = toCodePointOrder(rightCode);
|
|
351
|
+
}
|
|
352
|
+
return leftCode - rightCode;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return left.length - right.length;
|
|
356
|
+
}
|
|
357
|
+
/*
|
|
358
|
+
Moves U+E000 to U+FFFF down by 0x800 and the surrogates up by 0x2000, so the surrogates sort last.
|
|
359
|
+
*/
|
|
360
|
+
function toCodePointOrder(code) {
|
|
361
|
+
if (code >= 0xE0_00) {
|
|
362
|
+
return code - 0x8_00;
|
|
363
|
+
}
|
|
364
|
+
return code >= 0xD8_00 ? code + 0x20_00 : code;
|
|
365
|
+
}
|
|
366
|
+
export function formatKey(key) {
|
|
367
|
+
return isBareKey(key) ? key : formatString(key, 'key');
|
|
368
|
+
}
|
|
369
|
+
function formatString(string, kind) {
|
|
370
|
+
// One scan settles the common case: no quote, no control character, and no lone surrogate.
|
|
371
|
+
if (!NEEDS_ATTENTION.test(string)) {
|
|
372
|
+
return `'${string}'`;
|
|
373
|
+
}
|
|
374
|
+
if (!string.isWellFormed()) {
|
|
375
|
+
throw new TypeError(`Cannot serialize a ${kind} containing a lone surrogate, which is not a Unicode scalar value`);
|
|
376
|
+
}
|
|
377
|
+
if (string.includes('\r')) {
|
|
378
|
+
throw new TypeError(`Cannot serialize a ${kind} containing a carriage return (U+000D), which is not representable`);
|
|
379
|
+
}
|
|
380
|
+
return NEEDS_ESCAPED_STRING.test(string) ? `"${string.replaceAll(ESCAPED_CHARACTER, character => ESCAPES.get(character) ?? String.raw `\u{${character.codePointAt(0).toString(16)}}`)}"` : `'${string}'`;
|
|
381
|
+
}
|
|
382
|
+
function formatInteger(value) {
|
|
383
|
+
if (value < INT64_MIN || value > INT64_MAX) {
|
|
384
|
+
throw new RangeError(`Cannot serialize the integer ${value}, because it is outside the 64-bit range`);
|
|
385
|
+
}
|
|
386
|
+
return String(value);
|
|
387
|
+
}
|
|
388
|
+
/*
|
|
389
|
+
The shortest decimal that reads back as the same binary64 value, laid out as ECMAScript `Number::toString` does, which is also RFC 8785's choice. A fractional part is added when there is neither one nor an exponent, so that a float never reads back as an int.
|
|
390
|
+
*/
|
|
391
|
+
function formatFloat(value) {
|
|
392
|
+
if (Number.isNaN(value)) {
|
|
393
|
+
throw new TypeError('Cannot serialize NaN, which is not representable. Use null for a missing value');
|
|
394
|
+
}
|
|
395
|
+
if (value === Infinity) {
|
|
396
|
+
return 'infinity';
|
|
397
|
+
}
|
|
398
|
+
if (value === -Infinity) {
|
|
399
|
+
return '-infinity';
|
|
400
|
+
}
|
|
401
|
+
// Covers -0 too, since zero has one value whatever its sign.
|
|
402
|
+
if (value === 0) {
|
|
403
|
+
return '0.0';
|
|
404
|
+
}
|
|
405
|
+
const text = String(value);
|
|
406
|
+
if (text.includes('e')) {
|
|
407
|
+
return text.replace('e+', 'e');
|
|
408
|
+
}
|
|
409
|
+
return text.includes('.') ? text : `${text}.0`;
|
|
410
|
+
}
|
|
411
|
+
function readBranded(getter, value, name) {
|
|
412
|
+
try {
|
|
413
|
+
return getter.call(value);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
throw new TypeError(`Cannot serialize an object that claims to be a ${name} but is not one`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/*
|
|
420
|
+
The total is computed from the fields as BigInts, because `Temporal.Duration#total()` returns a float, which cannot hold every int64 count of nanoseconds.
|
|
421
|
+
*/
|
|
422
|
+
function formatDuration(duration) {
|
|
423
|
+
requireTemporal();
|
|
424
|
+
const { durationCalendarFields, durationTimeFields, durationToString } = temporal;
|
|
425
|
+
if (durationCalendarFields.some(getter => readBranded(getter, duration, 'Temporal.Duration') !== 0)) {
|
|
426
|
+
throw new TypeError(`Cannot serialize the duration ${durationToString.call(duration)}, because years, months, weeks, and days are not a fixed length. Use hours or smaller units`);
|
|
427
|
+
}
|
|
428
|
+
let nanoseconds = 0n;
|
|
429
|
+
for (const [index, getter] of durationTimeFields.entries()) {
|
|
430
|
+
nanoseconds += BigInt(getter.call(duration)) * durationSizes[index];
|
|
431
|
+
}
|
|
432
|
+
if (nanoseconds < INT64_MIN || nanoseconds > INT64_MAX) {
|
|
433
|
+
throw new RangeError(`Cannot serialize the duration ${durationToString.call(duration)}, because it is outside the 64-bit range of nanoseconds`);
|
|
434
|
+
}
|
|
435
|
+
if (nanoseconds === 0n) {
|
|
436
|
+
return '0s';
|
|
437
|
+
}
|
|
438
|
+
const magnitude = nanoseconds < 0n ? -nanoseconds : nanoseconds;
|
|
439
|
+
const hours = magnitude / 3600000000000n;
|
|
440
|
+
const minutes = (magnitude / 60000000000n) % 60n;
|
|
441
|
+
const seconds = (magnitude / 1000000000n) % 60n;
|
|
442
|
+
const fraction = magnitude % 1000000000n;
|
|
443
|
+
let text = nanoseconds < 0n ? '-' : '';
|
|
444
|
+
if (hours > 0n) {
|
|
445
|
+
text += `${hours}h`;
|
|
446
|
+
}
|
|
447
|
+
if (minutes > 0n) {
|
|
448
|
+
text += `${minutes}m`;
|
|
449
|
+
}
|
|
450
|
+
if (seconds > 0n || fraction > 0n) {
|
|
451
|
+
text += `${seconds}${formatFraction(fraction)}s`;
|
|
452
|
+
}
|
|
453
|
+
return text;
|
|
454
|
+
}
|
|
455
|
+
/*
|
|
456
|
+
`Date#toISOString()` writes the date and the time of day exactly for every instant that a `Date` or a `Temporal.Instant` can hold, so a `Date` needs no `Temporal`. Only the fraction comes from the nanoseconds.
|
|
457
|
+
*/
|
|
458
|
+
function formatInstant(nanoseconds) {
|
|
459
|
+
// The fraction is never negative, so an instant before 1970 is a whole second plus a fraction, as it is written.
|
|
460
|
+
const fraction = ((nanoseconds % 1000000000n) + 1000000000n) % 1000000000n;
|
|
461
|
+
const milliseconds = Number((nanoseconds - fraction) / 1000000n);
|
|
462
|
+
const text = `${new Date(milliseconds).toISOString().slice(0, -'.000Z'.length)}${formatFraction(fraction)}Z`;
|
|
463
|
+
if (nanoseconds < MIN_INSTANT || nanoseconds > MAX_INSTANT) {
|
|
464
|
+
throw new RangeError(`Cannot serialize the instant ${text}, because it is outside the years 0001 to 9999`);
|
|
465
|
+
}
|
|
466
|
+
return text;
|
|
467
|
+
}
|
|
468
|
+
/*
|
|
469
|
+
The fraction of a second, from its nanoseconds, as canonical form writes it for an instant and a duration: nothing when it is zero, and otherwise a `.` and nine digits with the trailing zeros removed.
|
|
470
|
+
*/
|
|
471
|
+
function formatFraction(nanoseconds) {
|
|
472
|
+
return nanoseconds === 0n ? '' : `.${trimTrailingZeros(String(nanoseconds).padStart(9, '0'))}`;
|
|
473
|
+
}
|