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,3 +1,20 @@
1
+ export declare const TAB = 9;
2
+ export declare const LF = 10;
3
+ export declare const SPACE = 32;
4
+ export declare const DOUBLE_QUOTE = 34;
5
+ export declare const HASH = 35;
6
+ export declare const SINGLE_QUOTE = 39;
7
+ export declare const ASTERISK = 42;
8
+ export declare const COMMA = 44;
9
+ export declare const DASH = 45;
10
+ export declare const DOT = 46;
11
+ export declare const SLASH = 47;
12
+ export declare const COLON = 58;
13
+ export declare const OPEN_BRACKET = 91;
14
+ export declare const BACKSLASH = 92;
15
+ export declare const CLOSE_BRACKET = 93;
16
+ export declare const OPEN_BRACE = 123;
17
+ export declare const CLOSE_BRACE = 125;
1
18
  export declare const MAX_DEPTH = 100;
2
19
  export declare const INT64_MIN: bigint;
3
20
  export declare const INT64_MAX: bigint;
@@ -13,17 +30,38 @@ export declare function trimTrailingZeros(digits: string): string;
13
30
  A duration as a `Temporal.Duration` in hours and smaller units, which hold any int64 count of nanoseconds exactly.
14
31
  */
15
32
  export declare function createDuration(nanoseconds: bigint): Temporal.Duration;
16
- export declare function validateIntegersOption(integers: unknown): asserts integers is 'bigint' | 'number';
33
+ /**
34
+ The `integers` option of `parse()`, `stringify()`, and `edit()`, checked, or its default when it is left out.
35
+ */
36
+ export declare function getIntegersOption(options: unknown): 'bigint' | 'number';
17
37
  /**
18
38
  Whether a UTF-16 code unit can be part of a bare key. `NaN`, which `charCodeAt()` returns past the end of a string, cannot.
19
39
  */
20
40
  export declare function isBareKeyCharacter(code: number): boolean;
21
41
  /**
22
- Whether a key can be written without quotes: one or more ASCII letters, digits, `_`, or `-`.
42
+ Whether a key can be written without quotes, as a bare key: one or more ASCII letters, digits, `_`, or `-`, in any order, so `404` and `-x` are bare keys too. `stringify()` writes such a key bare and quotes every other key.
43
+
44
+ @param key - The decoded key.
45
+ @returns Whether `key` can be a bare key.
46
+
47
+ @example
48
+ ```
49
+ import {isBareKey} from 'soml-lang';
50
+
51
+ isBareKey('content-type');
52
+ //=> true
53
+
54
+ isBareKey('a.b');
55
+ //=> false
56
+ ```
23
57
  */
24
58
  export declare function isBareKey(key: string): boolean;
25
59
  /**
26
- The end of the number, instant, or duration token that starts at `start`, which is a digit or `-`.
60
+ A key as an error message shows it: bare when it can be, and otherwise quoted, and cut short when it is long.
61
+ */
62
+ export declare function describeKey(key: string): string;
63
+ /**
64
+ The end of the number, instant, or duration token that starts at `start`, which is a digit, `-`, or the `i` of `infinity`.
27
65
  */
28
66
  export declare function findNumberEnd(source: string, start: number): number;
29
67
  /**
@@ -38,11 +76,27 @@ export declare function findBlockStringEnd(source: string, offset: number, quote
38
76
  delimiterStart: number;
39
77
  } | undefined;
40
78
  /**
79
+ Whether a UTF-16 code unit is a space or a tab, the whitespace within a line.
80
+ */
81
+ export declare function isSpace(code: number): boolean;
82
+ /**
83
+ The index after the spaces and tabs from `index` on.
84
+ */
85
+ export declare function skipSpaces(text: string, index: number): number;
86
+ /**
87
+ The index after the last character before `index` that is not a space or a tab.
88
+ */
89
+ export declare function skipSpacesBack(text: string, index: number): number;
90
+ /**
91
+ Whether a line in a block string is blank: empty, or only spaces and tabs. A blank line becomes an empty line in the value.
92
+ */
93
+ export declare function isBlankLine(line: string): boolean;
94
+ /**
41
95
  Shorten text quoted in an error message, so a huge token does not make a huge message. The cut never splits a surrogate pair.
42
96
  */
43
97
  export declare function abbreviate(text: string, maximumLength?: number): string;
44
98
  export declare function formatCodePoint(codePoint: number): string;
45
99
  /**
46
- Describe a character for an error message, such as `"$"` or `U+00A0 (NO-BREAK SPACE)`.
100
+ Describe a character for an error message, such as `“$”` or `U+00A0 (NO-BREAK SPACE)`.
47
101
  */
