@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,1155 @@
|
|
|
1
|
+
import { YamlMap, YamlPair, YamlScalar, YamlSeq } from "../../YamlNode.js";
|
|
2
|
+
import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
|
|
3
|
+
import { enterNesting, exitNesting, hasMeta, hasNonWhitespaceBeforeOnLine, lineCol, lineIndentColumn, sameLine } from "./state.js";
|
|
4
|
+
import { blockMapStartsWithValueSep, classifyPlainNumeric, collectMultilineKey, collectMultilinePlainScalar, findFirstContent, findLastContent, findNextContentInList, findNextSignificantChild, findValueSepOffset, getScalarStyle, hasValueSepAfterInList, hasValueSepBetween, hasValueSepThroughPlainScalars, makeScalar, resolveScalar, shouldPreserveRaw } from "./scalars.js";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/composer/block.ts
|
|
7
|
+
/**
|
|
8
|
+
* Compose a block map from its CST children, with an optional external first key.
|
|
9
|
+
*
|
|
10
|
+
* CST pattern for `a: 1, b: true`:
|
|
11
|
+
* `[flow-scalar("a"), block-map(children: [":"," ","1","\\n","b",":"," ","true"])]`
|
|
12
|
+
*
|
|
13
|
+
* The first key is external (sibling before block-map in document/parent).
|
|
14
|
+
* Subsequent keys are inside the block-map children.
|
|
15
|
+
*/
|
|
16
|
+
function composeBlockMap(blockMapCst, state, externalFirstKey, meta) {
|
|
17
|
+
if (!enterNesting(state, blockMapCst)) return new YamlMap({
|
|
18
|
+
items: [],
|
|
19
|
+
style: "block",
|
|
20
|
+
offset: blockMapCst.offset,
|
|
21
|
+
length: blockMapCst.length
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
return composeBlockMapInner(blockMapCst, state, externalFirstKey, meta);
|
|
25
|
+
} finally {
|
|
26
|
+
exitNesting(state);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function composeBlockMapInner(blockMapCst, state, externalFirstKey, meta) {
|
|
30
|
+
const children = blockMapCst.children ?? [];
|
|
31
|
+
const pairs = [];
|
|
32
|
+
const extKeyOffset = externalFirstKey && "offset" in externalFirstKey ? externalFirstKey.offset : void 0;
|
|
33
|
+
const items = flattenBlockMapChildren(children, state, extKeyOffset !== void 0 ? lineIndentColumn(state.text, extKeyOffset) : void 0, extKeyOffset);
|
|
34
|
+
if (externalFirstKey) items.unshift({
|
|
35
|
+
kind: "key",
|
|
36
|
+
node: externalFirstKey
|
|
37
|
+
});
|
|
38
|
+
buildPairs(items, pairs, state.text);
|
|
39
|
+
if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
|
|
40
|
+
checkMultilineImplicitKeys(pairs, state);
|
|
41
|
+
const offset = externalFirstKey ? "offset" in externalFirstKey ? externalFirstKey.offset : blockMapCst.offset : blockMapCst.offset;
|
|
42
|
+
const map = new YamlMap({
|
|
43
|
+
items: pairs,
|
|
44
|
+
style: "block",
|
|
45
|
+
offset,
|
|
46
|
+
length: blockMapCst.offset + blockMapCst.length - offset,
|
|
47
|
+
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
48
|
+
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
49
|
+
...meta?.comment !== void 0 ? { comment: meta.comment } : {}
|
|
50
|
+
});
|
|
51
|
+
if (meta?.anchor) registerAnchor(map, meta.anchor, state, offset);
|
|
52
|
+
return map;
|
|
53
|
+
}
|
|
54
|
+
function flattenBlockMapChildren(children, state, externalKeyColumn, externalKeyOffset) {
|
|
55
|
+
const items = [];
|
|
56
|
+
let pendingMeta = {};
|
|
57
|
+
let outerMeta = {};
|
|
58
|
+
let sawNewlineSincePending = false;
|
|
59
|
+
let afterValueSep = false;
|
|
60
|
+
let lastValueSepOffset = -1;
|
|
61
|
+
let lastKeyColumn = externalKeyColumn ?? -1;
|
|
62
|
+
let lastKeyOffset = externalKeyOffset ?? -1;
|
|
63
|
+
const hasExternalKeyColumn = externalKeyColumn !== void 0;
|
|
64
|
+
let pendingExplicitKeyCol = -1;
|
|
65
|
+
function commitOuterIfNewlineSeen() {
|
|
66
|
+
if (sawNewlineSincePending && hasMeta(pendingMeta)) {
|
|
67
|
+
outerMeta = hasMeta(outerMeta) ? {
|
|
68
|
+
...outerMeta,
|
|
69
|
+
...pendingMeta
|
|
70
|
+
} : pendingMeta;
|
|
71
|
+
pendingMeta = {};
|
|
72
|
+
}
|
|
73
|
+
sawNewlineSincePending = false;
|
|
74
|
+
}
|
|
75
|
+
function combinedPending() {
|
|
76
|
+
if (!hasMeta(outerMeta)) return pendingMeta;
|
|
77
|
+
if (!hasMeta(pendingMeta)) return outerMeta;
|
|
78
|
+
return {
|
|
79
|
+
...outerMeta,
|
|
80
|
+
...pendingMeta
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function resetAllMeta() {
|
|
84
|
+
pendingMeta = {};
|
|
85
|
+
outerMeta = {};
|
|
86
|
+
sawNewlineSincePending = false;
|
|
87
|
+
}
|
|
88
|
+
function validateKeyColumn(col, offset, length) {
|
|
89
|
+
if (lastKeyColumn >= 0 && col !== lastKeyColumn) state.errors.push({
|
|
90
|
+
code: "InvalidIndentation",
|
|
91
|
+
message: "Bad indentation in block mapping",
|
|
92
|
+
offset,
|
|
93
|
+
length
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function pushNode(node, nodeOffset) {
|
|
97
|
+
if (!afterValueSep && nodeOffset !== void 0 && nodeOffset >= 0) {
|
|
98
|
+
lastKeyColumn = pendingExplicitKeyCol >= 0 ? pendingExplicitKeyCol : lineCol(state.text, nodeOffset).column;
|
|
99
|
+
lastKeyOffset = nodeOffset;
|
|
100
|
+
}
|
|
101
|
+
pendingExplicitKeyCol = -1;
|
|
102
|
+
items.push({
|
|
103
|
+
kind: "node",
|
|
104
|
+
node
|
|
105
|
+
});
|
|
106
|
+
afterValueSep = false;
|
|
107
|
+
}
|
|
108
|
+
for (let i = 0; i < children.length; i++) {
|
|
109
|
+
const child = children[i];
|
|
110
|
+
if (!child) continue;
|
|
111
|
+
if (child.type === "error") {
|
|
112
|
+
state.errors.push({
|
|
113
|
+
code: "UnexpectedToken",
|
|
114
|
+
message: `Unexpected content: ${child.source.trim() || "(empty)"}`,
|
|
115
|
+
offset: child.offset,
|
|
116
|
+
length: child.length
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (child.type === "newline") {
|
|
121
|
+
if (hasMeta(pendingMeta)) sawNewlineSincePending = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (child.type === "whitespace") {
|
|
125
|
+
if (child.source === "?") {
|
|
126
|
+
const qCol = lineCol(state.text, child.offset).column;
|
|
127
|
+
pendingExplicitKeyCol = qCol;
|
|
128
|
+
afterValueSep = false;
|
|
129
|
+
const lookahead = scanExplicitKeyShape(children, i, qCol, state.text);
|
|
130
|
+
if (lookahead.kind === "inline-implicit-map") {
|
|
131
|
+
const sliceChildren = children.slice(i + 1, lookahead.endIdx);
|
|
132
|
+
const innerItems = flattenBlockMapChildren(sliceChildren, state);
|
|
133
|
+
const innerPairs = [];
|
|
134
|
+
buildPairs(innerItems, innerPairs, state.text);
|
|
135
|
+
const firstC = findFirstContent(sliceChildren);
|
|
136
|
+
const lastC = findLastContent(sliceChildren);
|
|
137
|
+
const innerOffset = firstC ? firstC.offset : child.offset;
|
|
138
|
+
pushNode(new YamlMap({
|
|
139
|
+
items: innerPairs,
|
|
140
|
+
style: "block",
|
|
141
|
+
offset: innerOffset,
|
|
142
|
+
length: (lastC ? lastC.offset + lastC.length : child.offset + child.length) - innerOffset
|
|
143
|
+
}), innerOffset);
|
|
144
|
+
i = lookahead.endIdx - 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (child.source === ":") {
|
|
150
|
+
validateNoTabAfterContinuationValueSep(child, children, i, state);
|
|
151
|
+
const flushMeta = combinedPending();
|
|
152
|
+
if (hasMeta(flushMeta)) {
|
|
153
|
+
const scalar = new YamlScalar({
|
|
154
|
+
value: resolveScalar("", "plain", flushMeta.tag, state),
|
|
155
|
+
style: "plain",
|
|
156
|
+
offset: child.offset,
|
|
157
|
+
length: 0,
|
|
158
|
+
...flushMeta.tag !== void 0 ? { tag: flushMeta.tag } : {},
|
|
159
|
+
...flushMeta.anchor !== void 0 ? { anchor: flushMeta.anchor } : {}
|
|
160
|
+
});
|
|
161
|
+
if (flushMeta.anchor) registerAnchor(scalar, flushMeta.anchor, state, child.offset);
|
|
162
|
+
resetAllMeta();
|
|
163
|
+
pushNode(scalar);
|
|
164
|
+
}
|
|
165
|
+
items.push({
|
|
166
|
+
kind: "value-sep",
|
|
167
|
+
offset: child.offset
|
|
168
|
+
});
|
|
169
|
+
afterValueSep = true;
|
|
170
|
+
lastValueSepOffset = child.offset;
|
|
171
|
+
}
|
|
172
|
+
if (child.source === "-" && lastValueSepOffset >= 0 && sameLine(state.text, lastValueSepOffset, child.offset) && hasNonWhitespaceBeforeOnLine(state.text, lastValueSepOffset)) state.errors.push({
|
|
173
|
+
code: "UnexpectedToken",
|
|
174
|
+
message: "Sequence entry on same line as mapping value indicator",
|
|
175
|
+
offset: child.offset,
|
|
176
|
+
length: child.length
|
|
177
|
+
});
|
|
178
|
+
else if (child.source === "-" && lastValueSepOffset >= 0 && !sameLine(state.text, lastValueSepOffset, child.offset) && pendingExplicitKeyCol < 0 && !precededByExplicitKeyMarker(children, i)) state.errors.push({
|
|
179
|
+
code: "InvalidIndentation",
|
|
180
|
+
message: "Block sequence entry indicator outside any sequence",
|
|
181
|
+
offset: child.offset,
|
|
182
|
+
length: child.length
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (child.type === "comment") {
|
|
187
|
+
const text = child.source.startsWith("#") ? child.source.slice(1).trim() : child.source;
|
|
188
|
+
items.push({
|
|
189
|
+
kind: "comment",
|
|
190
|
+
comment: text
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (child.type === "anchor") {
|
|
195
|
+
validatePropertyContinuationColumn(child, state, afterValueSep, lastValueSepOffset, lastKeyColumn);
|
|
196
|
+
commitOuterIfNewlineSeen();
|
|
197
|
+
pendingMeta.anchor = getAnchorName(child, state.text);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (child.type === "tag") {
|
|
201
|
+
validatePropertyContinuationColumn(child, state, afterValueSep, lastValueSepOffset, lastKeyColumn);
|
|
202
|
+
commitOuterIfNewlineSeen();
|
|
203
|
+
pendingMeta.tag = child.source;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (child.type === "flow-scalar" || child.type === "block-scalar") {
|
|
207
|
+
commitOuterIfNewlineSeen();
|
|
208
|
+
validateNoDoubleAnchorOnScalar(child, children, i, outerMeta, pendingMeta, state);
|
|
209
|
+
const valueSepOffset = findValueSepOffset(children, i + 1);
|
|
210
|
+
const sepOnSameLine = valueSepOffset >= 0 && lineCol(state.text, child.offset).line === lineCol(state.text, valueSepOffset).line;
|
|
211
|
+
if (afterValueSep && sepOnSameLine) {
|
|
212
|
+
const flushMeta = combinedPending();
|
|
213
|
+
if (hasMeta(flushMeta)) {
|
|
214
|
+
const scalar = new YamlScalar({
|
|
215
|
+
value: resolveScalar("", "plain", flushMeta.tag, state),
|
|
216
|
+
style: "plain",
|
|
217
|
+
offset: child.offset,
|
|
218
|
+
length: 0,
|
|
219
|
+
...flushMeta.tag !== void 0 ? { tag: flushMeta.tag } : {},
|
|
220
|
+
...flushMeta.anchor !== void 0 ? { anchor: flushMeta.anchor } : {}
|
|
221
|
+
});
|
|
222
|
+
if (flushMeta.anchor) registerAnchor(scalar, flushMeta.anchor, state, child.offset);
|
|
223
|
+
resetAllMeta();
|
|
224
|
+
pushNode(scalar);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const nextValueSepOffset = findValueSepOffset(children, i + 1);
|
|
228
|
+
if (afterValueSep && lastValueSepOffset >= 0 && hasNonWhitespaceBeforeOnLine(state.text, lastValueSepOffset) && child.type === "flow-scalar" && nextValueSepOffset >= 0 && sameLine(state.text, lastValueSepOffset, child.offset) && sameLine(state.text, child.offset, nextValueSepOffset)) state.errors.push({
|
|
229
|
+
code: "UnexpectedToken",
|
|
230
|
+
message: "Implicit mapping key on same line as previous value indicator",
|
|
231
|
+
offset: child.offset,
|
|
232
|
+
length: child.length
|
|
233
|
+
});
|
|
234
|
+
const nextContent = findNextContentInList(children, i + 1);
|
|
235
|
+
if (nextContent?.node.type === "block-map" && !hasValueSepBetween(children, i + 1, nextContent.idx)) {
|
|
236
|
+
if (!afterValueSep && hasExternalKeyColumn) validateKeyColumn(pendingExplicitKeyCol >= 0 ? pendingExplicitKeyCol : lineCol(state.text, child.offset).column, child.offset, child.length);
|
|
237
|
+
const keyMeta = hasMeta(pendingMeta) ? pendingMeta : void 0;
|
|
238
|
+
const mapMeta = hasMeta(outerMeta) ? outerMeta : void 0;
|
|
239
|
+
const key = makeScalar(child, state, keyMeta);
|
|
240
|
+
const map = composeBlockMap(nextContent.node, state, key, mapMeta);
|
|
241
|
+
resetAllMeta();
|
|
242
|
+
pushNode(map);
|
|
243
|
+
i = nextContent.idx;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!afterValueSep && child.type === "flow-scalar" && getScalarStyle(child) === "plain" && !hasValueSepAfterInList(children, i + 1) && hasValueSepThroughPlainScalars(children, i + 1)) {
|
|
247
|
+
let isExplicitKey = false;
|
|
248
|
+
let explicitKeyCol = -1;
|
|
249
|
+
for (let p = i - 1; p >= 0; p--) {
|
|
250
|
+
const prev = children[p];
|
|
251
|
+
if (!prev) continue;
|
|
252
|
+
if (prev.type === "whitespace" && prev.source === "?") {
|
|
253
|
+
explicitKeyCol = lineCol(state.text, prev.offset).column;
|
|
254
|
+
isExplicitKey = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (prev.type === "whitespace" && prev.source.trim() === "") continue;
|
|
258
|
+
if (prev.type === "newline") continue;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
if (isExplicitKey) {
|
|
262
|
+
let nextScalarIndented = false;
|
|
263
|
+
let sawNl = false;
|
|
264
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
265
|
+
const c = children[j];
|
|
266
|
+
if (!c) continue;
|
|
267
|
+
if (c.type === "newline") {
|
|
268
|
+
sawNl = true;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (c.type === "whitespace" && c.source.trim() === "") continue;
|
|
272
|
+
if (sawNl && c.type === "flow-scalar") nextScalarIndented = lineCol(state.text, c.offset).column > explicitKeyCol;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
isExplicitKey = nextScalarIndented;
|
|
276
|
+
}
|
|
277
|
+
if (isExplicitKey) {
|
|
278
|
+
const { value: keyValue, nextIdx: keyNextIdx } = collectMultilineKey(children, i);
|
|
279
|
+
const keyMeta = combinedPending();
|
|
280
|
+
const scalar = new YamlScalar({
|
|
281
|
+
value: resolveScalar(keyValue, "plain", keyMeta.tag, state),
|
|
282
|
+
style: "plain",
|
|
283
|
+
offset: child.offset,
|
|
284
|
+
length: child.length,
|
|
285
|
+
...keyMeta.tag !== void 0 ? { tag: keyMeta.tag } : {},
|
|
286
|
+
...keyMeta.anchor !== void 0 ? { anchor: keyMeta.anchor } : {}
|
|
287
|
+
});
|
|
288
|
+
if (keyMeta.anchor) registerAnchor(scalar, keyMeta.anchor, state, child.offset);
|
|
289
|
+
resetAllMeta();
|
|
290
|
+
pushNode(scalar, child.offset);
|
|
291
|
+
i = keyNextIdx - 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (child.type === "flow-scalar" && getScalarStyle(child) === "plain" && !hasValueSepAfterInList(children, i + 1)) {
|
|
296
|
+
const isValuePosition = afterValueSep;
|
|
297
|
+
if (!isValuePosition) {
|
|
298
|
+
let isLineStart = true;
|
|
299
|
+
for (let k = child.offset - 1; k >= 0; k--) {
|
|
300
|
+
const ch = state.text[k];
|
|
301
|
+
if (ch === "\n") break;
|
|
302
|
+
if (ch === " " || ch === " ") continue;
|
|
303
|
+
isLineStart = false;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
if (isLineStart) {
|
|
307
|
+
let precededByIndicator = false;
|
|
308
|
+
for (let p = i - 1; p >= 0; p--) {
|
|
309
|
+
const prev = children[p];
|
|
310
|
+
if (!prev) continue;
|
|
311
|
+
if (prev.type === "whitespace" && (prev.source === "-" || prev.source === "?")) {
|
|
312
|
+
precededByIndicator = true;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (prev.type === "whitespace" && prev.source.trim() === "") continue;
|
|
316
|
+
if (prev.type === "newline") continue;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
if (!precededByIndicator) state.errors.push({
|
|
320
|
+
code: "UnexpectedToken",
|
|
321
|
+
message: "Trailing content in block mapping",
|
|
322
|
+
offset: child.offset,
|
|
323
|
+
length: child.length
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const minContCol = isValuePosition && lastKeyColumn >= 0 && lastKeyOffset >= 0 && lastValueSepOffset >= 0 && sameLine(state.text, lastKeyOffset, lastValueSepOffset) ? lastKeyColumn + 1 : void 0;
|
|
328
|
+
const { value, nextIdx, partsCount, endOffset } = collectMultilinePlainScalar(children, i, minContCol, minContCol !== void 0 ? state.text : void 0);
|
|
329
|
+
const plainMeta = combinedPending();
|
|
330
|
+
const resolved = resolveScalar(value, "plain", plainMeta.tag, state);
|
|
331
|
+
const needsRaw = typeof resolved !== "string" && resolved !== void 0 && shouldPreserveRaw(value, resolved);
|
|
332
|
+
const scalar = new YamlScalar({
|
|
333
|
+
value: resolved,
|
|
334
|
+
style: "plain",
|
|
335
|
+
offset: child.offset,
|
|
336
|
+
length: partsCount > 1 ? endOffset - child.offset : child.length,
|
|
337
|
+
...plainMeta.tag !== void 0 ? { tag: plainMeta.tag } : {},
|
|
338
|
+
...plainMeta.anchor !== void 0 ? { anchor: plainMeta.anchor } : {},
|
|
339
|
+
...needsRaw ? { raw: value } : {}
|
|
340
|
+
});
|
|
341
|
+
if (plainMeta.anchor) registerAnchor(scalar, plainMeta.anchor, state, child.offset);
|
|
342
|
+
resetAllMeta();
|
|
343
|
+
pushNode(scalar, child.offset);
|
|
344
|
+
if (isValuePosition && partsCount > 1) {
|
|
345
|
+
const stoppedAtContent = findNextContentInList(children, nextIdx);
|
|
346
|
+
if (stoppedAtContent) {
|
|
347
|
+
const sn = stoppedAtContent.node;
|
|
348
|
+
const valueCol = lineCol(state.text, child.offset).column;
|
|
349
|
+
if (lineCol(state.text, sn.offset).column >= valueCol) {
|
|
350
|
+
if (sn.type === "flow-scalar" && getScalarStyle(sn) === "plain" && hasValueSepAfterInList(children, stoppedAtContent.idx + 1) || sn.type === "flow-scalar" && getScalarStyle(sn) === "plain" && (() => {
|
|
351
|
+
const after = findNextContentInList(children, stoppedAtContent.idx + 1);
|
|
352
|
+
return after !== null && after.node.type === "block-map";
|
|
353
|
+
})() || sn.type === "block-map") state.errors.push({
|
|
354
|
+
code: "UnexpectedToken",
|
|
355
|
+
message: "Mapping key after multiline plain scalar value",
|
|
356
|
+
offset: sn.offset,
|
|
357
|
+
length: sn.length
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
i = nextIdx - 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const style = getScalarStyle(child);
|
|
366
|
+
const isValuePosition = afterValueSep;
|
|
367
|
+
const scalarMeta = combinedPending();
|
|
368
|
+
const scalar = makeScalar(child, state, hasMeta(scalarMeta) ? scalarMeta : void 0);
|
|
369
|
+
resetAllMeta();
|
|
370
|
+
pushNode(scalar, child.offset);
|
|
371
|
+
if (isValuePosition && (style === "single-quoted" || style === "double-quoted")) {
|
|
372
|
+
checkTrailingContentOnSameLine(children, i + 1, child, state);
|
|
373
|
+
validateQuotedScalarContinuationIndent(child, state, lastKeyColumn);
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (child.type === "alias") {
|
|
378
|
+
commitOuterIfNewlineSeen();
|
|
379
|
+
const nextAlias = findNextContentInList(children, i + 1);
|
|
380
|
+
if (nextAlias?.node.type === "block-map") {
|
|
381
|
+
const alias = makeAlias(child, state);
|
|
382
|
+
const aliasMapMeta = hasMeta(outerMeta) ? outerMeta : void 0;
|
|
383
|
+
const map = composeBlockMap(nextAlias.node, state, alias, aliasMapMeta);
|
|
384
|
+
resetAllMeta();
|
|
385
|
+
pushNode(map);
|
|
386
|
+
i = nextAlias.idx;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
checkAnchorOnAlias(combinedPending(), child, state);
|
|
390
|
+
const alias = makeAlias(child, state);
|
|
391
|
+
resetAllMeta();
|
|
392
|
+
pushNode(alias);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (child.type === "block-map") {
|
|
396
|
+
commitOuterIfNewlineSeen();
|
|
397
|
+
const mapMeta = combinedPending();
|
|
398
|
+
if (hasMeta(pendingMeta) && blockMapStartsWithValueSep(child)) {
|
|
399
|
+
const emptyKey = new YamlScalar({
|
|
400
|
+
value: null,
|
|
401
|
+
style: "plain",
|
|
402
|
+
offset: child.offset,
|
|
403
|
+
length: 0,
|
|
404
|
+
...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
|
|
405
|
+
...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
|
|
406
|
+
});
|
|
407
|
+
if (pendingMeta.anchor) registerAnchor(emptyKey, pendingMeta.anchor, state, child.offset);
|
|
408
|
+
const map = composeBlockMap(child, state, emptyKey, hasMeta(outerMeta) ? outerMeta : void 0);
|
|
409
|
+
resetAllMeta();
|
|
410
|
+
pushNode(map);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const map = composeBlockMap(child, state, void 0, hasMeta(mapMeta) ? mapMeta : void 0);
|
|
414
|
+
resetAllMeta();
|
|
415
|
+
pushNode(map);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (child.type === "block-seq") {
|
|
419
|
+
commitOuterIfNewlineSeen();
|
|
420
|
+
const seqMeta = combinedPending();
|
|
421
|
+
if (!afterValueSep && pendingExplicitKeyCol < 0 && child.length > 0) state.errors.push({
|
|
422
|
+
code: "InvalidIndentation",
|
|
423
|
+
message: "Sequence in mapping key position",
|
|
424
|
+
offset: child.offset,
|
|
425
|
+
length: child.length
|
|
426
|
+
});
|
|
427
|
+
const seq = composeBlockSeq(child, state, hasMeta(seqMeta) ? seqMeta : void 0);
|
|
428
|
+
resetAllMeta();
|
|
429
|
+
pushNode(seq);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (child.type === "flow-map") {
|
|
433
|
+
commitOuterIfNewlineSeen();
|
|
434
|
+
const isValue = afterValueSep;
|
|
435
|
+
const flowMapMeta = combinedPending();
|
|
436
|
+
const map = state.flow.composeFlowMap(child, state, hasMeta(flowMapMeta) ? flowMapMeta : void 0, lastKeyColumn);
|
|
437
|
+
resetAllMeta();
|
|
438
|
+
pushNode(map);
|
|
439
|
+
if (isValue) checkTrailingContentOnSameLine(children, i + 1, child, state);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (child.type === "flow-seq") {
|
|
443
|
+
commitOuterIfNewlineSeen();
|
|
444
|
+
const isValue = afterValueSep;
|
|
445
|
+
const flowSeqMeta = combinedPending();
|
|
446
|
+
const seq = state.flow.composeFlowSeq(child, state, hasMeta(flowSeqMeta) ? flowSeqMeta : void 0, lastKeyColumn);
|
|
447
|
+
resetAllMeta();
|
|
448
|
+
pushNode(seq);
|
|
449
|
+
if (isValue) checkTrailingContentOnSameLine(children, i + 1, child, state);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const trailingMeta = combinedPending();
|
|
453
|
+
if (hasMeta(trailingMeta)) {
|
|
454
|
+
const scalar = new YamlScalar({
|
|
455
|
+
value: resolveScalar("", "plain", trailingMeta.tag, state),
|
|
456
|
+
style: "plain",
|
|
457
|
+
offset: 0,
|
|
458
|
+
length: 0,
|
|
459
|
+
...trailingMeta.tag !== void 0 ? { tag: trailingMeta.tag } : {},
|
|
460
|
+
...trailingMeta.anchor !== void 0 ? { anchor: trailingMeta.anchor } : {}
|
|
461
|
+
});
|
|
462
|
+
if (trailingMeta.anchor) registerAnchor(scalar, trailingMeta.anchor, state, 0);
|
|
463
|
+
items.push({
|
|
464
|
+
kind: "node",
|
|
465
|
+
node: scalar
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return items;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Build pairs from a semantic item stream.
|
|
472
|
+
* Pattern: node, value-sep, node produces a key:value pair.
|
|
473
|
+
* Pattern: node, value-sep (no node) produces a key:null pair.
|
|
474
|
+
* Pattern: value-sep, node produces a null:value pair.
|
|
475
|
+
*/
|
|
476
|
+
function buildPairs(items, pairs, text) {
|
|
477
|
+
let i = 0;
|
|
478
|
+
while (i < items.length) {
|
|
479
|
+
const item = items[i];
|
|
480
|
+
if (!item) {
|
|
481
|
+
i++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (item.kind === "comment") {
|
|
485
|
+
if (pairs.length > 0) {
|
|
486
|
+
const last = pairs[pairs.length - 1];
|
|
487
|
+
if (last) pairs[pairs.length - 1] = new YamlPair({
|
|
488
|
+
key: last.key,
|
|
489
|
+
value: last.value,
|
|
490
|
+
...item.comment !== void 0 ? { comment: item.comment } : {}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
i++;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (item.kind === "value-sep") {
|
|
497
|
+
const valueSepOffset = item.offset ?? 0;
|
|
498
|
+
i++;
|
|
499
|
+
const valueNode = consumeValueNodeForNullKey(items, i, text, valueSepOffset);
|
|
500
|
+
if (valueNode) {
|
|
501
|
+
const nullKey = new YamlScalar({
|
|
502
|
+
value: null,
|
|
503
|
+
style: "plain",
|
|
504
|
+
offset: 0,
|
|
505
|
+
length: 0
|
|
506
|
+
});
|
|
507
|
+
pairs.push(new YamlPair({
|
|
508
|
+
key: nullKey,
|
|
509
|
+
value: valueNode.node ?? null
|
|
510
|
+
}));
|
|
511
|
+
i = valueNode.nextIdx;
|
|
512
|
+
} else {
|
|
513
|
+
const nullKey = new YamlScalar({
|
|
514
|
+
value: null,
|
|
515
|
+
style: "plain",
|
|
516
|
+
offset: 0,
|
|
517
|
+
length: 0
|
|
518
|
+
});
|
|
519
|
+
pairs.push(new YamlPair({
|
|
520
|
+
key: nullKey,
|
|
521
|
+
value: null
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (item.kind === "node" || item.kind === "key") {
|
|
527
|
+
let keyNode = item.node;
|
|
528
|
+
i++;
|
|
529
|
+
if (item.kind === "key" && !keyNode) {
|
|
530
|
+
while (i < items.length && items[i]?.kind === "comment") i++;
|
|
531
|
+
if (i < items.length && items[i]?.kind === "node") {
|
|
532
|
+
keyNode = items[i]?.node;
|
|
533
|
+
i++;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
while (i < items.length && items[i]?.kind === "comment") i++;
|
|
537
|
+
if (i < items.length && items[i]?.kind === "value-sep") {
|
|
538
|
+
i++;
|
|
539
|
+
const valueResult = consumeValueNode(items, i);
|
|
540
|
+
if (valueResult) {
|
|
541
|
+
pairs.push(new YamlPair({
|
|
542
|
+
key: keyNode ?? new YamlScalar({
|
|
543
|
+
value: null,
|
|
544
|
+
style: "plain",
|
|
545
|
+
offset: 0,
|
|
546
|
+
length: 0
|
|
547
|
+
}),
|
|
548
|
+
value: valueResult.node ?? null
|
|
549
|
+
}));
|
|
550
|
+
i = valueResult.nextIdx;
|
|
551
|
+
} else pairs.push(new YamlPair({
|
|
552
|
+
key: keyNode ?? new YamlScalar({
|
|
553
|
+
value: null,
|
|
554
|
+
style: "plain",
|
|
555
|
+
offset: 0,
|
|
556
|
+
length: 0
|
|
557
|
+
}),
|
|
558
|
+
value: null
|
|
559
|
+
}));
|
|
560
|
+
} else pairs.push(new YamlPair({
|
|
561
|
+
key: keyNode ?? new YamlScalar({
|
|
562
|
+
value: null,
|
|
563
|
+
style: "plain",
|
|
564
|
+
offset: 0,
|
|
565
|
+
length: 0
|
|
566
|
+
}),
|
|
567
|
+
value: null
|
|
568
|
+
}));
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
i++;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function consumeValueNode(items, startIdx) {
|
|
575
|
+
let i = startIdx;
|
|
576
|
+
while (i < items.length) {
|
|
577
|
+
const item = items[i];
|
|
578
|
+
if (!item) break;
|
|
579
|
+
if (item.kind === "comment") {
|
|
580
|
+
i++;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (item.kind === "node") return {
|
|
584
|
+
node: item.node ?? null,
|
|
585
|
+
nextIdx: i + 1
|
|
586
|
+
};
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
return i > startIdx ? {
|
|
590
|
+
node: null,
|
|
591
|
+
nextIdx: i
|
|
592
|
+
} : null;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Like consumeValueNode but for implicit null-key entries (`: value`).
|
|
596
|
+
* If the next non-comment node is immediately followed by a value-sep
|
|
597
|
+
* AND is on a different line from the null key's `:`, it's actually a
|
|
598
|
+
* KEY for the next pair, not our value — return null so the null key
|
|
599
|
+
* gets a null value. When on the same line (e.g. `a: b: c: d`), consume
|
|
600
|
+
* normally to preserve the original pairing (which may produce duplicate
|
|
601
|
+
* keys that get rejected).
|
|
602
|
+
*/
|
|
603
|
+
function consumeValueNodeForNullKey(items, startIdx, text, valueSepOffset) {
|
|
604
|
+
let i = startIdx;
|
|
605
|
+
while (i < items.length) {
|
|
606
|
+
const item = items[i];
|
|
607
|
+
if (!item) break;
|
|
608
|
+
if (item.kind === "comment") {
|
|
609
|
+
i++;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (item.kind === "node") {
|
|
613
|
+
if (i + 1 < items.length && items[i + 1]?.kind === "value-sep") {
|
|
614
|
+
const nodeOffset = item.node && "offset" in item.node ? item.node.offset : 0;
|
|
615
|
+
if (text.slice(valueSepOffset, nodeOffset).includes("\n")) break;
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
node: item.node ?? null,
|
|
619
|
+
nextIdx: i + 1
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
return i > startIdx ? {
|
|
625
|
+
node: null,
|
|
626
|
+
nextIdx: i
|
|
627
|
+
} : null;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Canonical duplicate-key identity for a scalar key. Two keys collide only if
|
|
631
|
+
* they are the same YAML node: same type *and* value. Resolved JS values alone
|
|
632
|
+
* are ambiguous for numbers — `!!int 1` and `!!float 1.0` both become the JS
|
|
633
|
+
* number `1` yet are distinct keys — so the number branch disambiguates int
|
|
634
|
+
* from float via the source form (or an explicit tag). String/bool/null keys
|
|
635
|
+
* are distinguished by their type prefix, so `1` (int) never collides with
|
|
636
|
+
* `"1"` (string) or `true`.
|
|
637
|
+
*/
|
|
638
|
+
function keyIdentity(key, text) {
|
|
639
|
+
const v = key.value;
|
|
640
|
+
if (v === null) return "null";
|
|
641
|
+
switch (typeof v) {
|
|
642
|
+
case "boolean": return `b:${v}`;
|
|
643
|
+
case "string": return `s:${v}`;
|
|
644
|
+
case "bigint": return `i:${v.toString()}`;
|
|
645
|
+
case "number": {
|
|
646
|
+
let kind = classifyPlainNumeric(text.slice(key.offset, key.offset + key.length).trim());
|
|
647
|
+
if (key.tag !== void 0) {
|
|
648
|
+
if (key.tag.includes("float")) kind = "float";
|
|
649
|
+
else if (key.tag.includes("int")) kind = "int";
|
|
650
|
+
}
|
|
651
|
+
return `${kind === "float" ? "f" : "i"}:${v}`;
|
|
652
|
+
}
|
|
653
|
+
default: return `o:${String(v)}`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function checkDuplicateKeys(pairs, state) {
|
|
657
|
+
const seen = /* @__PURE__ */ new Set();
|
|
658
|
+
for (const pair of pairs) if (pair.key instanceof YamlScalar) {
|
|
659
|
+
const id = keyIdentity(pair.key, state.text);
|
|
660
|
+
if (seen.has(id)) state.warnings.push({
|
|
661
|
+
code: "DuplicateKey",
|
|
662
|
+
message: `Duplicate key: ${String(pair.key.value)}`,
|
|
663
|
+
offset: pair.key.offset,
|
|
664
|
+
length: pair.key.length
|
|
665
|
+
});
|
|
666
|
+
seen.add(id);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Inspect the slice of children after a `?` indicator to decide how the
|
|
671
|
+
* explicit key should be composed.
|
|
672
|
+
*
|
|
673
|
+
* - `terminated` — found a matching `:` at `qCol`; the key is the slice
|
|
674
|
+
* between `?` and that `:`. Existing per-node logic handles this.
|
|
675
|
+
* - `inline-implicit-map` — no matching `:` at `qCol`, but the slice
|
|
676
|
+
* contains a `:` at a deeper column. The whole slice is a compact
|
|
677
|
+
* inline implicit-map key (M2N8/00, M2N8/01).
|
|
678
|
+
* - `simple` — no internal `:` at all; the next content node is the
|
|
679
|
+
* single key (KK5P, M5DY block-seq keys; plain scalar keys).
|
|
680
|
+
*/
|
|
681
|
+
function scanExplicitKeyShape(children, qIdx, qCol, text) {
|
|
682
|
+
const qChild = children[qIdx];
|
|
683
|
+
const qLine = qChild ? lineCol(text, qChild.offset).line : -1;
|
|
684
|
+
let inlineColonOnQLine = false;
|
|
685
|
+
let endIdx = children.length;
|
|
686
|
+
for (let j = qIdx + 1; j < children.length; j++) {
|
|
687
|
+
const c = children[j];
|
|
688
|
+
if (!c) continue;
|
|
689
|
+
if (c.type === "whitespace" && c.source === ":") {
|
|
690
|
+
if (lineCol(text, c.offset).column === qCol) return {
|
|
691
|
+
kind: "terminated",
|
|
692
|
+
matchIdx: j
|
|
693
|
+
};
|
|
694
|
+
if (lineCol(text, c.offset).line === qLine) inlineColonOnQLine = true;
|
|
695
|
+
}
|
|
696
|
+
if (c.type === "whitespace" && c.source === "?") {
|
|
697
|
+
if (lineCol(text, c.offset).column === qCol) {
|
|
698
|
+
endIdx = j;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (inlineColonOnQLine) return {
|
|
704
|
+
kind: "inline-implicit-map",
|
|
705
|
+
endIdx
|
|
706
|
+
};
|
|
707
|
+
return { kind: "simple" };
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Returns true if a value at the given offset was introduced by a `?`
|
|
711
|
+
* explicit-key indicator. Scans backward through whitespace and newlines
|
|
712
|
+
* looking for a `?` at the start of a line (not part of a scalar).
|
|
713
|
+
*
|
|
714
|
+
* Named verbosely to avoid colliding with the local `isExplicitKey`
|
|
715
|
+
* boolean in `flattenBlockMapChildren` (used for multi-line key
|
|
716
|
+
* continuation detection — different concept).
|
|
717
|
+
*/
|
|
718
|
+
function wasIntroducedByExplicitKeyIndicator(text, offset) {
|
|
719
|
+
let i = offset - 1;
|
|
720
|
+
while (i >= 0) {
|
|
721
|
+
const ch = text[i];
|
|
722
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
723
|
+
i--;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (ch === "?") {
|
|
727
|
+
if (i === 0) return true;
|
|
728
|
+
const prev = text[i - 1];
|
|
729
|
+
return prev === " " || prev === " " || prev === "\n" || prev === "\r";
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Validate that implicit mapping keys do not span multiple lines.
|
|
737
|
+
* YAML 1.2 §7.4.2 requires implicit keys to fit on a single line.
|
|
738
|
+
*/
|
|
739
|
+
function checkMultilineImplicitKeys(pairs, state, items) {
|
|
740
|
+
for (const pair of pairs) {
|
|
741
|
+
const key = pair.key;
|
|
742
|
+
if (key.length === 0) continue;
|
|
743
|
+
if (key._tag === "YamlScalar") {
|
|
744
|
+
const s = key.style;
|
|
745
|
+
if (s !== "single-quoted" && s !== "double-quoted") continue;
|
|
746
|
+
const keySource = state.text.slice(key.offset, key.offset + key.length);
|
|
747
|
+
if (keySource.includes("\n") || keySource.includes("\r")) state.errors.push({
|
|
748
|
+
code: "UnexpectedToken",
|
|
749
|
+
message: "Implicit mapping key must not span multiple lines",
|
|
750
|
+
offset: key.offset,
|
|
751
|
+
length: key.length
|
|
752
|
+
});
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (key._tag === "YamlMap" || key._tag === "YamlSeq") {
|
|
756
|
+
if (key.style !== "flow") continue;
|
|
757
|
+
if (wasIntroducedByExplicitKeyIndicator(state.text, key.offset)) continue;
|
|
758
|
+
const keySource = state.text.slice(key.offset, key.offset + key.length);
|
|
759
|
+
if (keySource.includes("\n") || keySource.includes("\r")) state.errors.push({
|
|
760
|
+
code: "UnexpectedToken",
|
|
761
|
+
message: "Implicit mapping key must not span multiple lines",
|
|
762
|
+
offset: key.offset,
|
|
763
|
+
length: key.length
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (!items) return;
|
|
768
|
+
for (let i = 0; i < items.length; i++) {
|
|
769
|
+
const item = items[i];
|
|
770
|
+
if (!item) continue;
|
|
771
|
+
if (item.kind !== "node" && item.kind !== "key") continue;
|
|
772
|
+
const node = item.node;
|
|
773
|
+
if (node?._tag !== "YamlScalar" || node.length === 0) continue;
|
|
774
|
+
let j = i + 1;
|
|
775
|
+
while (j < items.length && items[j]?.kind === "comment") j++;
|
|
776
|
+
const next = items[j];
|
|
777
|
+
if (next?.kind !== "value-sep" || next.offset === void 0) continue;
|
|
778
|
+
if (lineCol(state.text, node.offset + node.length - 1).line !== lineCol(state.text, next.offset).line) state.errors.push({
|
|
779
|
+
code: "UnexpectedToken",
|
|
780
|
+
message: "Implicit mapping key and value indicator must be on the same line",
|
|
781
|
+
offset: node.offset,
|
|
782
|
+
length: node.length
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Check for non-trivial CST content on the same line after a completed value node.
|
|
788
|
+
* Used to detect trailing content after quoted scalars and flow collections.
|
|
789
|
+
* Skips if the next non-trivia content is a ":" (value-sep), since that means
|
|
790
|
+
* this node is actually a key, not a value.
|
|
791
|
+
*/
|
|
792
|
+
function checkTrailingContentOnSameLine(children, startIdx, valueNode, state) {
|
|
793
|
+
const valueEnd = valueNode.offset + valueNode.length;
|
|
794
|
+
for (let j = startIdx; j < children.length; j++) {
|
|
795
|
+
const next = children[j];
|
|
796
|
+
if (!next) continue;
|
|
797
|
+
if (next.type === "newline") break;
|
|
798
|
+
if (next.type === "comment") break;
|
|
799
|
+
if (next.type === "whitespace") {
|
|
800
|
+
if (next.source === ":") break;
|
|
801
|
+
if (next.source.trim() === "") continue;
|
|
802
|
+
}
|
|
803
|
+
if (sameLine(state.text, valueEnd - 1, next.offset)) state.errors.push({
|
|
804
|
+
code: "UnexpectedToken",
|
|
805
|
+
message: "Trailing content after value on same line",
|
|
806
|
+
offset: next.offset,
|
|
807
|
+
length: next.length
|
|
808
|
+
});
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* QB6E: continuation lines of a multi-line quoted scalar in value position
|
|
814
|
+
* must be indented past the parent key column.
|
|
815
|
+
*/
|
|
816
|
+
function validateQuotedScalarContinuationIndent(scalar, state, parentKeyColumn) {
|
|
817
|
+
if (parentKeyColumn < 0) return;
|
|
818
|
+
const text = state.text;
|
|
819
|
+
const start = scalar.offset;
|
|
820
|
+
const end = scalar.offset + scalar.length;
|
|
821
|
+
let i = start;
|
|
822
|
+
let inLineStart = false;
|
|
823
|
+
let lineStart = -1;
|
|
824
|
+
while (i < end && i < text.length) {
|
|
825
|
+
const ch = text[i];
|
|
826
|
+
if (ch === "\n") {
|
|
827
|
+
inLineStart = true;
|
|
828
|
+
lineStart = i + 1;
|
|
829
|
+
i++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (inLineStart) {
|
|
833
|
+
if (ch === " " || ch === " ") {
|
|
834
|
+
i++;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (i - lineStart <= parentKeyColumn) {
|
|
838
|
+
state.errors.push({
|
|
839
|
+
code: "InvalidIndentation",
|
|
840
|
+
message: "Multi-line quoted scalar continuation must be indented past the parent key",
|
|
841
|
+
offset: i,
|
|
842
|
+
length: 1
|
|
843
|
+
});
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
inLineStart = false;
|
|
847
|
+
}
|
|
848
|
+
i++;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* 4JVG: a single scalar cannot have two anchor declarations. When the
|
|
853
|
+
* outer-meta slot and the pending-meta slot both carry an anchor, AND the
|
|
854
|
+
* scalar is being consumed as a plain value (not as a key for a nested
|
|
855
|
+
* block-map, not followed by `:`), both anchors would collapse onto the
|
|
856
|
+
* scalar — that's invalid YAML.
|
|
857
|
+
*/
|
|
858
|
+
function validateNoDoubleAnchorOnScalar(scalar, children, idx, outerMeta, pendingMeta, state) {
|
|
859
|
+
if (outerMeta.anchor === void 0 || pendingMeta.anchor === void 0) return;
|
|
860
|
+
if (hasValueSepAfterInList(children, idx + 1)) return;
|
|
861
|
+
const nextContent = findNextContentInList(children, idx + 1);
|
|
862
|
+
if (nextContent?.node.type === "block-map" && !hasValueSepBetween(children, idx + 1, nextContent.idx)) return;
|
|
863
|
+
state.errors.push({
|
|
864
|
+
code: "UnexpectedToken",
|
|
865
|
+
message: "Scalar cannot have two anchor declarations",
|
|
866
|
+
offset: scalar.offset,
|
|
867
|
+
length: scalar.length
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Y79Y/009: when a `:` value-sep is at the start of a line (continuation
|
|
872
|
+
* line, not on the same line as a key) and is immediately followed by a
|
|
873
|
+
* tab and same-line content, the tab is being used as block indentation
|
|
874
|
+
* for the upcoming content — invalid per YAML 1.2 §6.1.
|
|
875
|
+
*/
|
|
876
|
+
function validateNoTabAfterContinuationValueSep(colonChild, children, idx, state) {
|
|
877
|
+
if (lineCol(state.text, colonChild.offset).column !== lineIndentColumn(state.text, colonChild.offset)) return;
|
|
878
|
+
let sawTab = false;
|
|
879
|
+
for (let j = idx + 1; j < children.length; j++) {
|
|
880
|
+
const c = children[j];
|
|
881
|
+
if (!c) continue;
|
|
882
|
+
if (c.type === "newline") return;
|
|
883
|
+
if (c.type === "whitespace") {
|
|
884
|
+
if (c.source.includes(" ")) sawTab = true;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (sawTab && sameLine(state.text, colonChild.offset, c.offset)) state.errors.push({
|
|
888
|
+
code: "TabIndentation",
|
|
889
|
+
message: "Tab character cannot be used as indentation after a value indicator",
|
|
890
|
+
offset: colonChild.offset,
|
|
891
|
+
length: colonChild.length
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Returns true when the upcoming "-" indicator is preceded (after only
|
|
898
|
+
* whitespace/newlines) by either an empty block-seq placeholder or a
|
|
899
|
+
* `?`-only block-map. The parser uses both shapes to mark "explicit key
|
|
900
|
+
* with a sequence as the key" (KK5P fixture).
|
|
901
|
+
*/
|
|
902
|
+
function precededByExplicitKeyMarker(children, idx) {
|
|
903
|
+
for (let j = idx - 1; j >= 0; j--) {
|
|
904
|
+
const c = children[j];
|
|
905
|
+
if (!c) continue;
|
|
906
|
+
if (c.type === "whitespace" || c.type === "newline" || c.type === "comment") continue;
|
|
907
|
+
if (c.type === "block-seq" && c.length === 0) return true;
|
|
908
|
+
if (c.type === "block-map" && c.source.trimEnd() === "?") return true;
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* G9HC, H7J7: anchor/tag in value position on a continuation line (not on
|
|
915
|
+
* the same line as `:`) must be at a column strictly greater than the
|
|
916
|
+
* parent key's column. Per YAML 1.2 §8.1.2, properties before a block
|
|
917
|
+
* collection must be at indent n+1, where n is the parent key column.
|
|
918
|
+
*/
|
|
919
|
+
function validatePropertyContinuationColumn(property, state, afterValueSep, lastValueSepOffset, parentKeyColumn) {
|
|
920
|
+
if (!afterValueSep) return;
|
|
921
|
+
if (parentKeyColumn < 0) return;
|
|
922
|
+
if (lastValueSepOffset >= 0 && sameLine(state.text, lastValueSepOffset, property.offset)) return;
|
|
923
|
+
if (lineCol(state.text, property.offset).column <= parentKeyColumn) state.errors.push({
|
|
924
|
+
code: "InvalidIndentation",
|
|
925
|
+
message: "Property (anchor or tag) must be indented past the parent key",
|
|
926
|
+
offset: property.offset,
|
|
927
|
+
length: property.length
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
function composeBlockSeq(cst, state, meta) {
|
|
931
|
+
if (!enterNesting(state, cst)) return new YamlSeq({
|
|
932
|
+
items: [],
|
|
933
|
+
style: "block",
|
|
934
|
+
offset: cst.offset,
|
|
935
|
+
length: cst.length
|
|
936
|
+
});
|
|
937
|
+
try {
|
|
938
|
+
return composeBlockSeqInner(cst, state, meta);
|
|
939
|
+
} finally {
|
|
940
|
+
exitNesting(state);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function composeBlockSeqInner(cst, state, meta) {
|
|
944
|
+
const children = cst.children ?? [];
|
|
945
|
+
const items = [];
|
|
946
|
+
let pendingMeta = {};
|
|
947
|
+
let sawEntry = false;
|
|
948
|
+
const seqIndent = lineIndentColumn(state.text, cst.offset);
|
|
949
|
+
let sawNewlineSincePending = false;
|
|
950
|
+
for (let ci = 0; ci < children.length; ci++) {
|
|
951
|
+
const child = children[ci];
|
|
952
|
+
if (!child) continue;
|
|
953
|
+
if (child.type === "newline") {
|
|
954
|
+
if (hasMeta(pendingMeta)) sawNewlineSincePending = true;
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (child.type === "comment") continue;
|
|
958
|
+
if (child.type === "whitespace") {
|
|
959
|
+
if (child.source.trim() === "-") {
|
|
960
|
+
if (sawEntry) {
|
|
961
|
+
const emptyScalar = new YamlScalar({
|
|
962
|
+
value: null,
|
|
963
|
+
style: "plain",
|
|
964
|
+
offset: child.offset,
|
|
965
|
+
length: 0,
|
|
966
|
+
...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
|
|
967
|
+
...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
|
|
968
|
+
});
|
|
969
|
+
if (pendingMeta.anchor) registerAnchor(emptyScalar, pendingMeta.anchor, state, child.offset);
|
|
970
|
+
pendingMeta = {};
|
|
971
|
+
items.push(emptyScalar);
|
|
972
|
+
}
|
|
973
|
+
sawEntry = true;
|
|
974
|
+
}
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (child.type === "error") {
|
|
978
|
+
state.errors.push({
|
|
979
|
+
code: "UnexpectedToken",
|
|
980
|
+
message: `Unexpected content: ${child.source.trim() || "(empty)"}`,
|
|
981
|
+
offset: child.offset,
|
|
982
|
+
length: child.length
|
|
983
|
+
});
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (child.type === "anchor") {
|
|
987
|
+
pendingMeta.anchor = getAnchorName(child, state.text);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (child.type === "tag") {
|
|
991
|
+
pendingMeta.tag = child.source;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (child.type === "flow-scalar" || child.type === "block-scalar") {
|
|
995
|
+
const nextSig = findNextSignificantChild(children, ci + 1, true);
|
|
996
|
+
const nextSigChild = nextSig !== null ? children[nextSig] : void 0;
|
|
997
|
+
if (nextSig !== null && nextSigChild && nextSigChild.type === "block-map") {
|
|
998
|
+
let keyMeta = hasMeta(pendingMeta) ? pendingMeta : void 0;
|
|
999
|
+
let mapMeta;
|
|
1000
|
+
if (sawNewlineSincePending && hasMeta(pendingMeta)) {
|
|
1001
|
+
mapMeta = pendingMeta;
|
|
1002
|
+
keyMeta = void 0;
|
|
1003
|
+
}
|
|
1004
|
+
const map = composeBlockMap(nextSigChild, state, makeScalar(child, state, keyMeta), mapMeta);
|
|
1005
|
+
pendingMeta = {};
|
|
1006
|
+
sawNewlineSincePending = false;
|
|
1007
|
+
sawEntry = false;
|
|
1008
|
+
items.push(map);
|
|
1009
|
+
ci = nextSig;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (child.type === "flow-scalar" && getScalarStyle(child) === "plain") {
|
|
1013
|
+
const { value: merged, nextIdx: mergeEnd, partsCount, endOffset } = collectMultilinePlainScalar(children, ci, void 0, state.text);
|
|
1014
|
+
if (partsCount > 1) {
|
|
1015
|
+
const scalar = new YamlScalar({
|
|
1016
|
+
value: resolveScalar(merged, "plain", pendingMeta.tag, state),
|
|
1017
|
+
style: "plain",
|
|
1018
|
+
offset: child.offset,
|
|
1019
|
+
length: endOffset - child.offset,
|
|
1020
|
+
...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
|
|
1021
|
+
...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
|
|
1022
|
+
});
|
|
1023
|
+
if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, child.offset);
|
|
1024
|
+
pendingMeta = {};
|
|
1025
|
+
sawEntry = false;
|
|
1026
|
+
items.push(scalar);
|
|
1027
|
+
ci = mergeEnd - 1;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const scalar = makeScalar(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0);
|
|
1032
|
+
pendingMeta = {};
|
|
1033
|
+
sawNewlineSincePending = false;
|
|
1034
|
+
sawEntry = false;
|
|
1035
|
+
items.push(scalar);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (child.type === "alias") {
|
|
1039
|
+
checkAnchorOnAlias(pendingMeta, child, state);
|
|
1040
|
+
const alias = makeAlias(child, state);
|
|
1041
|
+
pendingMeta = {};
|
|
1042
|
+
sawNewlineSincePending = false;
|
|
1043
|
+
sawEntry = false;
|
|
1044
|
+
items.push(alias);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (child.type === "block-map") {
|
|
1048
|
+
if (hasMeta(pendingMeta) && blockMapStartsWithValueSep(child)) {
|
|
1049
|
+
const emptyKey = new YamlScalar({
|
|
1050
|
+
value: null,
|
|
1051
|
+
style: "plain",
|
|
1052
|
+
offset: child.offset,
|
|
1053
|
+
length: 0,
|
|
1054
|
+
...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
|
|
1055
|
+
...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
|
|
1056
|
+
});
|
|
1057
|
+
if (pendingMeta.anchor) registerAnchor(emptyKey, pendingMeta.anchor, state, child.offset);
|
|
1058
|
+
pendingMeta = {};
|
|
1059
|
+
sawNewlineSincePending = false;
|
|
1060
|
+
const map = composeBlockMap(child, state, emptyKey, void 0);
|
|
1061
|
+
sawEntry = false;
|
|
1062
|
+
items.push(map);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const map = composeBlockMap(child, state, void 0, hasMeta(pendingMeta) ? pendingMeta : void 0);
|
|
1066
|
+
pendingMeta = {};
|
|
1067
|
+
sawNewlineSincePending = false;
|
|
1068
|
+
sawEntry = false;
|
|
1069
|
+
items.push(map);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (child.type === "block-seq") {
|
|
1073
|
+
const seq = composeBlockSeq(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0);
|
|
1074
|
+
pendingMeta = {};
|
|
1075
|
+
sawNewlineSincePending = false;
|
|
1076
|
+
sawEntry = false;
|
|
1077
|
+
items.push(seq);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (child.type === "flow-map") {
|
|
1081
|
+
const map = state.flow.composeFlowMap(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0, seqIndent);
|
|
1082
|
+
pendingMeta = {};
|
|
1083
|
+
sawEntry = false;
|
|
1084
|
+
items.push(map);
|
|
1085
|
+
checkTrailingContentOnSameLine(children, ci + 1, child, state);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (child.type === "flow-seq") {
|
|
1089
|
+
const seq = state.flow.composeFlowSeq(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0, seqIndent);
|
|
1090
|
+
pendingMeta = {};
|
|
1091
|
+
sawEntry = false;
|
|
1092
|
+
items.push(seq);
|
|
1093
|
+
checkTrailingContentOnSameLine(children, ci + 1, child, state);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (sawEntry && !hasMeta(pendingMeta)) items.push(new YamlScalar({
|
|
1097
|
+
value: null,
|
|
1098
|
+
style: "plain",
|
|
1099
|
+
offset: cst.offset + cst.length,
|
|
1100
|
+
length: 0
|
|
1101
|
+
}));
|
|
1102
|
+
if (hasMeta(pendingMeta)) {
|
|
1103
|
+
const scalar = new YamlScalar({
|
|
1104
|
+
value: resolveScalar("", "plain", pendingMeta.tag, state),
|
|
1105
|
+
style: "plain",
|
|
1106
|
+
offset: 0,
|
|
1107
|
+
length: 0,
|
|
1108
|
+
...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
|
|
1109
|
+
...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
|
|
1110
|
+
});
|
|
1111
|
+
if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, 0);
|
|
1112
|
+
items.push(scalar);
|
|
1113
|
+
}
|
|
1114
|
+
const seq = new YamlSeq({
|
|
1115
|
+
items,
|
|
1116
|
+
style: "block",
|
|
1117
|
+
offset: cst.offset,
|
|
1118
|
+
length: cst.length,
|
|
1119
|
+
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
1120
|
+
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
1121
|
+
...meta?.comment !== void 0 ? { comment: meta.comment } : {}
|
|
1122
|
+
});
|
|
1123
|
+
if (meta?.anchor) registerAnchor(seq, meta.anchor, state, cst.offset);
|
|
1124
|
+
return seq;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Compose a block map from flat document children (no block-map wrapper node).
|
|
1128
|
+
* This happens in multi-document scenarios where the parser doesn't create a block-map node.
|
|
1129
|
+
*/
|
|
1130
|
+
function composeFlatBlockMap(children, startIdx, parentCst, state, externalFirstKey, meta) {
|
|
1131
|
+
const items = flattenBlockMapChildren(children.slice(startIdx), state);
|
|
1132
|
+
items.unshift({
|
|
1133
|
+
kind: "key",
|
|
1134
|
+
node: externalFirstKey
|
|
1135
|
+
});
|
|
1136
|
+
const pairs = [];
|
|
1137
|
+
buildPairs(items, pairs, state.text);
|
|
1138
|
+
if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
|
|
1139
|
+
checkMultilineImplicitKeys(pairs, state);
|
|
1140
|
+
const offset = "offset" in externalFirstKey ? externalFirstKey.offset : parentCst.offset;
|
|
1141
|
+
const map = new YamlMap({
|
|
1142
|
+
items: pairs,
|
|
1143
|
+
style: "block",
|
|
1144
|
+
offset,
|
|
1145
|
+
length: parentCst.offset + parentCst.length - offset,
|
|
1146
|
+
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
1147
|
+
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
1148
|
+
...meta?.comment !== void 0 ? { comment: meta.comment } : {}
|
|
1149
|
+
});
|
|
1150
|
+
if (meta?.anchor) registerAnchor(map, meta.anchor, state, offset);
|
|
1151
|
+
return map;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
//#endregion
|
|
1155
|
+
export { buildPairs, checkDuplicateKeys, checkMultilineImplicitKeys, checkTrailingContentOnSameLine, composeBlockMap, composeBlockSeq, composeFlatBlockMap, flattenBlockMapChildren };
|