@cplieger/deadset-ts 8.0.0-dev.11 → 8.0.0-dev.13

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.
Files changed (73) hide show
  1. package/README.md +2 -0
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/dist/src/build-scripts.js +6 -13
  4. package/dist/src/commands/read.js +156 -0
  5. package/dist/src/commands/shell.js +75 -0
  6. package/dist/src/commands/tools.js +460 -0
  7. package/dist/src/configuration-files.js +11 -25
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/dependencies.js +67 -115
  10. package/dist/src/load/synthesize.js +12 -5
  11. package/dist/src/manifest.js +27 -14
  12. package/dist/src/roots.js +18 -11
  13. package/dist/src/text/named-files.js +24 -3
  14. package/dist/src/text/yaml.js +464 -173
  15. package/dist/src/vendor/unbash/ansi-c.js +132 -0
  16. package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
  17. package/dist/src/vendor/unbash/arithmetic.js +591 -0
  18. package/dist/src/vendor/unbash/assignment.js +131 -0
  19. package/dist/src/vendor/unbash/chars.js +48 -0
  20. package/dist/src/vendor/unbash/command.js +41 -0
  21. package/dist/src/vendor/unbash/expansion.js +80 -0
  22. package/dist/src/vendor/unbash/glob.js +268 -0
  23. package/dist/src/vendor/unbash/heredoc.js +27 -0
  24. package/dist/src/vendor/unbash/internal-types.js +1 -0
  25. package/dist/src/vendor/unbash/lexer.js +4274 -0
  26. package/dist/src/vendor/unbash/limits.js +3 -0
  27. package/dist/src/vendor/unbash/parser.js +1366 -0
  28. package/dist/src/vendor/unbash/parts.js +57 -0
  29. package/dist/src/vendor/unbash/substitution-context.js +8 -0
  30. package/dist/src/vendor/unbash/traversal.js +212 -0
  31. package/dist/src/vendor/unbash/types.js +1 -0
  32. package/dist/src/vendor/unbash/word-value.js +61 -0
  33. package/dist/src/vendor/unbash/word.js +40 -0
  34. package/dist/src/workflow-steps.js +111 -133
  35. package/jsr.json +2 -1
  36. package/package.json +9 -7
  37. package/src/build-scripts.ts +6 -13
  38. package/src/commands/read.ts +230 -0
  39. package/src/commands/shell.ts +126 -0
  40. package/src/commands/tools.ts +618 -0
  41. package/src/configuration-files.ts +10 -26
  42. package/src/conformance-record.ts +1 -1
  43. package/src/dependencies.ts +99 -123
  44. package/src/load/synthesize.ts +18 -5
  45. package/src/manifest.ts +43 -25
  46. package/src/roots.ts +20 -13
  47. package/src/text/named-files.ts +26 -3
  48. package/src/text/yaml.ts +503 -179
  49. package/src/vendor/unbash/LICENSE +15 -0
  50. package/src/vendor/unbash/VENDORED.md +16 -0
  51. package/src/vendor/unbash/ansi-c.ts +132 -0
  52. package/src/vendor/unbash/arithmetic-parts.ts +53 -0
  53. package/src/vendor/unbash/arithmetic.ts +641 -0
  54. package/src/vendor/unbash/assignment.ts +163 -0
  55. package/src/vendor/unbash/chars.ts +48 -0
  56. package/src/vendor/unbash/command.ts +47 -0
  57. package/src/vendor/unbash/expansion.ts +144 -0
  58. package/src/vendor/unbash/glob.ts +284 -0
  59. package/src/vendor/unbash/heredoc.ts +34 -0
  60. package/src/vendor/unbash/internal-types.ts +94 -0
  61. package/src/vendor/unbash/lexer.ts +4352 -0
  62. package/src/vendor/unbash/limits.ts +3 -0
  63. package/src/vendor/unbash/parser.ts +1612 -0
  64. package/src/vendor/unbash/parts.ts +96 -0
  65. package/src/vendor/unbash/substitution-context.ts +20 -0
  66. package/src/vendor/unbash/traversal.ts +182 -0
  67. package/src/vendor/unbash/types.ts +660 -0
  68. package/src/vendor/unbash/word-value.ts +64 -0
  69. package/src/vendor/unbash/word.ts +50 -0
  70. package/src/workflow-steps.ts +153 -154
  71. package/dist/src/text/shell.js +0 -159
  72. package/src/text/named-files.inputs.json +0 -1522
  73. package/src/text/shell.ts +0 -161
package/src/text/yaml.ts CHANGED
@@ -1,17 +1,24 @@
1
1
  /**
2
- * The value of a YAML configuration document in the subset tool configurations write:
3
- * block mappings and sequences, flow mappings and sequences, and plain, single-quoted and
4
- * double-quoted scalars, with comments. A document using anything else, an anchor, an
5
- * alias, a tag, a block scalar, a complex key or a second document, has no value here,
6
- * as a JSON document that does not parse has none.
2
+ * The value of a YAML document, the one reader of YAML the analysis has, in the subset
3
+ * configurations and workflow files write: block and flow mappings and sequences, plain,
4
+ * quoted and block scalars as [YAML 1.2](https://yaml.org/spec/1.2.2/) decodes them, the core
5
+ * schema's scalars as {@link CORE} and {@link NUMBER} read them, anchors, aliases and comments.
6
+ * A document using anything else, a tag, a complex key, an anchor on a key or a second
7
+ * document, or whose aliases grow it past {@link ALIAS_BUDGET}, has no value here.
7
8
  */