48
102
  export declare function describeCharacter(codePoint: number): string;
@@ -1,4 +1,24 @@
1
1
  /*
2
+ UTF-16 code units of the characters that the lexers look for.
3
+ */
4
+ export const TAB = 0x09;
5
+ export const LF = 0x0A;
6
+ export const SPACE = 0x20;
7
+ export const DOUBLE_QUOTE = 0x22;
8
+ export const HASH = 0x23;
9
+ export const SINGLE_QUOTE = 0x27;
10
+ export const ASTERISK = 0x2A;
11
+ export const COMMA = 0x2C;
12
+ export const DASH = 0x2D;
13
+ export const DOT = 0x2E;
14
+ export const SLASH = 0x2F;
15
+ export const COLON = 0x3A;
16
+ export const OPEN_BRACKET = 0x5B;
17
+ export const BACKSLASH = 0x5C;
18
+ export const CLOSE_BRACKET = 0x5D;
19
+ export const OPEN_BRACE = 0x7B;
20
+ export const CLOSE_BRACE = 0x7D;
21
+ /*
2
22
  The deepest nesting either direction accepts. A recursive-descent parser would otherwise end in an engine stack overflow.
3
23
  */
4
24
  export const MAX_DEPTH = 100;
@@ -10,7 +30,7 @@ The instant range is checked in UTC, so that every instant has a canonical form
10
30
  export const MIN_INSTANT = -62135596800000000000n; // 0001-01-01T00:00:00Z
11
31
  export const MAX_INSTANT = 253402300799999999999n; // 9999-12-31T23:59:59.999999999Z
12
32
  /*
13
- `Temporal` is built into Node.js 26 and later. Before that, only instants and durations need it, so that everything else works without a polyfill. It is read at import, so a polyfill must be loaded before this package.
33
+ `Temporal` is built into Node.js 26 and later. Before that, only the values of instants and durations need it, so that everything else works without a polyfill, including `format()` and `parseTree()`. It is read at import, so a polyfill must be loaded before this package.
14
34
  */
15
35
  const temporal = typeof Temporal === 'undefined' ? undefined : Temporal;
