@cplieger/deadset-ts 8.0.0-dev.12 → 8.0.0-dev.14

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