@effected/yaml 0.1.0

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.
@@ -0,0 +1,677 @@
1
+ import { YamlAlias, YamlMap, YamlPair, YamlScalar, YamlSeq } from "../../YamlNode.js";
2
+ import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
3
+ import { parseCSTAll } from "../cst-parser.js";
4
+ import { clearMeta, createState, hasMeta, sameLine } from "./state.js";
5
+ import { parseDirective, validateTagHandlesInDocument } from "./tags.js";
6
+ import { collectMultilinePlainScalar, findNextContentChild, getScalarStyle, hasBlockMapAfterInList, hasValueSepAfter, indexOfChild, makeScalar, resolveScalar } from "./scalars.js";
7
+ import { composeBlockMap, composeBlockSeq, composeFlatBlockMap } from "./block.js";
8
+ import { composeFlowMap, composeFlowSeq } from "./flow.js";
9
+
10
+ //#region src/internal/composer/document.ts
11
+ /** The flow-composer dispatch wired into every state this module creates. */
12
+ const FLOW = {
13
+ composeFlowMap,
14
+ composeFlowSeq
15
+ };
16
+ /**
17
+ * SY6V: at document level, an anchor or tag must not be followed by a
18
+ * block-sequence entry indicator "-" on the same line. The anchor/tag
19
+ * applies to the next node, but a "-" on the same line means the parser
20
+ * is interpreting it as a sequence start without proper structure.
21
+ */
22
+ function validateAnchorTagNotFollowedBySeqDashOnSameLine(meta, children, idx, state) {
23
+ for (let j = idx + 1; j < children.length; j++) {
24
+ const c = children[j];
25
+ if (!c) continue;
26
+ if (c.type === "newline") return;
27
+ if (c.type === "whitespace") {
28
+ if (c.source === "-" && sameLine(state.text, meta.offset, c.offset)) {
29
+ state.errors.push({
30
+ code: "UnexpectedToken",
31
+ message: "Block sequence entry indicator '-' cannot follow an anchor or tag on the same line",
32
+ offset: c.offset,
33
+ length: c.length
34
+ });
35
+ return;
36
+ }
37
+ continue;
38
+ }
39
+ if (c.type === "block-seq" && c.length === 0) continue;
40
+ return;
41
+ }
42
+ }
43
+ /**
44
+ * Validate that document markers (--- and ...) are not followed by content
45
+ * on the same line. YAML 1.2 §9.1.4/§9.2 require these markers to be on
46
+ * their own line (followed only by whitespace/comments).
47
+ *
48
+ * Checks within a single document's children AND across document boundaries
49
+ * (e.g. `... invalid` where `...` ends doc 1 and `invalid` starts doc 2).
50
+ */
51
+ function checkDocumentMarkerSameLine(children, state, nextDocChildren) {
52
+ for (let i = 0; i < children.length; i++) {
53
+ const child = children[i];
54
+ if (!child) continue;
55
+ if (child.type !== "whitespace") continue;
56
+ if (child.source !== "...") continue;
57
+ let found = false;
58
+ for (let j = i + 1; j < children.length; j++) {
59
+ const next = children[j];
60
+ if (!next) continue;
61
+ if (next.type === "newline") break;
62
+ if (next.type === "whitespace" && next.source.trim() === "") continue;
63
+ if (next.type === "comment") break;
64
+ if (sameLine(state.text, child.offset, next.offset)) state.errors.push({
65
+ code: "UnexpectedToken",
66
+ message: "Content on same line as document-end marker",
67
+ offset: next.offset,
68
+ length: next.length
69
+ });
70
+ found = true;
71
+ break;
72
+ }
73
+ if (!found && nextDocChildren) for (const next of nextDocChildren) {
74
+ if (!next) continue;
75
+ if (next.type === "newline") break;
76
+ if (next.type === "whitespace" && next.source.trim() === "") continue;
77
+ if (sameLine(state.text, child.offset, next.offset)) state.errors.push({
78
+ code: "UnexpectedToken",
79
+ message: "Content on same line as document-end marker",
80
+ offset: next.offset,
81
+ length: next.length
82
+ });
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ /**
88
+ * Check for trailing content after a complete value at document level.
89
+ * After a flow collection or scalar at the top level, only trivia and
90
+ * document markers should follow. Skips if next meaningful content is ":"
91
+ * (the flow collection is being used as a mapping key).
92
+ */
93
+ function checkTrailingContentAfterDocValue(children, startIdx, state, allowMappingKey = true) {
94
+ for (let j = startIdx; j < children.length; j++) {
95
+ const next = children[j];
96
+ if (!next) continue;
97
+ if (next.type === "newline" || next.type === "comment") continue;
98
+ if (next.type === "whitespace") {
99
+ if (next.source === "---" || next.source === "...") break;
100
+ if (next.source === ":") break;
101
+ if (next.source.trim() === "") continue;
102
+ }
103
+ if (allowMappingKey && (next.type === "flow-scalar" || next.type === "block-scalar" || next.type === "flow-map" || next.type === "flow-seq")) {
104
+ const afterNode = findNextContentChild(children, j + 1);
105
+ if (hasValueSepAfter(children, j + 1) || afterNode !== null && afterNode.type === "block-map") break;
106
+ }
107
+ if (next.type === "flow-scalar" || next.type === "block-scalar" || next.type === "block-map" || next.type === "block-seq" || next.type === "flow-map" || next.type === "flow-seq" || next.type === "anchor" || next.type === "tag" || next.type === "alias") state.errors.push({
108
+ code: "UnexpectedToken",
109
+ message: "Trailing content after document value",
110
+ offset: next.offset,
111
+ length: next.length
112
+ });
113
+ break;
114
+ }
115
+ }
116
+ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst) {
117
+ const spanEnd = Math.min(cst.offset + cst.length, state.text.length);
118
+ for (let ci = cst.offset; ci < spanEnd; ci++) {
119
+ const cc = state.text.charCodeAt(ci);
120
+ if (cc < 32 && cc !== 9 && cc !== 10 && cc !== 13) {
121
+ state.errors.push({
122
+ code: "UnexpectedCharacter",
123
+ message: `Unescaped control character U+${cc.toString(16).toUpperCase().padStart(4, "0")}`,
124
+ offset: ci,
125
+ length: 1
126
+ });
127
+ break;
128
+ }
129
+ }
130
+ const children = cst.children ?? [];
131
+ const directives = [];
132
+ let contents = null;
133
+ let documentComment;
134
+ const hasDocStart = children.some((c) => c.type === "whitespace" && c.source === "---");
135
+ let i = 0;
136
+ const meta = {};
137
+ const outerMeta = {};
138
+ let sawNewlineSinceMeta = false;
139
+ const commitMetaAcrossNewline = () => {
140
+ if (sawNewlineSinceMeta && hasMeta(meta)) {
141
+ if (meta.tag !== void 0) outerMeta.tag = meta.tag;
142
+ if (meta.anchor !== void 0) outerMeta.anchor = meta.anchor;
143
+ if (meta.comment !== void 0) outerMeta.comment = meta.comment;
144
+ clearMeta(meta);
145
+ }
146
+ sawNewlineSinceMeta = false;
147
+ };
148
+ while (i < children.length) {
149
+ const child = children[i];
150
+ if (!child) {
151
+ i++;
152
+ continue;
153
+ }
154
+ if (child.type === "directive") {
155
+ const directive = parseDirective(child.source);
156
+ if (directive) {
157
+ directives.push(directive);
158
+ if (directive.name === "TAG" && directive.parameters.length >= 2) {
159
+ const handle = directive.parameters[0];
160
+ const prefix = directive.parameters[1];
161
+ if (handle && prefix) state.tagMap.set(handle, prefix);
162
+ }
163
+ }
164
+ i++;
165
+ continue;
166
+ }
167
+ if (child.type === "newline" && hasMeta(meta)) sawNewlineSinceMeta = true;
168
+ if (child.type === "whitespace" || child.type === "newline") {
169
+ if (child.type === "whitespace" && (child.source === "]" || child.source === "}")) state.errors.push({
170
+ code: "MalformedFlowCollection",
171
+ message: `Unexpected flow indicator '${child.source}' at document level`,
172
+ offset: child.offset,
173
+ length: child.length
174
+ });
175
+ i++;
176
+ continue;
177
+ }
178
+ if (child.type === "comment" && contents === null) {
179
+ documentComment = child.source.startsWith("#") ? child.source.slice(1).trim() : child.source;
180
+ i++;
181
+ continue;
182
+ }
183
+ if (child.type === "error") {
184
+ state.errors.push({
185
+ code: "UnexpectedToken",
186
+ message: `Unexpected content: ${child.source.trim() || "(empty)"}`,
187
+ offset: child.offset,
188
+ length: child.length
189
+ });
190
+ i++;
191
+ continue;
192
+ }
193
+ if (child.type === "anchor") {
194
+ validateAnchorTagNotFollowedBySeqDashOnSameLine(child, children, i, state);
195
+ commitMetaAcrossNewline();
196
+ meta.anchor = getAnchorName(child, state.text);
197
+ i++;
198
+ continue;
199
+ }
200
+ if (child.type === "tag") {
201
+ validateAnchorTagNotFollowedBySeqDashOnSameLine(child, children, i, state);
202
+ commitMetaAcrossNewline();
203
+ meta.tag = child.source;
204
+ i++;
205
+ continue;
206
+ }
207
+ if (child.type === "flow-scalar" || child.type === "block-scalar") {
208
+ const nextContent = findNextContentChild(children, i + 1);
209
+ if (nextContent && nextContent.type === "block-map") {
210
+ if (hasDocStart) {
211
+ const docStartChild = children.find((c) => c.type === "whitespace" && c.source === "---");
212
+ if (docStartChild && sameLine(state.text, docStartChild.offset, child.offset)) state.errors.push({
213
+ code: "UnexpectedToken",
214
+ message: "Mapping cannot start on document-start (---) line",
215
+ offset: child.offset,
216
+ length: child.length
217
+ });
218
+ }
219
+ let mapMeta;
220
+ let keyMeta;
221
+ if (hasMeta(outerMeta)) {
222
+ mapMeta = { ...outerMeta };
223
+ keyMeta = hasMeta(meta) ? { ...meta } : void 0;
224
+ } else if (hasDocStart && hasMeta(meta)) {
225
+ mapMeta = { ...meta };
226
+ keyMeta = void 0;
227
+ } else keyMeta = hasMeta(meta) ? { ...meta } : void 0;
228
+ const key = makeScalar(child, state, keyMeta);
229
+ clearMeta(meta);
230
+ clearMeta(outerMeta);
231
+ sawNewlineSinceMeta = false;
232
+ contents = composeBlockMap(nextContent, state, key, mapMeta);
233
+ i = indexOfChild(children, nextContent) + 1;
234
+ continue;
235
+ }
236
+ if (hasValueSepAfter(children, i + 1)) {
237
+ let mapMeta;
238
+ let keyMeta;
239
+ if (hasMeta(outerMeta)) {
240
+ mapMeta = { ...outerMeta };
241
+ keyMeta = hasMeta(meta) ? { ...meta } : void 0;
242
+ } else if (hasDocStart && hasMeta(meta)) {
243
+ mapMeta = { ...meta };
244
+ keyMeta = void 0;
245
+ } else keyMeta = hasMeta(meta) ? { ...meta } : void 0;
246
+ const key = makeScalar(child, state, keyMeta);
247
+ clearMeta(meta);
248
+ clearMeta(outerMeta);
249
+ sawNewlineSinceMeta = false;
250
+ contents = composeFlatBlockMap(children, i + 1, cst, state, key, mapMeta);
251
+ break;
252
+ }
253
+ if (child.type === "flow-scalar" && getScalarStyle(child) === "plain") {
254
+ const { value, nextIdx, partsCount, endOffset } = collectMultilinePlainScalar(children, i, void 0, state.text);
255
+ const combined = { ...outerMeta };
256
+ if (meta.tag !== void 0) combined.tag = meta.tag;
257
+ if (meta.anchor !== void 0) combined.anchor = meta.anchor;
258
+ const resolved = resolveScalar(value, "plain", combined.tag, state);
259
+ const scalarLength = partsCount > 1 ? endOffset - child.offset : child.length;
260
+ contents = new YamlScalar({
261
+ value: resolved,
262
+ style: "plain",
263
+ offset: child.offset,
264
+ length: scalarLength,
265
+ ...combined.tag !== void 0 ? { tag: combined.tag } : {},
266
+ ...combined.anchor !== void 0 ? { anchor: combined.anchor } : {}
267
+ });
268
+ if (combined.anchor) registerAnchor(contents, combined.anchor, state, child.offset);
269
+ clearMeta(meta);
270
+ clearMeta(outerMeta);
271
+ sawNewlineSinceMeta = false;
272
+ if (partsCount > 1) {
273
+ const nextContent2 = findNextContentChild(children, nextIdx);
274
+ if (nextContent2) {
275
+ if (nextContent2.type === "flow-scalar" && hasValueSepAfter(children, indexOfChild(children, nextContent2) + 1) || nextContent2.type === "block-map" || nextContent2.type === "flow-scalar" && hasBlockMapAfterInList(children, indexOfChild(children, nextContent2) + 1)) state.errors.push({
276
+ code: "UnexpectedToken",
277
+ message: "Trailing content after document value",
278
+ offset: nextContent2.offset,
279
+ length: nextContent2.length
280
+ });
281
+ }
282
+ } else checkTrailingContentAfterDocValue(children, nextIdx, state, false);
283
+ i = nextIdx;
284
+ continue;
285
+ }
286
+ const combined = { ...outerMeta };
287
+ if (meta.tag !== void 0) combined.tag = meta.tag;
288
+ if (meta.anchor !== void 0) combined.anchor = meta.anchor;
289
+ contents = makeScalar(child, state, hasMeta(combined) ? combined : void 0);
290
+ clearMeta(meta);
291
+ clearMeta(outerMeta);
292
+ sawNewlineSinceMeta = false;
293
+ i++;
294
+ continue;
295
+ }
296
+ if (child.type === "block-map") {
297
+ const combined = { ...outerMeta };
298
+ if (meta.tag !== void 0) combined.tag = meta.tag;
299
+ if (meta.anchor !== void 0) combined.anchor = meta.anchor;
300
+ contents = composeBlockMap(child, state, void 0, hasMeta(combined) ? combined : void 0);
301
+ clearMeta(meta);
302
+ clearMeta(outerMeta);
303
+ sawNewlineSinceMeta = false;
304
+ i++;
305
+ continue;
306
+ }
307
+ if (child.type === "block-seq") {
308
+ const isRootSeq = contents === null;
309
+ const combined = { ...outerMeta };
310
+ if (meta.tag !== void 0) combined.tag = meta.tag;
311
+ if (meta.anchor !== void 0) combined.anchor = meta.anchor;
312
+ contents = composeBlockSeq(child, state, hasMeta(combined) ? combined : void 0);
313
+ clearMeta(meta);
314
+ clearMeta(outerMeta);
315
+ sawNewlineSinceMeta = false;
316
+ i++;
317
+ if (isRootSeq) checkTrailingContentAfterDocValue(children, i, state, false);
318
+ continue;
319
+ }
320
+ if (child.type === "flow-map") {
321
+ const nextAfterFlowMap0 = findNextContentChild(children, i + 1);
322
+ const flowIsKey = !!nextAfterFlowMap0 && nextAfterFlowMap0.type === "block-map";
323
+ let flowMeta;
324
+ let mapMeta;
325
+ if (flowIsKey && hasMeta(outerMeta)) {
326
+ mapMeta = { ...outerMeta };
327
+ flowMeta = hasMeta(meta) ? { ...meta } : void 0;
328
+ } else {
329
+ const combined = { ...outerMeta };
330
+ if (meta.tag !== void 0) combined.tag = meta.tag;
331
+ if (meta.anchor !== void 0) combined.anchor = meta.anchor;
332
+ flowMeta = hasMeta(combined) ? combined : void 0;
333
+ }
334
+ const flowMap = composeFlowMap(child, state, flowMeta);
335
+ clearMeta(meta);
336
+ clearMeta(outerMeta);
337
+ sawNewlineSinceMeta = false;
338
+ i++;
339
+ if (flowIsKey && nextAfterFlowMap0) {
340
+ contents = composeBlockMap(nextAfterFlowMap0, state, flowMap, mapMeta);
341
+ while (i < children.length && children[i] !== nextAfterFlowMap0) i++;
342
+ i++;
343
+ } else {
344
+ contents = flowMap;
345
+ checkTrailingContentAfterDocValue(children, i, state);
346
+ }
347
+ continue;
348
+ }
349
+ if (child.type === "flow-seq") {
350
+ const nextAfterFlowSeq0 = findNextContentChild(children, i + 1);
351
+ const flowIsKey = !!nextAfterFlowSeq0 && nextAfterFlowSeq0.type === "block-map";
352
+ let flowMeta;
353
+ let mapMeta;
354
+ if (flowIsKey && hasMeta(outerMeta)) {
355
+ mapMeta = { ...outerMeta };
356
+ flowMeta = hasMeta(meta) ? { ...meta } : void 0;
357
+ } else {
358
+ const combined = { ...outerMeta };
359
+ if (meta.tag !== void 0) combined.tag = meta.tag;
360
+ if (meta.anchor !== void 0) combined.anchor = meta.anchor;
361
+ flowMeta = hasMeta(combined) ? combined : void 0;
362
+ }
363
+ const flowSeq = composeFlowSeq(child, state, flowMeta);
364
+ clearMeta(meta);
365
+ clearMeta(outerMeta);
366
+ sawNewlineSinceMeta = false;
367
+ i++;
368
+ const nextAfterFlowSeq = findNextContentChild(children, i);
369
+ if (nextAfterFlowSeq && nextAfterFlowSeq.type === "block-map") {
370
+ contents = composeBlockMap(nextAfterFlowSeq, state, flowSeq, mapMeta);
371
+ while (i < children.length && children[i] !== nextAfterFlowSeq) i++;
372
+ i++;
373
+ } else {
374
+ contents = flowSeq;
375
+ checkTrailingContentAfterDocValue(children, i, state);
376
+ }
377
+ continue;
378
+ }
379
+ if (child.type === "alias") {
380
+ checkAnchorOnAlias(meta, child, state);
381
+ contents = makeAlias(child, state);
382
+ i++;
383
+ continue;
384
+ }
385
+ i++;
386
+ }
387
+ validateDirectives(directives, cst, state, hasSubsequentDocuments);
388
+ checkDocumentMarkerSameLine(children, state, nextDocCst?.children);
389
+ const hasDocEnd = children.some((c) => c.type === "whitespace" && c.source === "...");
390
+ let hasDocStartTab = false;
391
+ for (let ci = 0; ci < children.length; ci++) {
392
+ const c = children[ci];
393
+ if (c && c.type === "whitespace" && c.source === "---") {
394
+ if (state.text[c.offset + c.length] === " ") hasDocStartTab = true;
395
+ break;
396
+ }
397
+ }
398
+ return {
399
+ contents,
400
+ errors: [...state.errors],
401
+ warnings: [...state.warnings],
402
+ directives,
403
+ hasDocumentStart: hasDocStart,
404
+ hasDocumentEnd: hasDocEnd,
405
+ hasDocumentStartTab: hasDocStartTab,
406
+ ...documentComment !== void 0 ? { comment: documentComment } : {}
407
+ };
408
+ }
409
+ /**
410
+ * Validate YAML directive rules within a single document's CST.
411
+ * Pushes errors into state.errors for any violations found.
412
+ */
413
+ function validateDirectives(directives, cst, state, hasSubsequentDocuments = false) {
414
+ const children = cst.children ?? [];
415
+ if (directives.filter((d) => d.name === "YAML").length > 1) {
416
+ let directiveCount = 0;
417
+ for (const child of children) if (child.type === "directive" && child.source.trim().startsWith("%YAML")) {
418
+ directiveCount++;
419
+ if (directiveCount === 2) {
420
+ state.errors.push({
421
+ code: "InvalidDirective",
422
+ message: "Duplicate %YAML directive",
423
+ offset: child.offset,
424
+ length: child.length
425
+ });
426
+ break;
427
+ }
428
+ }
429
+ }
430
+ for (const child of children) {
431
+ if (child.type !== "directive") continue;
432
+ const src = child.source.trim();
433
+ if (!src.startsWith("%YAML")) continue;
434
+ const hashIdx = src.indexOf("#");
435
+ if (hashIdx > 0) {
436
+ const before = src[hashIdx - 1];
437
+ if (before !== " " && before !== " ") {
438
+ state.errors.push({
439
+ code: "InvalidDirective",
440
+ message: "Comment in directive requires preceding whitespace",
441
+ offset: child.offset,
442
+ length: child.length
443
+ });
444
+ continue;
445
+ }
446
+ }
447
+ const params = (hashIdx > 0 ? src.slice(0, hashIdx).trimEnd() : src).slice(1).split(/\s+/).slice(1);
448
+ if (params.length !== 1) state.errors.push({
449
+ code: "InvalidDirective",
450
+ message: params.length === 0 ? "%YAML directive requires a version parameter" : `%YAML directive has extra parameters: ${params.slice(1).join(" ")}`,
451
+ offset: child.offset,
452
+ length: child.length
453
+ });
454
+ }
455
+ let hasDirective = false;
456
+ let hasDocumentStart = false;
457
+ for (const child of children) {
458
+ if (child.type === "directive") hasDirective = true;
459
+ if (child.type === "whitespace" && child.source === "---") hasDocumentStart = true;
460
+ }
461
+ if (hasDirective && !hasDocumentStart) {
462
+ for (const child of children) if (child.type === "directive") {
463
+ state.errors.push({
464
+ code: "InvalidDirective",
465
+ message: "Directive must be followed by a document-start marker (---)",
466
+ offset: child.offset,
467
+ length: child.length
468
+ });
469
+ break;
470
+ }
471
+ }
472
+ if (hasSubsequentDocuments) {
473
+ let hasContent = false;
474
+ for (const child of children) {
475
+ if (child.type === "flow-scalar" || child.type === "block-scalar" || child.type === "block-map" || child.type === "block-seq" || child.type === "flow-map" || child.type === "flow-seq" || child.type === "alias" || child.type === "anchor" || child.type === "tag") hasContent = true;
476
+ if (child.type === "directive" && hasContent) state.errors.push({
477
+ code: "InvalidDirective",
478
+ message: "Directive after content requires a document-end marker (...) first",
479
+ offset: child.offset,
480
+ length: child.length
481
+ });
482
+ if (hasContent && child.children) {
483
+ const nested = findNestedDirective(child);
484
+ if (nested) state.errors.push({
485
+ code: "InvalidDirective",
486
+ message: "Directive after content requires a document-end marker (...) first",
487
+ offset: nested.offset,
488
+ length: nested.length
489
+ });
490
+ }
491
+ }
492
+ }
493
+ }
494
+ /** Recursively find the first directive node within a CST subtree. */
495
+ function findNestedDirective(node) {
496
+ if (node.type === "directive") return node;
497
+ if (node.children) for (const child of node.children) {
498
+ const found = findNestedDirective(child);
499
+ if (found) return found;
500
+ }
501
+ return null;
502
+ }
503
+ /**
504
+ * Validate directive placement across a multi-document CST stream.
505
+ *
506
+ * YAML 1.2 requires that directives appearing between documents must be
507
+ * preceded by a document-end marker (`...`). This function checks each
508
+ * CST document node after the first: if it contains directives, the
509
+ * preceding document must have ended with `...`.
510
+ */
511
+ function validateCrossDocumentDirectives(cstNodes, state) {
512
+ for (let docIdx = 1; docIdx < cstNodes.length; docIdx++) {
513
+ const cst = cstNodes[docIdx];
514
+ if (!cst) continue;
515
+ const children = cst.children ?? [];
516
+ validateTagHandlesInDocument(cst, state);
517
+ if (!children.some((c) => c.type === "directive")) continue;
518
+ const prevCst = cstNodes[docIdx - 1];
519
+ if (!prevCst) continue;
520
+ const prevChildren = prevCst.children ?? [];
521
+ let prevEndedWithDocEnd = false;
522
+ for (let i = prevChildren.length - 1; i >= 0; i--) {
523
+ const c = prevChildren[i];
524
+ if (!c) continue;
525
+ if (c.source === "...") {
526
+ prevEndedWithDocEnd = true;
527
+ break;
528
+ }
529
+ if (c.type === "newline" || c.type === "whitespace" || c.type === "comment") continue;
530
+ break;
531
+ }
532
+ if (!prevEndedWithDocEnd) {
533
+ for (const child of children) if (child.type === "directive") {
534
+ state.errors.push({
535
+ code: "InvalidDirective",
536
+ message: "Directive between documents requires a document-end marker (...) after the previous document",
537
+ offset: child.offset,
538
+ length: child.length
539
+ });
540
+ break;
541
+ }
542
+ }
543
+ }
544
+ }
545
+ /**
546
+ * Walk the AST and stamp `sourceMultiline: true` on every YamlScalar/Map/Seq
547
+ * whose source span (offset..offset+length in `text`) contains a newline.
548
+ *
549
+ * The composer uses this single post-pass instead of threading the flag
550
+ * through dozens of construction sites. Nodes whose span is single-line are
551
+ * returned unchanged (no copy) to avoid unnecessary allocation.
552
+ */
553
+ function isSourceMultiline(text, offset, length) {
554
+ if (length <= 0) return false;
555
+ const end = Math.min(offset + length, text.length);
556
+ for (let i = offset; i < end; i++) {
557
+ const ch = text.charCodeAt(i);
558
+ if (ch === 10 || ch === 13) return true;
559
+ }
560
+ return false;
561
+ }
562
+ function decorateSourceMultiline(node, text) {
563
+ if (node === null || node instanceof YamlAlias) return node;
564
+ if (node instanceof YamlScalar) {
565
+ if (!isSourceMultiline(text, node.offset, node.length)) return node;
566
+ return new YamlScalar({
567
+ value: node.value,
568
+ style: node.style,
569
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
570
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
571
+ ...node.comment !== void 0 ? { comment: node.comment } : {},
572
+ ...node.chomp !== void 0 ? { chomp: node.chomp } : {},
573
+ ...node.raw !== void 0 ? { raw: node.raw } : {},
574
+ sourceMultiline: true,
575
+ offset: node.offset,
576
+ length: node.length
577
+ });
578
+ }
579
+ if (node instanceof YamlMap) {
580
+ const newItems = node.items.map((pair) => new YamlPair({
581
+ key: decorateSourceMultiline(pair.key, text) ?? pair.key,
582
+ value: pair.value === null ? null : decorateSourceMultiline(pair.value, text),
583
+ ...pair.comment !== void 0 ? { comment: pair.comment } : {}
584
+ }));
585
+ const multiline = isSourceMultiline(text, node.offset, node.length);
586
+ return new YamlMap({
587
+ items: newItems,
588
+ style: node.style,
589
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
590
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
591
+ ...node.comment !== void 0 ? { comment: node.comment } : {},
592
+ ...multiline ? { sourceMultiline: true } : {},
593
+ offset: node.offset,
594
+ length: node.length
595
+ });
596
+ }
597
+ if (node instanceof YamlSeq) {
598
+ const newItems = node.items.map((item) => decorateSourceMultiline(item, text) ?? item);
599
+ const multiline = isSourceMultiline(text, node.offset, node.length);
600
+ return new YamlSeq({
601
+ items: newItems,
602
+ style: node.style,
603
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
604
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
605
+ ...node.comment !== void 0 ? { comment: node.comment } : {},
606
+ ...multiline ? { sourceMultiline: true } : {},
607
+ offset: node.offset,
608
+ length: node.length
609
+ });
610
+ }
611
+ return node;
612
+ }
613
+ function decorateDocumentSourceMultiline(doc, text) {
614
+ const decorated = decorateSourceMultiline(doc.contents ?? null, text);
615
+ if (decorated === doc.contents) return doc;
616
+ return {
617
+ contents: decorated,
618
+ errors: doc.errors,
619
+ warnings: doc.warnings,
620
+ directives: doc.directives,
621
+ ...doc.comment !== void 0 ? { comment: doc.comment } : {},
622
+ hasDocumentStart: doc.hasDocumentStart,
623
+ hasDocumentEnd: doc.hasDocumentEnd,
624
+ hasDocumentStartTab: doc.hasDocumentStartTab
625
+ };
626
+ }
627
+ const EMPTY_DOCUMENT = {
628
+ contents: null,
629
+ errors: [],
630
+ warnings: [],
631
+ directives: [],
632
+ hasDocumentStart: false,
633
+ hasDocumentEnd: false,
634
+ hasDocumentStartTab: false
635
+ };
636
+ /**
637
+ * Compose the first document of `text` with full error recovery — v3
638
+ * `parseDocument` semantics minus the Effect wrapper and minus fatal-code
639
+ * filtering (the facade applies `isFatalCode` to the returned diagnostics).
640
+ * Cross-document directive-placement errors are validated into the same
641
+ * state and therefore appear in the returned document's `errors`.
642
+ */
643
+ function composeFirstDocument(text, options) {
644
+ const cstNodes = parseCSTAll(text);
645
+ const state = createState(text, FLOW, options);
646
+ validateCrossDocumentDirectives(cstNodes, state);
647
+ const doc = cstNodes[0];
648
+ if (!doc) return EMPTY_DOCUMENT;
649
+ return decorateDocumentSourceMultiline(composeDocument(doc, state, cstNodes.length > 1, cstNodes[1]), text);
650
+ }
651
+ /**
652
+ * Compose every document of `text` with full error recovery — v3
653
+ * `parseAllDocuments` semantics minus the Effect wrapper and minus
654
+ * fatal-code filtering. Each document is composed with a fresh state; the
655
+ * cross-document directive validation runs in its own state whose errors
656
+ * are returned unfiltered as `streamErrors` (v3 filtered these to
657
+ * `InvalidDirective` before failing — the facade applies that filter).
658
+ */
659
+ function composeAllDocuments(text, options) {
660
+ const cstNodes = parseCSTAll(text);
661
+ const documents = [];
662
+ const crossDocState = createState(text, FLOW, options);
663
+ validateCrossDocumentDirectives(cstNodes, crossDocState);
664
+ for (let i = 0; i < cstNodes.length; i++) {
665
+ const cst = cstNodes[i];
666
+ if (!cst) continue;
667
+ const doc = composeDocument(cst, createState(text, FLOW, options), i < cstNodes.length - 1, cstNodes[i + 1]);
668
+ documents.push(decorateDocumentSourceMultiline(doc, text));
669
+ }
670
+ return {
671
+ documents,
672
+ streamErrors: crossDocState.errors
673
+ };
674
+ }
675
+
676
+ //#endregion
677
+ export { composeAllDocuments, composeDocument, composeFirstDocument, validateCrossDocumentDirectives };