@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/Yaml.js +414 -0
- package/YamlDiagnostic.js +126 -0
- package/YamlDocument.js +181 -0
- package/YamlEdit.js +45 -0
- package/YamlFormat.js +309 -0
- package/YamlNode.js +396 -0
- package/YamlVisitor.js +172 -0
- package/index.d.ts +1331 -0
- package/index.js +9 -0
- package/internal/composer/anchors.js +97 -0
- package/internal/composer/block.js +1155 -0
- package/internal/composer/document.js +677 -0
- package/internal/composer/flow.js +400 -0
- package/internal/composer/scalars.js +885 -0
- package/internal/composer/state.js +117 -0
- package/internal/composer/tags.js +88 -0
- package/internal/cst-parser.js +716 -0
- package/internal/diagnostics.js +80 -0
- package/internal/diff.js +56 -0
- package/internal/fold.js +153 -0
- package/internal/lexer.js +959 -0
- package/internal/stringifier.js +797 -0
- package/package.json +47 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -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 };
|