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.
@@ -1,4 +1,4 @@
1
- import { MAX_DEPTH, INT64_MIN, INT64_MAX, MIN_INSTANT, MAX_INSTANT, DURATION_UNITS, trimTrailingZeros, validateIntegersOption, requireTemporal, isBareKey, abbreviate, } from "./shared.js";
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
- const ESCAPES = {
23
- '\\': '\\\\',
24
- '"': String.raw `\"`,
25
- '\n': String.raw `\n`,
26
- '\t': String.raw `\t`,
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 canonical form.
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
- Canonical form is unique for a value: two equal values produce the same bytes, so the output can be hashed, signed, or compared. Members are sorted by key, and comments, dotted keys, and block strings are never written.
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
- A `Date` is accepted and written as an instant. A member whose value is `undefined` is left out.
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 write ints.
39
- @returns The canonical form, ending with one line feed.
40
- @throws {TypeError} When the value contains something that cannot be represented, such as `NaN`, a function, a class instance, an invalid `Date`, a `Temporal.Duration` with years, months, weeks, or days, a circular reference, or a string or key with a lone surrogate or a carriage return. Also when `options.integers` is not `'bigint'` or `'number'`.
41
- @throws {RangeError} When an int is outside the 64-bit range, an instant is outside the years 0001 to 9999, a duration is outside the 64-bit range of nanoseconds, or the value is nested more than 100 levels deep.
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(integers).writeDocument(value);
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 "${abbreviate(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
- if (value.length === 0) {
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('[\n');
151
- for (const [index, item] of value.entries()) {
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, ',\n');
235
+ this.#writeItem(this.#itemPrefix(index, innerIndent), item, depth + 1, innerIndent, lineEnd);
157
236
  }
158
- parts.push(`${indent}]`);
237
+ parts.push(`${closingIndent}]`);
159
238
  }
160
239
  }
161
240
  else {
162
- const members = sortedMembers(value);
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('{\n');
168
- for (const [key, member] of members) {
169
- this.#writeItem(`${innerIndent}${formatKey(key)}: `, member, depth + 1, innerIndent, ',\n', key);
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
- this.#enter(value, 1);
183
- const members = sortedMembers(value);
184
- for (const [key, member] of members) {
185
- this.#writeItem(`${formatKey(key)}: `, member, 2, '', '\n', key);
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
- this.#ancestors.delete(value);
188
- // A brace-less object needs at least one entry, so the empty object keeps its braces.
189
- return members.length === 0 ? '{}\n' : this.#parts.join('');
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, with each value read once. A member whose value is `undefined` is left out, as `JSON.stringify` does.
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 sortedMembers(object) {
284
+ function readMembers(object, isCanonical) {
196
285
  const keys = Object.keys(object);
197
- // The default sort compares UTF-16 code units, which matches code point order unless a key has a code unit from U+D800 up.
198
- if (keys.some(key => HIGH_CODE_UNIT.test(key))) {
199
- keys.sort(compareCodePoints);
200
- }
201
- else {
202
- keys.sort(); // The native string order is the point, and it is much faster than a comparator.
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
- Members are sorted by the Unicode scalar values of their keys. `Array#sort` compares UTF-16 code units, which orders U+E000 to U+FFFF after every astral character, so the code units are compared with surrogates moved above that range.
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 compareCodePoints(left, right) {
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[character] ?? String.raw `\u{${character.codePointAt(0).toString(16)}}`)}"` : `'${string}'`;
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
- // The fraction is written like an instant's: nine digits with the trailing zeros removed.
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
- const instant = new (requireTemporal().Instant)(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`;
355
463
  if (nanoseconds < MIN_INSTANT || nanoseconds > MAX_INSTANT) {
356
- throw new RangeError(`Cannot serialize the instant ${instant.toString()}, because it is outside the years 0001 to 9999`);
464
+ throw new RangeError(`Cannot serialize the instant ${text}, because it is outside the years 0001 to 9999`);
357
465
  }
358
- return instant.toString();
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
  }
@@ -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 `{`, `}`, `[`, `]`, `:`, `,`, and `.`. A `Keyword` is `true`, `false`, or `null`. `infinity` and `-infinity` are `Float` tokens.
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, with more than one segment when it is dotted.
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 | KeySegmentNode | ValueNode;
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. For tools that walk the tree, such as an ESLint language plugin.
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.