soml-lang 0.0.2 → 0.0.4
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 +536 -0
- package/distribution/error.d.ts +19 -2
- package/distribution/error.js +20 -3
- package/distribution/format.d.ts +48 -4
- package/distribution/format.js +209 -49
- package/distribution/index.d.ts +6 -3
- package/distribution/index.js +7 -2
- package/distribution/parse.d.ts +69 -6
- package/distribution/parse.js +603 -227
- package/distribution/shared.d.ts +58 -4
- package/distribution/shared.js +93 -11
- package/distribution/stringify.d.ts +88 -8
- package/distribution/stringify.js +173 -59
- package/distribution/tree.d.ts +111 -9
- package/distribution/tree.js +214 -95
- package/package.json +4 -3
- package/readme.md +256 -50
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_DEPTH, INT64_MIN, INT64_MAX, MIN_INSTANT, MAX_INSTANT, DURATION_UNITS, trimTrailingZeros,
|
|
1
|
+
import { MAX_DEPTH, INT64_MIN, INT64_MAX, MIN_INSTANT, MAX_INSTANT, DURATION_UNITS, trimTrailingZeros, getIntegersOption, requireTemporal, isBareKey, abbreviate, } from "./shared.js";
|
|
2
2
|
const dateTime = Date.prototype.getTime;
|
|
3
3
|
const durationField = (name) => Object.getOwnPropertyDescriptor(Temporal.Duration.prototype, name).get;
|
|
4
4
|
const durationSizes = DURATION_UNITS.values().toArray();
|
|
@@ -19,26 +19,29 @@ const NEEDS_ESCAPED_STRING = /[\u{0}-\u{1F}'\u{7F}]/v;
|
|
|
19
19
|
const NEEDS_ATTENTION = /[\u{0}-\u{1F}'\u{7F}\u{D800}-\u{DFFF}]/v;
|
|
20
20
|
// eslint-disable-next-line no-control-regex, regexp/no-control-character -- Control characters are exactly what must be escaped.
|
|
21
21
|
const ESCAPED_CHARACTER = /[\u{0}-\u{1F}"\\\u{7F}]/gv;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'\
|
|
27
|
-
|
|
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
|
+
]);
|
|
28
29
|
/**
|
|
29
|
-
Serialize an object or an array to
|
|
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 and block strings are never written.
|
|
30
31
|
|
|
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.
|
|
32
35
|
|
|
33
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.
|
|
34
37
|
|
|
35
|
-
|
|
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.
|
|
36
39
|
|
|
37
40
|
@param value - A plain object or an array.
|
|
38
|
-
@param options - How to
|
|
39
|
-
@returns The
|
|
40
|
-
@throws {TypeError}
|
|
41
|
-
@throws {RangeError}
|
|
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.
|
|
42
45
|
|
|
43
46
|
@example
|
|
44
47
|
```
|
|
@@ -46,26 +49,97 @@ import {stringify} from 'soml-lang';
|
|
|
46
49
|
|
|
47
50
|
stringify({name: 'api-gateway', replicas: 3n, timeout: 30});
|
|
48
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"
|
|
49
58
|
```
|
|
50
59
|
*/
|
|
51
60
|
// `object` rather than `Record<string, unknown>`, so that a value typed with an interface is accepted too. The value is checked at runtime.
|
|
52
|
-
export function stringify(value, options
|
|
53
|
-
const { integers = 'bigint' } = options;
|
|
54
|
-
validateIntegersOption(integers);
|
|
61
|
+
export function stringify(value, options) {
|
|
55
62
|
if (!Array.isArray(value) && !isPlainObject(value)) {
|
|
56
63
|
throw new TypeError(`The top-level value must be an object or an array, because a document is a collection. Got ${describeType(value)}`);
|
|
57
64
|
}
|
|
58
|
-
return new Writer(
|
|
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 };
|
|
59
120
|
}
|
|
60
121
|
/*
|
|
61
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.
|
|
62
123
|
*/
|
|
63
124
|
class Writer {
|
|
64
125
|
#integers;
|
|
126
|
+
#isCanonical;
|
|
127
|
+
#isOneLine;
|
|
65
128
|
#ancestors = new Set();
|
|
66
129
|
#parts = [];
|
|
67
|
-
constructor(integers) {
|
|
130
|
+
constructor({ integers, canonical }, isOneLine = false) {
|
|
68
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;
|
|
69
143
|
}
|
|
70
144
|
/*
|
|
71
145
|
Writes `prefix`, the value, and `suffix`. A container's own depth is `depth`, and its lines are indented by `indent`.
|
|
@@ -102,7 +176,7 @@ class Writer {
|
|
|
102
176
|
break;
|
|
103
177
|
}
|
|
104
178
|
default: {
|
|
105
|
-
throw new TypeError(`Cannot serialize ${describeType(value)}${key === undefined ? '' : ` at key
|
|
179
|
+
throw new TypeError(`Cannot serialize ${describeType(value)}${key === undefined ? '' : ` at key “${abbreviate(key)}”`}`);
|
|
106
180
|
}
|
|
107
181
|
}
|
|
108
182
|
if (value === null) {
|
|
@@ -142,64 +216,81 @@ class Writer {
|
|
|
142
216
|
this.#enter(value, depth);
|
|
143
217
|
const parts = this.#parts;
|
|
144
218
|
const innerIndent = `${indent}\t`;
|
|
219
|
+
const lineEnd = this.#isOneLine ? '' : '\n';
|
|
220
|
+
const closingIndent = this.#isOneLine ? '' : indent;
|
|
145
221
|
if (Array.isArray(value)) {
|
|
146
|
-
|
|
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) {
|
|
147
225
|
parts.push('[]');
|
|
148
226
|
}
|
|
149
227
|
else {
|
|
150
|
-
parts.push(
|
|
151
|
-
for (
|
|
228
|
+
parts.push(`[${lineEnd}`);
|
|
229
|
+
for (let index = 0; index < length; index++) {
|
|
230
|
+
const item = value[index];
|
|
152
231
|
// A hole reads as `undefined`, and both would otherwise have to be guessed into something.
|
|
153
232
|
if (item === undefined) {
|
|
154
233
|
throw new TypeError(`Cannot serialize undefined in an array, at index ${index}`);
|
|
155
234
|
}
|
|
156
|
-
this.#writeItem(innerIndent, item, depth + 1, innerIndent,
|
|
235
|
+
this.#writeItem(this.#itemPrefix(index, innerIndent), item, depth + 1, innerIndent, lineEnd);
|
|
157
236
|
}
|
|
158
|
-
parts.push(`${
|
|
237
|
+
parts.push(`${closingIndent}]`);
|
|
159
238
|
}
|
|
160
239
|
}
|
|
161
240
|
else {
|
|
162
|
-
const members =
|
|
241
|
+
const members = readMembers(value, this.#isCanonical);
|
|
163
242
|
if (members.length === 0) {
|
|
164
243
|
parts.push('{}');
|
|
165
244
|
}
|
|
166
245
|
else {
|
|
167
|
-
parts.push(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
parts.push(`${indent}}`);
|
|
246
|
+
parts.push(`{${lineEnd}`);
|
|
247
|
+
this.#writeMembers(members, depth + 1, innerIndent);
|
|
248
|
+
parts.push(`${closingIndent}}`);
|
|
172
249
|
}
|
|
173
250
|
}
|
|
174
251
|
this.#ancestors.delete(value);
|
|
175
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
|
+
}
|
|
176
263
|
writeDocument(value) {
|
|
177
264
|
if (Array.isArray(value)) {
|
|
178
265
|
this.#writeContainer(value, 1, '');
|
|
179
266
|
this.#parts.push('\n');
|
|
180
267
|
return this.#parts.join('');
|
|
181
268
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
this.#
|
|
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');
|
|
186
273
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
274
|
+
else {
|
|
275
|
+
this.#enter(value, 1);
|
|
276
|
+
this.#writeMembers(members, 2, '');
|
|
277
|
+
}
|
|
278
|
+
return this.#parts.join('');
|
|
190
279
|
}
|
|
191
280
|
}
|
|
192
281
|
/*
|
|
193
|
-
The members to write, sorted by key
|
|
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.
|
|
194
283
|
*/
|
|
195
|
-
function
|
|
284
|
+
function readMembers(object, isCanonical) {
|
|
196
285
|
const keys = Object.keys(object);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
keys.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
294
|
}
|
|
204
295
|
const members = [];
|
|
205
296
|
for (const key of keys) {
|
|
@@ -221,9 +312,6 @@ function describeType(value) {
|
|
|
221
312
|
if (value === null) {
|
|
222
313
|
return 'null';
|
|
223
314
|
}
|
|
224
|
-
if (typeof value === 'number' && Number.isNaN(value)) {
|
|
225
|
-
return 'NaN, which is not representable';
|
|
226
|
-
}
|
|
227
315
|
if (typeof value === 'function') {
|
|
228
316
|
return 'a function';
|
|
229
317
|
}
|
|
@@ -233,10 +321,25 @@ function describeType(value) {
|
|
|
233
321
|
}
|
|
234
322
|
return value === undefined ? 'undefined' : `a ${typeof value}`;
|
|
235
323
|
}
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
```
|
|
238
340
|
*/
|
|
239
|
-
function
|
|
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.
|
|
240
343
|
const length = Math.min(left.length, right.length);
|
|
241
344
|
for (let index = 0; index < length; index++) {
|
|
242
345
|
let leftCode = left.charCodeAt(index);
|
|
@@ -260,7 +363,7 @@ function toCodePointOrder(code) {
|
|
|
260
363
|
}
|
|
261
364
|
return code >= 0xD8_00 ? code + 0x20_00 : code;
|
|
262
365
|
}
|
|
263
|
-
function formatKey(key) {
|
|
366
|
+
export function formatKey(key) {
|
|
264
367
|
return isBareKey(key) ? key : formatString(key, 'key');
|
|
265
368
|
}
|
|
266
369
|
function formatString(string, kind) {
|
|
@@ -274,7 +377,7 @@ function formatString(string, kind) {
|
|
|
274
377
|
if (string.includes('\r')) {
|
|
275
378
|
throw new TypeError(`Cannot serialize a ${kind} containing a carriage return (U+000D), which is not representable`);
|
|
276
379
|
}
|
|
277
|
-
return NEEDS_ESCAPED_STRING.test(string) ? `"${string.replaceAll(ESCAPED_CHARACTER, character => ESCAPES
|
|
380
|
+
return NEEDS_ESCAPED_STRING.test(string) ? `"${string.replaceAll(ESCAPED_CHARACTER, character => ESCAPES.get(character) ?? String.raw `\u{${character.codePointAt(0).toString(16)}}`)}"` : `'${string}'`;
|
|
278
381
|
}
|
|
279
382
|
function formatInteger(value) {
|
|
280
383
|
if (value < INT64_MIN || value > INT64_MAX) {
|
|
@@ -345,15 +448,26 @@ function formatDuration(duration) {
|
|
|
345
448
|
text += `${minutes}m`;
|
|
346
449
|
}
|
|
347
450
|
if (seconds > 0n || fraction > 0n) {
|
|
348
|
-
|
|
349
|
-
text += fraction === 0n ? `${seconds}s` : `${seconds}.${trimTrailingZeros(String(fraction).padStart(9, '0'))}s`;
|
|
451
|
+
text += `${seconds}${formatFraction(fraction)}s`;
|
|
350
452
|
}
|
|
351
453
|
return text;
|
|
352
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
|
+
*/
|
|
353
458
|
function formatInstant(nanoseconds) {
|
|
354
|
-
|
|
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`;
|
|
355
463
|
if (nanoseconds < MIN_INSTANT || nanoseconds > MAX_INSTANT) {
|
|
356
|
-
throw new RangeError(`Cannot serialize the instant ${
|
|
464
|
+
throw new RangeError(`Cannot serialize the instant ${text}, because it is outside the years 0001 to 9999`);
|
|
357
465
|
}
|
|
358
|
-
return
|
|
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'))}`;
|
|
359
473
|
}
|
package/distribution/tree.d.ts
CHANGED
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
A position in the source: a 1-based line, and a 0-based column in UTF-16 code units, as in ESTree.
|
|
3
3
|
*/
|
|
4
4
|
export type Position = {
|
|
5
|
+
/**
|
|
6
|
+
The 1-based line.
|
|
7
|
+
*/
|
|
5
8
|
readonly line: number;
|
|
9
|
+
/**
|
|
10
|
+
The 0-based column, in UTF-16 code units.
|
|
11
|
+
*/
|
|
6
12
|
readonly column: number;
|
|
7
13
|
};
|
|
8
14
|
/**
|
|
9
15
|
Where a node, token, or comment is in the source.
|
|
10
16
|
*/
|
|
11
17
|
export type SourceLocation = {
|
|
18
|
+
/**
|
|
19
|
+
The position of the first character.
|
|
20
|
+
*/
|
|
12
21
|
readonly start: Position;
|
|
22
|
+
/**
|
|
23
|
+
The position after the last character.
|
|
24
|
+
*/
|
|
13
25
|
readonly end: Position;
|
|
14
26
|
};
|
|
15
27
|
type Located<Type extends string> = {
|
|
@@ -18,12 +30,15 @@ type Located<Type extends string> = {
|
|
|
18
30
|
The start and end, as UTF-16 offsets into the text.
|
|
19
31
|
*/
|
|
20
32
|
readonly range: readonly [start: number, end: number];
|
|
33
|
+
/**
|
|
34
|
+
The start and end, as lines and columns.
|
|
35
|
+
*/
|
|
21
36
|
readonly loc: SourceLocation;
|
|
22
37
|
};
|
|
23
38
|
/**
|
|
24
39
|
A token. `value` is its source text.
|
|
25
40
|
|
|
26
|
-
A `Punctuator` is one of `{`, `}`, `[`, `]`, `:`,
|
|
41
|
+
A `Punctuator` is one of `{`, `}`, `[`, `]`, `:`, and `,`. A `Keyword` is `true`, `false`, or `null`. `infinity` and `-infinity` are `Float` tokens, and a quoted key is a `String` token.
|
|
27
42
|
*/
|
|
28
43
|
export type Token = Located<'Punctuator' | 'BareKey' | 'String' | 'Integer' | 'Float' | 'Keyword' | 'Instant' | 'Duration'> & {
|
|
29
44
|
readonly value: string;
|
|
@@ -38,6 +53,9 @@ export type Comment = Located<'Line' | 'Block'> & {
|
|
|
38
53
|
The root of a tree.
|
|
39
54
|
*/
|
|
40
55
|
export type DocumentNode = Located<'Document'> & {
|
|
56
|
+
/**
|
|
57
|
+
The document's collection, an object or an array.
|
|
58
|
+
*/
|
|
41
59
|
readonly body: ObjectNode | ArrayNode;
|
|
42
60
|
/**
|
|
43
61
|
Every token, in source order, without comments.
|
|
@@ -48,60 +66,144 @@ export type DocumentNode = Located<'Document'> & {
|
|
|
48
66
|
*/
|
|
49
67
|
readonly comments: readonly Comment[];
|
|
50
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
An object, written with braces or as a top-level object without them.
|
|
71
|
+
*/
|
|
51
72
|
export type ObjectNode = Located<'Object'> & {
|
|
73
|
+
/**
|
|
74
|
+
The members, in source order.
|
|
75
|
+
*/
|
|
52
76
|
readonly members: readonly MemberNode[];
|
|
53
77
|
/**
|
|
54
78
|
`false` only for a top-level object written without braces.
|
|
55
79
|
*/
|
|
56
80
|
readonly braced: boolean;
|
|
57
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
A `key: value` member of an object.
|
|
84
|
+
*/
|
|
58
85
|
export type MemberNode = Located<'Member'> & {
|
|
86
|
+
/**
|
|
87
|
+
The key.
|
|
88
|
+
*/
|
|
59
89
|
readonly key: KeyNode;
|
|
90
|
+
/**
|
|
91
|
+
The value after the `:`.
|
|
92
|
+
*/
|
|
60
93
|
readonly value: ValueNode;
|
|
61
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
An array, with its items in `elements`.
|
|
97
|
+
*/
|
|
62
98
|
export type ArrayNode = Located<'Array'> & {
|
|
99
|
+
/**
|
|
100
|
+
The items, in source order.
|
|
101
|
+
*/
|
|
63
102
|
readonly elements: readonly ValueNode[];
|
|
64
103
|
};
|
|
65
104
|
/**
|
|
66
|
-
A key
|
|
105
|
+
A key.
|
|
67
106
|
*/
|
|
68
107
|
export type KeyNode = Located<'Key'> & {
|
|
69
|
-
readonly segments: readonly KeySegmentNode[];
|
|
70
|
-
};
|
|
71
|
-
export type KeySegmentNode = Located<'KeySegment'> & {
|
|
72
108
|
/**
|
|
73
109
|
The decoded key.
|
|
74
110
|
*/
|
|
75
111
|
readonly value: string;
|
|
112
|
+
/**
|
|
113
|
+
How the key is written: bare, as `'...'`, or as `"..."`.
|
|
114
|
+
*/
|
|
76
115
|
readonly style: 'bare' | 'literal' | 'escaped';
|
|
77
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
A string, written as `'...'`, `"..."`, or a block string.
|
|
119
|
+
*/
|
|
78
120
|
export type StringNode = Located<'String'> & {
|
|
79
121
|
/**
|
|
80
122
|
The decoded string.
|
|
81
123
|
*/
|
|
82
124
|
readonly value: string;
|
|
125
|
+
/**
|
|
126
|
+
Whether the string is written with `'` (literal) or `"` (escaped).
|
|
127
|
+
*/
|
|
83
128
|
readonly style: 'literal' | 'escaped';
|
|
84
129
|
/**
|
|
85
130
|
Whether it is a block string.
|
|
86
131
|
*/
|
|
87
132
|
readonly block: boolean;
|
|
88
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
An int, in any radix.
|
|
136
|
+
*/
|
|
89
137
|
export type IntegerNode = Located<'Integer'> & {
|
|
138
|
+
/**
|
|
139
|
+
The value, which is always a `bigint`.
|
|
140
|
+
*/
|
|
90
141
|
readonly value: bigint;
|
|
142
|
+
/**
|
|
143
|
+
The radix it is written in: `2` for `0b`, `8` for `0o`, `16` for `0x`, and otherwise `10`.
|
|
144
|
+
*/
|
|
91
145
|
readonly radix: 2 | 8 | 10 | 16;
|
|
92
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
A float, including `infinity` and `-infinity`.
|
|
149
|
+
*/
|
|
93
150
|
export type FloatNode = Located<'Float'> & {
|
|
151
|
+
/**
|
|
152
|
+
The value, including `Infinity` and `-Infinity`.
|
|
153
|
+
*/
|
|
94
154
|
readonly value: number;
|
|
95
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
`true` or `false`.
|
|
158
|
+
*/
|
|
96
159
|
export type BooleanNode = Located<'Boolean'> & {
|
|
160
|
+
/**
|
|
161
|
+
The value.
|
|
162
|
+
*/
|
|
97
163
|
readonly value: boolean;
|
|
98
164
|
};
|
|
165
|
+
/**
|
|
166
|
+
The `null` keyword, which has no fields of its own.
|
|
167
|
+
*/
|
|
99
168
|
export type NullNode = Located<'Null'>;
|
|
169
|
+
/**
|
|
170
|
+
An instant, such as `2026-09-19T14:00:00Z`.
|
|
171
|
+
*/
|
|
100
172
|
export type InstantNode = Located<'Instant'> & {
|
|
173
|
+
/**
|
|
174
|
+
Made when it is first read, so that only reading it needs `Temporal`. Spreading or serializing the node reads it too.
|
|
175
|
+
*/
|
|
101
176
|
readonly value: Temporal.Instant;
|
|
102
177
|
};
|
|
178
|
+
/**
|
|
179
|
+
One part of a duration, such as `30m` in `1h30m`.
|
|
180
|
+
*/
|
|
181
|
+
export type DurationPart = {
|
|
182
|
+
/**
|
|
183
|
+
The number as it is written, with any `_` and fraction, such as `1_000` or `1.5`.
|
|
184
|
+
*/
|
|
185
|
+
readonly number: string;
|
|
186
|
+
/**
|
|
187
|
+
The unit.
|
|
188
|
+
*/
|
|
189
|
+
readonly unit: 'h' | 'm' | 's' | 'ms' | 'us' | 'ns';
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
A duration, such as `1h30m`.
|
|
193
|
+
*/
|
|
103
194
|
export type DurationNode = Located<'Duration'> & {
|
|
195
|
+
/**
|
|
196
|
+
Made when it is first read, so that only reading it needs `Temporal`. Spreading or serializing the node reads it too.
|
|
197
|
+
*/
|
|
104
198
|
readonly value: Temporal.Duration;
|
|
199
|
+
/**
|
|
200
|
+
Whether it is written with a `-`, which negates the whole duration.
|
|
201
|
+
*/
|
|
202
|
+
readonly negative: boolean;
|
|
203
|
+
/**
|
|
204
|
+
The parts as they are written, in order. So `-1h30m` is `negative` with the parts `1` `h` and `30` `m`.
|
|
205
|
+
*/
|
|
206
|
+
readonly parts: readonly DurationPart[];
|
|
105
207
|
};
|
|
106
208
|
/**
|
|
107
209
|
A node that is a value.
|
|
@@ -110,9 +212,9 @@ export type ValueNode = ObjectNode | ArrayNode | StringNode | IntegerNode | Floa
|
|
|
110
212
|
/**
|
|
111
213
|
Any node in a tree.
|
|
112
214
|
*/
|
|
113
|
-
export type Node = DocumentNode | MemberNode | KeyNode |
|
|
215
|
+
export type Node = DocumentNode | MemberNode | KeyNode | ValueNode;
|
|
114
216
|
/**
|
|
115
|
-
The properties of each node type that hold its child nodes, in source order
|
|
217
|
+
The properties of each node type that hold its child nodes, in source order, for tools that walk the tree, such as an ESLint language plugin.
|
|
116
218
|
|
|
117
219
|
@example
|
|
118
220
|
```
|
|
@@ -127,9 +229,9 @@ visitorKeys.Integer;
|
|
|
127
229
|
*/
|
|
128
230
|
export declare const visitorKeys: Readonly<Record<Node['type'], readonly string[]>>;
|
|
129
231
|
/**
|
|
130
|
-
Parse a document into a syntax tree, for tools such as linters and formatters.
|
|
232
|
+
Parse a document into a syntax tree, for tools such as linters and formatters. Returns a `Document` node, which also holds every token and comment.
|
|
131
233
|
|
|
132
|
-
Every node, token, and comment has a `range` and a `loc
|
|
234
|
+
Every node, token, and comment has a `range`, which is `[start, end]` as UTF-16 offsets into `text`, and a `loc`, which is `{start: {line, column}, end: {line, column}}`, with a 1-based line and a 0-based column in UTF-16 code units, as in ESTree. A `ParseError` counts its column differently, for people to read, so use its `offset` to find the position in the tree. A scalar node or a key shares its `range` and `loc` with its token, and other nodes, except `Document`, share the positions in `loc` with their first and last token, so treat them as read-only.
|
|
133
235
|
|
|
134
236
|
@param text - The document.
|
|
135
237
|
@returns The root node, which also holds every token and comment.
|