8
9
 
9
10
  import { isRecord } from "../json-document.ts";
10
11
 
11
- /** One line of the document with its comment removed, by its indentation and its content. */
12
+ /** One line of the document with its comment removed, by its indentation, its content and its row. */
12
13
  interface Line {
13
14
  readonly indent: number;
14
15
  readonly text: string;
16
+ /** Its index among the document's lines, blank and comment lines included. */
17
+ readonly row: number;
18
+ /** Whether a tab stands in its indentation, which YAML forbids. */
19
+ readonly tabbed: boolean;
20
+ /** Whether a comment ends it, which ends a plain scalar. */
21
+ readonly commented: boolean;
15
22
  }
16
23
 
17
24
  /** A failure to read the document within the subset; it ends the whole reading. */
@@ -35,7 +42,64 @@ const CORE: ReadonlyMap<string, unknown> = new Map<string, unknown>([
35
42
  ["", null],
36
43
  ]);
37
44
 
38
- const NUMBER = /^[-+]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?$/u;
45
+ /**
46
+ * The core schema's integers and floats, https://yaml.org/spec/1.2.2/#1032-tag-resolution, but
47
+ * `.inf` and `.nan`, which JSON has no value for, so they stay strings.
48
+ */
49
+ const NUMBER = /^(?:[-+]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?|0o[0-7]+|0x[\da-fA-F]+)$/u;
50
+
51
+ /**
52
+ * The size, in nodes and characters, a document's aliases may add to its value. Aliases share
53
+ * the value they name, but a consumer walking or serializing it expands each one, and an alias
54
+ * of a collection of aliases doubles that work at each level; eemeli/yaml's `maxAliasCount`
55
+ * bounds the same: https://eemeli.org/yaml/#options
56
+ */
57
+ const ALIAS_BUDGET = 100_000;
58
+
59
+ /** The escapes of a double-quoted scalar: https://yaml.org/spec/1.2.2/#57-escaped-characters */
60
+ const ESCAPES: ReadonlyMap<string, string> = new Map([
61
+ ["0", "\0"],
62
+ ["a", "\x07"],
63
+ ["b", "\b"],
64
+ ["t", "\t"],
65
+ ["\t", "\t"],
66
+ ["n", "\n"],
67
+ ["v", "\v"],
68
+ ["f", "\f"],
69
+ ["r", "\r"],
70
+ ["e", "\x1b"],
71
+ [" ", " "],
72
+ ['"', '"'],
73
+ ["/", "/"],
74
+ ["\\", "\\"],
75
+ ["N", "\x85"],
76
+ ["_", "\xa0"],
77
+ ["L", "\u2028"],
78
+ ["P", "\u2029"],
79
+ ]);
80
+
81
+ /** The escapes of a double-quoted scalar that a code point's hexadecimal digits follow, with their count. */
82
+ const CODE_ESCAPES: ReadonlyMap<string, number> = new Map([
83
+ ["x", 2],
84
+ ["u", 4],
85
+ ["U", 8],
86
+ ]);
87
+
88
+ /** A block scalar's header, its comment removed: https://yaml.org/spec/1.2.2/#81-block-scalar-styles */
89
+ const BLOCK_HEADER = /^([|>])(?:([1-9])([+-]?)|([+-]?)([1-9]?))$/u;
90
+
91
+ /** An anchor before a node, and the white space after it: https://yaml.org/spec/1.2.2/#692-node-anchors */
92
+ const ANCHOR = /^&([^\s,[\]{}]+)(?:\s+|$)/u;
93
+
94
+ /** An alias node: https://yaml.org/spec/1.2.2/#71-alias-nodes */
95
+ const ALIAS = /^\*([^\s,[\]{}]+)/u;
96
+
97
+ /** The match of `pattern`, written from `^`, at index `at` of `text`, which it does not copy. */
98
+ function matchAt(pattern: RegExp, text: string, at: number): RegExpExecArray | null {
99
+ const sticky = new RegExp(pattern.source.slice(1), "uy");
100
+ sticky.lastIndex = at;
101
+ return sticky.exec(text);
102
+ }
39
103
 
40
104
  /** The text of one line before its comment: a `#` at the line's start or after a blank, outside quotes. */