16
36
  export function requireTemporal() {
@@ -54,10 +74,21 @@ export function createDuration(nanoseconds) {
54
74
  const [hours, minutes, seconds, milliseconds, microseconds, nanosecondsPart] = parts;
55
75
  return new (requireTemporal().Duration)(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanosecondsPart);
56
76
  }
57
- export function validateIntegersOption(integers) {
77
+ /**
78
+ The `integers` option of `parse()`, `stringify()`, and `edit()`, checked, or its default when it is left out.
79
+ */
80
+ export function getIntegersOption(options) {
81
+ if (options === undefined) {
82
+ return 'bigint';
83
+ }
84
+ if (typeof options !== 'object' || options === null) {
85
+ throw new TypeError(`The options must be an object, got ${options === null ? 'null' : typeof options}`);
86
+ }
87
+ const { integers = 'bigint' } = options;
58
88
  if (integers !== 'bigint' && integers !== 'number') {
59
89
  throw new TypeError(`The \`integers\` option must be 'bigint' or 'number', got ${typeof integers === 'string' ? `'${abbreviate(integers)}'` : typeof integers}`);
60
90
  }
91
+ return integers;
61
92
  }
62
93
  /*
63
94
  Names for the characters that are invisible or easy to mistake, so an error can say what it found.
@@ -122,7 +153,21 @@ export function isBareKeyCharacter(code) {
122
153
  return code < 128 && bareKeyCharacters[code] === 1;
123
154
  }
124
155
  /**
125
- Whether a key can be written without quotes: one or more ASCII letters, digits, `_`, or `-`.
156
+ Whether a key can be written without quotes, as a bare key: one or more ASCII letters, digits, `_`, or `-`, in any order, so `404` and `-x` are bare keys too. `stringify()` writes such a key bare and quotes every other key.
157
+
158
+ @param key - The decoded key.
159
+ @returns Whether `key` can be a bare key.
160
+
161
+ @example
162
+ ```
163
+ import {isBareKey} from 'soml-lang';
164
+
165
+ isBareKey('content-type');
166
+ //=> true
167
+
168
+ isBareKey('a.b');
169
+ //=> false
170
+ ```
126
171
  */
127
172
  export function isBareKey(key) {
128
173
  if (key === '') {
@@ -136,7 +181,13 @@ export function isBareKey(key) {
136
181
  return true;
137
182
  }
138
183
  /**
139
- The end of the number, instant, or duration token that starts at `start`, which is a digit or `-`.
184
+ A key as an error message shows it: bare when it can be, and otherwise quoted, and cut short when it is long.
185
+ */
186
+ export function describeKey(key) {
187
+ return isBareKey(key) ? abbreviate(key) : JSON.stringify(abbreviate(key));
188
+ }
189
+ /**
190
+ The end of the number, instant, or duration token that starts at `start`, which is a digit, `-`, or the `i` of `infinity`.
140
191
  */
141
192
  export function findNumberEnd(source, start) {
142
193
  let end = start + 1;
@@ -160,10 +211,7 @@ Find the closing delimiter of a block string: the first line from `offset` on th
160
211
  */
161
212
  export function findBlockStringEnd(source, offset, quote, length) {
162
213
  for (let lineStart = offset; lineStart < source.length; lineStart = findLineEnd(source, lineStart) + 1) {
163
- let delimiterStart = lineStart;
164
- while (source.charCodeAt(delimiterStart) === 0x20 || source.charCodeAt(delimiterStart) === 0x09) {
165
- delimiterStart++;
166
- }
214
+ const delimiterStart = skipSpaces(source, lineStart);
167
215
  let runLength = 0;
168
216
  while (source.charCodeAt(delimiterStart + runLength) === quote) {
169
217
  runLength++;
@@ -172,9 +220,43 @@ export function findBlockStringEnd(source, offset, quote, length) {
172
220
  return { lineStart, delimiterStart };
173
221
  }
174
222
  }
223
+ // Past the last line, either because the source ended with a line break or because the last line was content.
175
224
  return undefined;
176
225
  }
177
226
  /**
227
+ Whether a UTF-16 code unit is a space or a tab, the whitespace within a line.
228
+ */
229
+ export function isSpace(code) {
230
+ return code === SPACE || code === TAB;
231
+ }
232
+ /*
233
+ Whitespace within a line is scanned with loops rather than regular expressions such as `/[\t ]*:/`, because a regular expression that backtracks over a run of a few million spaces runs out of stack.
234
+ */
235
+ /**
236
+ The index after the spaces and tabs from `index` on.
237
+ */
238
+ export function skipSpaces(text, index) {
239
+ while (isSpace(text.charCodeAt(index))) {
240
+ index++;
241
+ }
242
+ return index;
243
+ }
244
+ /**
245
+ The index after the last character before `index` that is not a space or a tab.
246
+ */
247
+ export function skipSpacesBack(text, index) {
248
+ while (index > 0 && isSpace(text.charCodeAt(index - 1))) {
249
+ index--;
250
+ }
251
+ return index;
252
+ }
253
+ /**
254
+ Whether a line in a block string is blank: empty, or only spaces and tabs. A blank line becomes an empty line in the value.
255
+ */
256
+ export function isBlankLine(line) {
257
+ return skipSpaces(line, 0) === line.length;
258
+ }
259
+ /**
178
260
  Shorten text quoted in an error message, so a huge token does not make a huge message. The cut never splits a surrogate pair.
179
261
  */
180
262
  export function abbreviate(text, maximumLength = 40) {
@@ -189,7 +271,7 @@ export function formatCodePoint(codePoint) {
189
271
  return `U+${codePoint.toString(16).toUpperCase().padStart(4, '0')}`;
190
272
  }
191
273
  /**
192
- Describe a character for an error message, such as `"$"` or `U+00A0 (NO-BREAK SPACE)`.
274
+ Describe a character for an error message, such as `“$”` or `U+00A0 (NO-BREAK SPACE)`.
193
275
  */
194
276
  export function describeCharacter(codePoint) {
195
277
  const name = CHARACTER_NAMES.get(codePoint);
@@ -198,6 +280,6 @@ export function describeCharacter(codePoint) {
198
280
  return `${formatCodePoint(codePoint)} (${name}${isWhitespace ? '; only space, tab, and line feed are whitespace' : ''})`;
199
281
  }
200
282
  const character = String.fromCodePoint(codePoint);
201
- // Control, format, private-use, unassigned, surrogate, and separator characters are invisible or ambiguous, so they are shown by code point.
202
- return codePoint !== 0x20 && /^[\p{Other}\p{Separator}]$/v.test(character) ? formatCodePoint(codePoint) : `"${character}"`;
283
+ // Control, format, private-use, unassigned, surrogate, separator, and default-ignorable characters are invisible or ambiguous, so they are shown by code point.
284
+ return codePoint !== 0x20 && /^[\p{Default_Ignorable_Code_Point}\p{Other}\p{Separator}]$/v.test(character) ? formatCodePoint(codePoint) : `“${character}”`;
203
285
  }
@@ -1,6 +1,9 @@
1
+ /**
2
+ Options for `stringify()`.
3
+ */
1
4
  export type StringifyOptions = {
2
5
  /**
3
- How an int is represented in the value.
6
+ How an int is represented in `value`.
4
7
 
5
8
  - `'bigint'`: A `bigint` is written as an int, and a `number` is always written as a float, so `8080` becomes `8080.0`.
6
9
  - `'number'`: A `number` that is a safe integer is written as an int, and any other `number` as a float. A `bigint` is still written as an int.
@@ -8,21 +11,42 @@ export type StringifyOptions = {
8
11
  @default 'bigint'
9
12
  */
10
13
  readonly integers?: 'bigint' | 'number';
14
+ /**
15
+ Write exact canonical form, with the members of every object sorted by key, for hashing, signing, or comparing.
16
+
17
+ By default, members keep the order of `value`, which reads better in a file for people, and every other rule of canonical form is followed. 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.
18
+
19
+ @default false
20
+
21
+ @example
22
+ ```
23
+ import {stringify} from 'soml-lang';
24
+
25
+ stringify({name: 'api', description: 'The edge service'});
26
+ //=> "name: 'api'\ndescription: 'The edge service'\n"
27
+
28
+ stringify({name: 'api', description: 'The edge service'}, {canonical: true});
29
+ //=> "description: 'The edge service'\nname: 'api'\n"
30
+ ```
31
+ */
32
+ readonly canonical?: boolean;
11
33
  };
12
34
  /**
13
- Serialize an object or an array to canonical form.
35
+ 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.
14
36
 
15
- 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.
37
+ 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.
38
+
39
+ 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.
16
40
 
17
41
  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.
18
42
 
19
- A `Date` is accepted and written as an instant. A member whose value is `undefined` is left out.
43
+ 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.
20
44
 
21
45
  @param value - A plain object or an array.
22
- @param options - How to write ints.
23
- @returns The canonical form, ending with one line feed.
24
- @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'`.
25
- @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.
46
+ @param options - How an int is represented in `value`, and whether to sort members.
47
+ @returns The document, ending with one line feed.
48
+ @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.
49
+ @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.
26
50
 
27
51
  @example
28
52
  ```
@@ -30,6 +54,62 @@ import {stringify} from 'soml-lang';
30
54
 
31
55
  stringify({name: 'api-gateway', replicas: 3n, timeout: 30});
32
56
  //=> "name: 'api-gateway'\nreplicas: 3\ntimeout: 30.0\n"
57
+
58
+ stringify({port: 8080}, {integers: 'number'});
59
+ //=> 'port: 8080\n'
60
+
61
+ stringify({name: 'api', description: 'The edge service'}, {canonical: true});
62
+ //=> "description: 'The edge service'\nname: 'api'\n"
33
63
  ```
34
64
  */
35
65
  export declare function stringify(value: object, options?: StringifyOptions): string;
66
+ /**
67
+ 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.
68
+
69
+ 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.
70
+
71
+ 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.
72
+
73
+ @param value - A value of any type that `stringify()` accepts in a document.
74
+ @param options - How an int is represented in `value`, and whether to sort members.
75
+ @returns The value as it is written in a document.
76
+ @throws {TypeError} For a value that cannot be represented, as for `stringify()`, and for `undefined`.
77
+ @throws {RangeError} For a value out of range, as for `stringify()`.
78
+
79
+ @example
80
+ ```
81
+ import {stringifyValue} from 'soml-lang';
82
+
83
+ stringifyValue(0.1 + 0.2);
84
+ //=> '0.30000000000000004'
85
+
86
+ stringifyValue(8080n);
87
+ //=> '8080'
88
+
89
+ stringifyValue({b: 1n, a: [true]});
90
+ //=> '{\n\tb: 1\n\ta: [\n\t\ttrue\n\t]\n}'
91
+
92
+ stringifyValue({b: 1n, a: [true]}, {canonical: true});
93
+ //=> '{\n\ta: [\n\t\ttrue\n\t]\n\tb: 1\n}'
94
+ ```
95
+ */
96
+ export declare function stringifyValue(value: unknown, options?: StringifyOptions): string;
97
+ /**
98
+ 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.
99
+
100
+ 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.
101
+
102
+ @param left - A key.
103
+ @param right - Another key.
104
+ @returns A negative number when `left` comes first, a positive number when `right` comes first, and `0` when they are equal.
105
+
106
+ @example
107
+ ```
108
+ import {compareKeys} from 'soml-lang';
109
+
110
+ ['b', 'a', '😀', 'ff'].toSorted(compareKeys);
111
+ //=> ['a', 'b', 'ff', '😀']
112
+ ```
113
+ */
114
+ export declare function compareKeys(left: string, right: string): number;
115
+ export declare function formatKey(key: string): string;