41
105
  function withoutComment(line: string): string {
@@ -63,23 +127,118 @@ function withoutComment(line: string): string {
63
127
  return line;
64
128
  }
65
129
 
66
- /** The value one scalar's text denotes, quoted or plain. */
67
- function scalar(text: string): unknown {
68
- const value = text.trim();
69
- if (value.startsWith("'")) {
70
- if (!value.endsWith("'") || value.length < 2) {
71
- throw new Unsupported();
130
+ /** The number of spaces a line starts with. */
131
+ function spacesOf(line: string): number {
132
+ return line.length - line.replace(/^ +/u, "").length;
133
+ }
134
+
135
+ function afterBlanks(text: string, at: number): number {
136
+ let end = at;
137
+ while (text[end] === " " || text[end] === "\t") {
138
+ end += 1;
139
+ }
140
+ return end;
141
+ }
142
+
143
+ /** The text one escape of a double-quoted scalar stands for, from its letter on, and its length. */
144
+ function escaped(text: string, at: number): { text: string; length: number } | undefined {
145
+ const letter = text.charAt(at);
146
+ const digits = CODE_ESCAPES.get(letter);
147
+ if (digits === undefined) {
148
+ const one = ESCAPES.get(letter);
149
+ return one === undefined ? undefined : { text: one, length: 1 };
150
+ }
151
+ const code = text.slice(at + 1, at + 1 + digits);
152
+ const point = /^[\da-f]+$/iu.test(code) ? Number.parseInt(code, 16) : Number.NaN;
153
+ return code.length === digits && point <= 0x10ffff
154
+ ? { text: String.fromCodePoint(point), length: 1 + digits }
155
+ : undefined;
156
+ }
157
+
158
+ /**
159
+ * The value of the quoted scalar whose opening quote is at `start` of `text`, and the index
160
+ * after its closing quote, or `undefined` where it does not close or holds an escape YAML does
161
+ * not define. A line break folds as YAML folds it, after the white space before it, which an
162
+ * escape keeps: https://yaml.org/spec/1.2.2/#73-flow-scalar-styles
163
+ */
164
+ function quotedScalar(text: string, start: number): { value: string; end: number } | undefined {
165
+ const quote = text[start];
166
+ let value = "";
167
+ let kept = 0;
168
+ let at = start + 1;
169
+ while (at < text.length) {
170
+ const char = text[at] ?? "";
171
+ if (char === "\n") {
172
+ value = value.slice(0, kept) + value.slice(kept).replace(/[ \t]+$/u, "");
173
+ let breaks = 0;
174
+ for (; text[at] === "\n"; breaks += 1) {
175
+ at = afterBlanks(text, at + 1);
176
+ }
177
+ value += breaks === 1 ? " " : "\n".repeat(breaks - 1);
178
+ } else if (char === quote && !(quote === "'" && text[at + 1] === "'")) {
179
+ return { value, end: at + 1 };
180
+ } else if (char === "'" && quote === "'") {
181
+ value += "'";
182
+ at += 2;
183
+ } else if (char === "\\" && quote === '"' && text[at + 1] === "\n") {
184
+ at = afterBlanks(text, at + 2);
185
+ } else if (char === "\\" && quote === '"') {
186
+ const escape = escaped(text, at + 1);
187
+ if (escape === undefined) {
188
+ return undefined;
189
+ }
190
+ value += escape.text;
191
+ at += 1 + escape.length;
192
+ } else {
193
+ value += char;
194
+ at += 1;
195
+ continue;
196
+ }
197
+ kept = value.length;
198
+ }
199
+ return undefined;
200
+ }
201
+
202
+ /**
203
+ * The value of a scalar's lines, each without its indentation, a blank one empty: a literal
204
+ * block's, `|`, joined by `\n`, and a folded block's, `>`, or a plain one's with each line
205
+ * break folded: https://yaml.org/spec/1.2.2/#813-folded-style
206
+ */
207
+ function foldedLines(lines: readonly string[], kind: "|" | ">" | "plain"): string {
208
+ let value = "";
209
+ let previous: string | undefined;
210
+ let empty = 0;
211
+ for (const line of lines) {
212
+ if (line.trim() === "") {
213
+ empty += previous === undefined && kind === "plain" ? 0 : 1;
214
+ continue;
72
215
  }
73
- return value.slice(1, -1).replaceAll("''", "'");
216
+ const folds =
217
+ kind !== "|" && previous !== undefined && !/^[ \t]/u.test(previous) && !/^[ \t]/u.test(line);
218
+ value +=
219
+ previous === undefined
220
+ ? "\n".repeat(empty)
221
+ : folds && empty === 0
222
+ ? " "
223
+ : "\n".repeat(empty + (folds ? 0 : 1));
224
+ value += line;
225
+ previous = line;
226
+ empty = 0;
74
227
  }
75
- if (value.startsWith('"')) {
76
- try {
77
- return JSON.parse(value) as unknown;
78
- } catch {
228
+ return value;
229
+ }
230
+
231
+ /** The value one scalar's text on one line denotes, quoted or plain. */
232
+ function scalar(text: string): unknown {
233
+ const value = text.trim();
234
+ if (value.startsWith("'") || value.startsWith('"')) {
235
+ const read = quotedScalar(value, 0);
236
+ if (read?.end !== value.length) {
79
237
  throw new Unsupported();
80
238
  }
239
+ return read.value;
81
240
  }
82
- if (/^[&*!|>%@`?]/u.test(value) || value === "---" || value === "...") {
241
+ if (/^[&*!|>%@`?#]/u.test(value) || value === "---" || value === "...") {
83
242
  throw new Unsupported();
84
243
  }
85
244
  if (/:(?:\s|$)/u.test(value)) {
@@ -91,84 +250,32 @@ function scalar(text: string): unknown {
91
250
  return NUMBER.test(value) ? Number(value) : value;
92
251
  }
93
252
 
94
- /** Reads one flow collection or flow scalar from `text` at `at`, answering the value and where it ends. */
95
- function flow(text: string, start: number): { readonly value: unknown; readonly end: number } {
96
- let at = start;
97
- const skip = (): void => {
98
- while (at < text.length && /\s/u.test(text.charAt(at))) {
99
- at += 1;
100
- }
101
- };
102
- skip();
103
- const open = text.charAt(at);
104
- if (open === "[" || open === "{") {
105
- const close = open === "[" ? "]" : "}";
106
- const items: unknown[] = [];
107
- const object: Record<string, unknown> = {};
108
- at += 1;
109
- for (;;) {
110
- skip();
111
- if (text.charAt(at) === close) {
112
- return { value: open === "[" ? items : object, end: at + 1 };
113
- }
114
- const item = flow(text, at);
115
- at = item.end;
116
- skip();
117
- if (open === "{") {
118
- if (text.charAt(at) !== ":" || typeof item.value !== "string") {
119
- throw new Unsupported();
120
- }
121
- const held = flow(text, at + 1);
122
- object[item.value] = held.value;
123
- at = held.end;
124
- skip();
125
- } else {
126
- items.push(item.value);
127
- }
128
- if (text.charAt(at) === ",") {
129
- at += 1;
130
- } else if (text.charAt(at) !== close) {
131
- throw new Unsupported();
132
- }
133
- }
134
- }
135
- if (open === "'" || open === '"') {
136
- let end = at + 1;
137
- for (; end < text.length; end += 1) {
138
- const char = text.charAt(end);
139
- if (open === '"' && char === "\\") {
140
- end += 1;
141
- } else if (char === open && !(open === "'" && text.charAt(end + 1) === "'")) {
142
- break;
143
- } else if (char === open) {
144
- end += 1;
145
- }
146
- }
147
- if (end >= text.length) {
148
- throw new Unsupported();
149
- }
150
- return { value: scalar(text.slice(at, end + 1)), end: end + 1 };
151
- }
152
- let end = at;
153
- while (end < text.length && !",]}".includes(text.charAt(end))) {
154
- if (text.charAt(end) === ":" && /[\s,\]}]/u.test(text.charAt(end + 1) || " ")) {
155
- break;
156
- }
157
- end += 1;
158
- }
159
- return { value: scalar(text.slice(at, end)), end };
253
+ /** Sets `key` of `object` as an own member, so a key such as `__proto__` changes no prototype. */
254
+ function setMember(object: Record<string, unknown>, key: string, value: unknown): void {
255
+ Object.defineProperty(object, key, {
256
+ value,
257
+ enumerable: true,
258
+ writable: true,
259
+ configurable: true,
260
+ });
160
261
  }
161
262
 
162
- /** A value written on one or more lines of a block, read whole: a flow node, else one plain scalar. */
163
- function flowValue(text: string): unknown {
164
- if (!/^["'[{]/u.test(text)) {
165
- return scalar(text);
263
+ /** The size of a value once every value it shares is expanded, each node 1 and each string its length more. */
264
+ function sizeOf(value: unknown, sizes: WeakMap<object, number>): number {
265
+ if (typeof value === "string") {
266
+ return 1 + value.length;
166
267
  }
167
- const read = flow(text, 0);
168
- if (text.slice(read.end).trim() !== "") {
169
- throw new Unsupported();
268
+ if (typeof value !== "object" || value === null) {
269
+ return 1;
170
270
  }
171
- return read.value;
271
+ const known = sizes.get(value);
272
+ if (known !== undefined) {
273
+ return known;
274
+ }
275
+ const children = Array.isArray(value) ? value : Object.entries(value).flat();
276
+ const size = children.reduce<number>((sum, child) => sum + sizeOf(child, sizes), 1);
277
+ sizes.set(value, size);
278
+ return size;
172
279
  }
173
280
 
174
281
  /** The position of a mapping key's `:` in one line's content, or -1 where the content is no mapping entry. */
@@ -193,49 +300,48 @@ function keyEnd(text: string): number {
193
300
  return -1;
194
301
  }
195
302
 
196
- /** How many flow collections one text opens and leaves unclosed, quotes read through. */
197
- function openCollections(text: string): number {
198
- let depth = 0;
199
- let quote: string | undefined;
200
- for (let at = 0; at < text.length; at += 1) {
201
- const char = text.charAt(at);
202
- if (quote !== undefined) {
203
- if (quote === '"' && char === "\\") {
204
- at += 1;
205
- } else if (char === quote) {
206
- quote = undefined;
207
- }
208
- } else if (
209
- (char === "'" || char === '"') &&
210
- SCALAR_START.has(text.slice(0, at).trim().slice(-1))
211
- ) {
212
- quote = char;
213
- } else if (char === "[" || char === "{") {
214
- depth += 1;
215
- } else if (char === "]" || char === "}") {
216
- depth -= 1;
217
- }
218
- }
219
- return depth;
220
- }
221
-
222
303
  /** Whether one line's content is a block sequence item. */
223
304
  function isItem(text: string): boolean {
224
305
  return text === "-" || text.startsWith("- ");
225
306
  }
226
307
 
308
+ /** Whether one line's content opens a block collection: a sequence item or a mapping key. */
309
+ function opensCollection(text: string): boolean {
310
+ return isItem(text) || keyEnd(text) >= 0;
311
+ }
312
+
227
313
  /** Reads the document's lines as one value. */
228
314
  class Reader {
229
315
  private at = 0;
230
316
  private readonly lines: Line[];
317
+ /** Every line of the document, as written. */
318
+ private readonly raw: readonly string[];
319
+ /** The whole document, its lines joined by `\n`, and where each line starts in it. */
320
+ private readonly text: string;
321
+ private readonly starts: readonly number[];
322
+ /** Whether a line break ends the document's last line. */
323
+ private readonly broken: boolean;
324
+ private readonly anchors = new Map<string, unknown>();
325
+ /** The size the aliases read so far add, against {@link ALIAS_BUDGET}. */
326
+ private expanded = 0;
327
+ private readonly sizes = new WeakMap<object, number>();
231
328
 
232
- constructor(lines: Line[]) {
329
+ constructor(lines: Line[], raw: readonly string[], broken: boolean) {
233
330
  this.lines = lines;
331
+ this.raw = raw;
332
+ this.broken = broken;
333
+ this.text = raw.join("\n");
334
+ let start = 0;
335
+ this.starts = raw.map((line) => {
336
+ const here = start;
337
+ start += line.length + 1;
338
+ return here;
339
+ });
234
340
  }
235
341
 
236
342
  /** The whole document's value. */
237
343
  document(): unknown {
238
- const first = this.lines[0];
344
+ const first = this.peek();
239
345
  if (first === undefined) {
240
346
  return null;
241
347
  }
@@ -246,9 +352,38 @@ class Reader {
246
352
  return value;
247
353
  }
248
354
 
355
+ /** The next line to read; one indented with a tab is outside the subset. */
356
+ private peek(): Line | undefined {
357
+ const line = this.lines[this.at];
358
+ if (line?.tabbed === true) {
359
+ throw new Unsupported();
360
+ }
361
+ return line;
362
+ }
363
+
364
+ /** The value the anchor `name` names, its size counted against {@link ALIAS_BUDGET}. */
365
+ private alias(name: string): unknown {
366
+ if (!this.anchors.has(name)) {
367
+ throw new Unsupported();
368
+ }
369
+ const value = this.anchors.get(name);
370
+ this.expanded += sizeOf(value, this.sizes);
371
+ if (this.expanded > ALIAS_BUDGET) {
372
+ throw new Unsupported();
373
+ }
374
+ return value;
375
+ }
376
+
377
+ /** Moves past every line before row `end`. */
378
+ private skipTo(end: number): void {
379
+ while ((this.lines[this.at]?.row ?? end) < end) {
380
+ this.at += 1;
381
+ }
382
+ }
383
+
249
384
  /** The block node whose first line is the next one, at `indent`. */
250
385
  private block(indent: number): unknown {
251
- const line = this.lines[this.at];
386
+ const line = this.peek();
252
387
  if (line === undefined) {
253
388
  return null;
254
389
  }
@@ -258,51 +393,225 @@ class Reader {
258
393
  if (keyEnd(line.text) >= 0) {
259
394
  return this.mapping(indent);
260
395
  }
261
- return this.multiline(indent);
396
+ this.at += 1;
397
+ return this.value(line, line.indent, line.text, indent - 1, false);
262
398
  }
263
399
 
264
- /** A scalar or a flow collection whose text runs over the next lines indented deeper than `indent`. */
265
- private multiline(indent: number): unknown {
266
- const parts: string[] = [];
267
- for (
268
- let line = this.lines[this.at];
269
- line !== undefined && (parts.length === 0 || line.indent > indent);
270
- line = this.lines[this.at]
400
+ /** The node below a key or an item marker with nothing after it: the deeper block, else null. */
401
+ private below(indent: number, sequenceAtIndent: boolean): unknown {
402
+ const next = this.peek();
403
+ if (
404
+ next === undefined ||
405
+ !(next.indent > indent || (sequenceAtIndent && next.indent === indent && isItem(next.text)))
271
406
  ) {
272
- parts.push(line.text);
407
+ return null;
408
+ }
409
+ if (opensCollection(next.text)) {
410
+ return this.block(next.indent);
411
+ }
412
+ this.at += 1;
413
+ return this.value(next, next.indent, next.text, indent, false);
414
+ }
415
+
416
+ /**
417
+ * The node `text` starts, at column `column` of `line`, which is read, after a key or an item
418
+ * marker at `indent`: an alias, a node an anchor names, a block scalar, a quoted scalar, or a
419
+ * flow collection or plain scalar whose text runs over the next lines indented deeper.
420
+ */
421
+ private value(
422
+ line: Line,
423
+ column: number,
424
+ text: string,
425
+ indent: number,
426
+ sequenceAtIndent: boolean,
427
+ ): unknown {
428
+ const alias = ALIAS.exec(text);
429
+ if (alias !== null) {
430
+ const [whole, name = ""] = alias;
431
+ if (whole !== text) {
432
+ throw new Unsupported();
433
+ }
434
+ return this.alias(name);
435
+ }
436
+ const anchor = ANCHOR.exec(text);
437
+ if (anchor !== null) {
438
+ const rest = text.slice(anchor[0].length);
439
+ if (opensCollection(rest)) {
440
+ throw new Unsupported();
441
+ }
442
+ const value =
443
+ rest === ""
444
+ ? this.below(indent, sequenceAtIndent)
445
+ : this.value(line, column + anchor[0].length, rest, indent, sequenceAtIndent);
446
+ this.anchors.set(anchor[1] ?? "", value);
447
+ return value;
448
+ }
449
+ const header = BLOCK_HEADER.exec(text);
450
+ if (header !== null) {
451
+ return this.blockScalar(header, line.row, indent);
452
+ }
453
+ if (/^['"[{]/u.test(text)) {
454
+ const start = (this.starts[line.row] ?? 0) + column;
455
+ const read = this.flow(start);
456
+ this.close(line.row, start, read.end);
457
+ return read.value;
458
+ }
459
+ const parts = [line];
460
+ for (let next = this.peek(); next !== undefined && next.indent > indent; next = this.peek()) {
461
+ parts.push(next);
273
462
  this.at += 1;
274
463
  }
275
- return flowValue(parts.join(" "));
464
+ const texts = [text, ...parts.slice(1).map((one) => one.text)];
465
+ const lines = parts.flatMap((part, at) => {
466
+ const after = parts[at + 1];
467
+ const between = after === undefined ? [] : this.raw.slice(part.row + 1, after.row);
468
+ // A comment ends a plain scalar, so a line of it after one is outside YAML.
469
+ if (after !== undefined && (part.commented || between.some((one) => one.trim() !== ""))) {
470
+ throw new Unsupported();
471
+ }
472
+ return [texts[at] ?? "", ...between.map(() => "")];
473
+ });
474
+ return scalar(foldedLines(lines, "plain"));
276
475
  }
277
476
 
278
- /** The value after a key or an item marker: inline text, else the deeper block below it. */
279
- private valueAfter(inline: string, indent: number, sequenceAtIndent: boolean): unknown {
280
- if (inline !== "") {
281
- const parts = [inline];
282
- // A flow collection's closing bracket may stand at the key's own column.
283
- for (
284
- let line = this.lines[this.at];
285
- line !== undefined && (line.indent > indent || openCollections(parts.join(" ")) > 0);
286
- line = this.lines[this.at]
287
- ) {
288
- parts.push(line.text);
289
- this.at += 1;
477
+ /**
478
+ * The block scalar whose header is on row `row`, under a node at `indent`: its lines are those
479
+ * after it that are blank or indented to its content's indentation, the indentation indicator's
480
+ * or else its first line's that is not blank, with its chomping read as YAML reads it.
481
+ */
482
+ private blockScalar(header: RegExpExecArray, row: number, indent: number): string {
483
+ const [, style] = header;
484
+ const digit = header[2] ?? header[5] ?? "";
485
+ const chomp = header[3] ?? header[4] ?? "";
486
+ const first = this.raw.slice(row + 1).find((one) => one.trim() !== "");
487
+ const content =
488
+ digit === ""
489
+ ? first === undefined || spacesOf(first) <= indent
490
+ ? Number.POSITIVE_INFINITY
491
+ : spacesOf(first)
492
+ : Math.max(indent, 0) + Number(digit);
493
+ let end = row + 1;
494
+ while (
495
+ end < this.raw.length &&
496
+ ((this.raw[end] ?? "").trim() === "" || spacesOf(this.raw[end] ?? "") >= content)
497
+ ) {
498
+ end += 1;
499
+ }
500
+ this.skipTo(end);
501
+ const body = this.raw
502
+ .slice(row + 1, end)
503
+ .map((one) => one.slice(Math.min(content, spacesOf(one))));
504
+ const value = foldedLines(body, style === ">" ? ">" : "|");
505
+ const unbroken = end === this.raw.length && !this.broken ? 1 : 0;
506
+ const last = body.findLastIndex((one) => one.trim() !== "");
507
+ const breaks = Math.max(body.length - Math.max(last, 0) - unbroken, 0);
508
+ const kept = chomp === "+" ? breaks : chomp === "-" || value === "" ? 0 : Math.min(breaks, 1);
509
+ return `${value}${"\n".repeat(kept)}`;
510
+ }
511
+
512
+ /**
513
+ * Reads one flow node of the document's text from `start`, answering its value and where it
514
+ * ends: a flow collection, whose lines and comments it reads through, an alias, a node an
515
+ * anchor names, a quoted scalar, or a plain scalar, its line breaks folded.
516
+ */
517
+ private flow(start: number): { readonly value: unknown; readonly end: number } {
518
+ const text = this.text;
519
+ let at = start;
520
+ const skip = (): void => {
521
+ for (;;) {
522
+ while (at < text.length && /\s/u.test(text.charAt(at))) {
523
+ at += 1;
524
+ }
525
+ if (text.charAt(at) !== "#" || !/\s/u.test(text.charAt(at - 1))) {
526
+ return;
527
+ }
528
+ const lineEnd = text.indexOf("\n", at);
529
+ at = lineEnd < 0 ? text.length : lineEnd;
290
530
  }
291
- return flowValue(parts.join(" "));
531
+ };
532
+ skip();
533
+ const open = text.charAt(at);
534
+ const alias = open === "*" ? matchAt(ALIAS, text, at) : null;
535
+ if (alias !== null) {
536
+ const [whole, name = ""] = alias;
537
+ return { value: this.alias(name), end: at + whole.length };
292
538
  }
293
- const next = this.lines[this.at];
294
- if (next === undefined) {
295
- return null;
539
+ const anchor = open === "&" ? matchAt(ANCHOR, text, at) : null;
540
+ if (anchor !== null) {
541
+ const read = this.flow(at + anchor[0].length);
542
+ this.anchors.set(anchor[1] ?? "", read.value);
543
+ return read;
296
544
  }
297
- if (next.indent > indent || (sequenceAtIndent && next.indent === indent && isItem(next.text))) {
298
- return this.block(next.indent);
545
+ if (open === "[" || open === "{") {
546
+ const close = open === "[" ? "]" : "}";
547
+ const items: unknown[] = [];
548
+ const object: Record<string, unknown> = {};
549
+ at += 1;
550
+ for (;;) {
551
+ skip();
552
+ if (text.charAt(at) === close) {
553
+ return { value: open === "[" ? items : object, end: at + 1 };
554
+ }
555
+ const item = this.flow(at);
556
+ at = item.end;
557
+ skip();
558
+ if (open === "{") {
559
+ if (text.charAt(at) !== ":" || typeof item.value !== "string") {
560
+ throw new Unsupported();
561
+ }
562
+ const held = this.flow(at + 1);
563
+ setMember(object, item.value, held.value);
564
+ at = held.end;
565
+ skip();
566
+ } else {
567
+ items.push(item.value);
568
+ }
569
+ if (text.charAt(at) === ",") {
570
+ at += 1;
571
+ } else if (text.charAt(at) !== close) {
572
+ throw new Unsupported();
573
+ }
574
+ }
575
+ }
576
+ if (open === "'" || open === '"') {
577
+ const read = quotedScalar(text, at);
578
+ if (read === undefined) {
579
+ throw new Unsupported();
580
+ }
581
+ return read;
299
582
  }
300
- return null;
583
+ let end = at;
584
+ while (
585
+ end < text.length &&
586
+ !",]}".includes(text.charAt(end)) &&
587
+ !(text.charAt(end) === ":" && /[\s,\]}]/u.test(text.charAt(end + 1) || " ")) &&
588
+ !(text.charAt(end) === "#" && /\s/u.test(text.charAt(end - 1)))
589
+ ) {
590
+ end += 1;
591
+ }
592
+ const lines = text
593
+ .slice(at, end)
594
+ .split("\n")
595
+ .map((one) => one.trim());
596
+ return { value: scalar(foldedLines(lines, "plain")), end };
597
+ }
598
+
599
+ /**
600
+ * Ends the flow node read from `start`, on row `row`, to `end`: the rest of its last line must
601
+ * be blank or a comment, and every line it runs over is read.
602
+ */
603
+ private close(row: number, start: number, end: number): void {
604
+ const lineEnd = this.text.indexOf("\n", end);
605
+ const tail = this.text.slice(end, lineEnd < 0 ? this.text.length : lineEnd);
606
+ if (!/^(?:\s*$|\s+#)/u.test(tail)) {
607
+ throw new Unsupported();
608
+ }
609
+ this.skipTo(row + (this.text.slice(start, end).match(/\n/gu)?.length ?? 0) + 1);
301
610
  }
302
611
 
303
612
  private mapping(indent: number): Record<string, unknown> {
304
613
  const object: Record<string, unknown> = {};
305
- for (let line = this.lines[this.at]; line?.indent === indent; line = this.lines[this.at]) {
614
+ for (let line = this.peek(); line?.indent === indent; line = this.peek()) {
306
615
  const end = keyEnd(line.text);
307
616
  if (end < 0) {
308
617
  throw new Unsupported();
@@ -312,9 +621,16 @@ class Reader {
312
621
  throw new Unsupported();
313
622
  }
314
623
  this.at += 1;
315
- object[String(key)] = this.valueAfter(line.text.slice(end + 1).trim(), indent, true);
624
+ const after = line.text.slice(end + 1);
625
+ const inline = after.trim();
626
+ const column = line.indent + end + 1 + after.length - after.trimStart().length;
627
+ setMember(
628
+ object,
629
+ String(key),
630
+ inline === "" ? this.below(indent, true) : this.value(line, column, inline, indent, true),
631
+ );
316
632
  }
317
- const left = this.lines[this.at];
633
+ const left = this.peek();
318
634
  if (left !== undefined && left.indent > indent) {
319
635
  throw new Unsupported();
320
636
  }
@@ -323,24 +639,24 @@ class Reader {
323
639
 
324
640
  private sequence(indent: number): unknown[] {
325
641
  const items: unknown[] = [];
326
- for (
327
- let line = this.lines[this.at];
328
- line?.indent === indent && isItem(line.text);
329
- line = this.lines[this.at]
330
- ) {
642
+ for (let line = this.peek(); line?.indent === indent && isItem(line.text); line = this.peek()) {
331
643
  const content = line.text.slice(1);
332
644
  const inline = content.trimStart();
333
- if (inline !== "" && (isItem(inline) || keyEnd(inline) >= 0)) {
645
+ const column = indent + 1 + content.length - inline.length;
646
+ if (opensCollection(inline)) {
334
647
  // A node on the item's own line continues at its own column: read it as a block there.
335
- const moved = { indent: indent + 1 + content.length - inline.length, text: inline };
336
- this.lines[this.at] = moved;
337
- items.push(this.block(moved.indent));
648
+ this.lines[this.at] = { ...line, indent: column, text: inline };
649
+ items.push(this.block(column));
338
650
  } else {
339
651
  this.at += 1;
340
- items.push(this.valueAfter(inline, indent, false));
652
+ items.push(
653
+ inline === ""
654
+ ? this.below(indent, false)
655
+ : this.value(line, column, inline, indent, false),
656
+ );
341
657
  }
342
658
  }
343
- const left = this.lines[this.at];
659
+ const left = this.peek();
344
660
  if (left !== undefined && left.indent > indent) {
345
661
  throw new Unsupported();
346
662
  }
@@ -350,26 +666,33 @@ class Reader {
350
666
 
351
667
  /** The JSON value one YAML document's text denotes, or undefined where it lies outside the subset. */
352
668
  export function yamlDocument(text: string): unknown {
669
+ const raw = text.replace(/^\uFEFF/u, "").split(/\r?\n/u);
670
+ const broken = raw.length > 1 && raw.at(-1) === "";
671
+ if (broken) {
672
+ raw.pop();
673
+ }
353
674
  const lines: Line[] = [];
354
- for (const raw of text.replace(/^\uFEFF/u, "").split(/\r?\n/u)) {
355
- if (/^ *\t/u.test(raw)) {
675
+ for (const [row, line] of raw.entries()) {
676
+ const content = withoutComment(line).trimEnd();
677
+ if (/^(?:---|\.\.\.)(?:\s|$)/u.test(content) || content.startsWith("%")) {
678
+ if (content === "---" && lines.length === 0) {
679
+ continue;
680
+ }
356
681
  return undefined;
357
682
  }
358
- const content = withoutComment(raw).trimEnd();
359
683
  const trimmed = content.trimStart();
360
- if (trimmed === "") {
361
- continue;
362
- }
363
- if (trimmed === "---" && lines.length === 0) {
364
- continue;
365
- }
366
- if (trimmed.startsWith("%") || trimmed === "---" || trimmed === "...") {
367
- return undefined;
684
+ if (trimmed !== "") {
685
+ lines.push({
686
+ indent: content.length - trimmed.length,
687
+ text: trimmed,
688
+ row,
689
+ tabbed: /^ *\t/u.test(line),
690
+ commented: content !== line.trimEnd(),
691
+ });
368
692
  }
369
- lines.push({ indent: content.length - trimmed.length, text: trimmed });
370
693
  }
371
694
  try {
372
- return new Reader(lines).document();
695
+ return new Reader(lines, raw, broken).document();
373
696
  } catch (error) {
374
697
  if (error instanceof Unsupported) {
375
698
  return undefined;
@@ -402,7 +725,8 @@ function topLevelKey(raw: string): string | undefined {
402
725
  /**
403
726
  * The value of one key of a document's top-level block mapping, read from that key's lines
404
727
  * alone, so a document whose other keys lie outside the subset still yields it. A key
405
- * declared twice, or one whose own lines lie outside the subset, is refused.
728
+ * declared twice, or one whose own lines lie outside the subset or name an anchor written
729
+ * under another key, is refused.
406
730
  */
407
731
  export function yamlKey(text: string, key: string): YamlKey {
408
732
  const lines = text.replace(/^\uFEFF/u, "").split(/\r?\n/u);