@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,400 @@
1
+ import { YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
2
+ import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
3
+ import { enterNesting, exitNesting, hasMeta } from "./state.js";
4
+ import { collectMultilineKey, collectMultilinePlainScalar, getScalarStyle, hasValueSepThroughPlainScalars, makeScalar, resolveScalar } from "./scalars.js";
5
+ import { buildPairs, checkDuplicateKeys, checkMultilineImplicitKeys } from "./block.js";
6
+
7
+ //#region src/internal/composer/flow.ts
8
+ /**
9
+ * Validate that flow collection entries are separated by commas.
10
+ *
11
+ * Detects the specific pattern: content, `:`, content, content (no comma).
12
+ * This catches `{foo: 1 bar: 2}` while allowing multiline plain scalars
13
+ * like `{multi\n line: value}` (consecutive scalars without colon between).
14
+ *
15
+ * State machine: idle → saw-colon → saw-value → error-if-no-comma
16
+ */
17
+ function validateFlowSeparators(children, state, openBracket, closeBracket) {
18
+ let colonCount = 0;
19
+ let contentAfterColon = 0;
20
+ for (const child of children) {
21
+ if (child.type === "whitespace" && (child.source === openBracket || child.source === closeBracket)) continue;
22
+ if (child.type === "newline") continue;
23
+ if (child.type === "comment") {
24
+ if (contentAfterColon > 0) {
25
+ colonCount = 1;
26
+ contentAfterColon = 1;
27
+ }
28
+ continue;
29
+ }
30
+ if (child.type === "whitespace" && child.source.trim() === "") continue;
31
+ if (child.type === "whitespace" && child.source === ",") {
32
+ colonCount = 0;
33
+ contentAfterColon = 0;
34
+ continue;
35
+ }
36
+ if (child.type === "whitespace" && child.source === ":") {
37
+ colonCount++;
38
+ contentAfterColon = 0;
39
+ continue;
40
+ }
41
+ if (child.type === "flow-scalar" || child.type === "block-scalar" || child.type === "flow-map" || child.type === "flow-seq" || child.type === "alias") {
42
+ contentAfterColon++;
43
+ if (colonCount > 0 && contentAfterColon > 1) state.errors.push({
44
+ code: "MalformedFlowCollection",
45
+ message: "Missing comma between flow collection entries",
46
+ offset: child.offset,
47
+ length: child.length
48
+ });
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * 9C9N, VJP3/00: continuation lines of multi-line flow content must be
54
+ * indented past the parent block context. Caller passes the parent block
55
+ * column; for document-root flow content (no parent block) the caller
56
+ * omits it and the check is skipped.
57
+ */
58
+ function validateFlowContentIndent(cst, state, parentBlockColumn) {
59
+ if (parentBlockColumn === void 0 || parentBlockColumn < 0) return;
60
+ const text = state.text;
61
+ const start = cst.offset;
62
+ const end = cst.offset + cst.length;
63
+ let i = start;
64
+ let inLineStart = false;
65
+ let lineStart = -1;
66
+ while (i < end && i < text.length) {
67
+ const ch = text[i];
68
+ if (ch === "\n") {
69
+ inLineStart = true;
70
+ lineStart = i + 1;
71
+ i++;
72
+ continue;
73
+ }
74
+ if (inLineStart) {
75
+ if (ch === " " || ch === " ") {
76
+ i++;
77
+ continue;
78
+ }
79
+ if (i - lineStart <= parentBlockColumn) {
80
+ state.errors.push({
81
+ code: "InvalidIndentation",
82
+ message: "Flow content continuation line must be indented past the parent block",
83
+ offset: i,
84
+ length: 1
85
+ });
86
+ return;
87
+ }
88
+ inLineStart = false;
89
+ }
90
+ i++;
91
+ }
92
+ }
93
+ function composeFlowMap(cst, state, meta, parentBlockColumn) {
94
+ if (!enterNesting(state, cst)) return new YamlMap({
95
+ items: [],
96
+ style: "flow",
97
+ offset: cst.offset,
98
+ length: cst.length
99
+ });
100
+ try {
101
+ return composeFlowMapInner(cst, state, meta, parentBlockColumn);
102
+ } finally {
103
+ exitNesting(state);
104
+ }
105
+ }
106
+ function composeFlowMapInner(cst, state, meta, parentBlockColumn) {
107
+ const children = cst.children ?? [];
108
+ const pairs = [];
109
+ const hasOpen = children.some((c) => c.type === "whitespace" && c.source === "{");
110
+ const hasClose = children.some((c) => c.type === "whitespace" && c.source === "}");
111
+ if (hasOpen && !hasClose) state.errors.push({
112
+ code: "MalformedFlowCollection",
113
+ message: "Unclosed flow mapping (missing `}`)",
114
+ offset: cst.offset,
115
+ length: cst.length
116
+ });
117
+ validateFlowContentIndent(cst, state, parentBlockColumn);
118
+ validateFlowSeparators(children, state, "{", "}");
119
+ buildPairs(flattenFlowChildren(children.filter((c) => !(c.type === "whitespace" && (c.source === "{" || c.source === "}" || c.source.trim() === ""))), state), pairs, state.text);
120
+ if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
121
+ const map = new YamlMap({
122
+ items: pairs,
123
+ style: "flow",
124
+ offset: cst.offset,
125
+ length: cst.length,
126
+ ...meta?.tag !== void 0 ? { tag: meta.tag } : {},
127
+ ...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
128
+ ...meta?.comment !== void 0 ? { comment: meta.comment } : {}
129
+ });
130
+ if (meta?.anchor) registerAnchor(map, meta.anchor, state, cst.offset);
131
+ return map;
132
+ }
133
+ function flattenFlowChildren(children, state) {
134
+ const items = [];
135
+ let pendingMeta = {};
136
+ for (let i = 0; i < children.length; i++) {
137
+ const child = children[i];
138
+ if (!child) continue;
139
+ if (child.type === "newline") continue;
140
+ if (child.type === "whitespace") {
141
+ if (child.source === ",") {
142
+ if (hasMeta(pendingMeta)) {
143
+ const scalar = new YamlScalar({
144
+ value: resolveScalar("", "plain", pendingMeta.tag, state),
145
+ style: "plain",
146
+ offset: child.offset,
147
+ length: 0,
148
+ ...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
149
+ ...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
150
+ });
151
+ if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, child.offset);
152
+ pendingMeta = {};
153
+ items.push({
154
+ kind: "node",
155
+ node: scalar
156
+ });
157
+ }
158
+ continue;
159
+ }
160
+ if (child.source === ":") {
161
+ if (hasMeta(pendingMeta)) {
162
+ const scalar = new YamlScalar({
163
+ value: resolveScalar("", "plain", pendingMeta.tag, state),
164
+ style: "plain",
165
+ offset: child.offset,
166
+ length: 0,
167
+ ...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
168
+ ...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
169
+ });
170
+ if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, child.offset);
171
+ pendingMeta = {};
172
+ items.push({
173
+ kind: "node",
174
+ node: scalar
175
+ });
176
+ }
177
+ items.push({
178
+ kind: "value-sep",
179
+ offset: child.offset
180
+ });
181
+ }
182
+ if (child.source === "?") items.push({ kind: "key" });
183
+ continue;
184
+ }
185
+ if (child.type === "comment") {
186
+ items.push({
187
+ kind: "comment",
188
+ comment: child.source.startsWith("#") ? child.source.slice(1).trim() : child.source
189
+ });
190
+ continue;
191
+ }
192
+ if (child.type === "error") {
193
+ state.errors.push({
194
+ code: "UnexpectedToken",
195
+ message: `Unexpected content: ${child.source.trim() || "(empty)"}`,
196
+ offset: child.offset,
197
+ length: child.length
198
+ });
199
+ continue;
200
+ }
201
+ if (child.type === "anchor") {
202
+ pendingMeta.anchor = getAnchorName(child, state.text);
203
+ continue;
204
+ }
205
+ if (child.type === "tag") {
206
+ pendingMeta.tag = child.source;
207
+ continue;
208
+ }
209
+ if (child.type === "flow-scalar" || child.type === "block-scalar") {
210
+ if (child.type === "flow-scalar" && getScalarStyle(child) === "plain" && child.source.startsWith("#") && child.offset > 0) {
211
+ const prev = state.text[child.offset - 1];
212
+ if (prev !== " " && prev !== " " && prev !== "\n" && prev !== "\r") state.errors.push({
213
+ code: "UnexpectedToken",
214
+ message: "Comment must be preceded by whitespace",
215
+ offset: child.offset,
216
+ length: child.length
217
+ });
218
+ }
219
+ if (child.type === "flow-scalar" && getScalarStyle(child) === "plain" && (child.source === "-" || child.source === "?")) state.errors.push({
220
+ code: "UnexpectedToken",
221
+ message: `Invalid plain scalar '${child.source}' in flow context`,
222
+ offset: child.offset,
223
+ length: child.length
224
+ });
225
+ if (child.type === "flow-scalar" && getScalarStyle(child) === "plain") {
226
+ if (hasValueSepThroughPlainScalars(children, i + 1)) {
227
+ const { value, nextIdx } = collectMultilineKey(children, i);
228
+ const scalar = new YamlScalar({
229
+ value: resolveScalar(value, "plain", pendingMeta.tag, state),
230
+ style: "plain",
231
+ offset: child.offset,
232
+ length: child.length,
233
+ ...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
234
+ ...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
235
+ });
236
+ if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, child.offset);
237
+ pendingMeta = {};
238
+ items.push({
239
+ kind: "node",
240
+ node: scalar
241
+ });
242
+ i = nextIdx - 1;
243
+ continue;
244
+ }
245
+ const { value, nextIdx, partsCount, endOffset } = collectMultilinePlainScalar(children, i, void 0, state.text);
246
+ const scalar = new YamlScalar({
247
+ value: resolveScalar(value, "plain", pendingMeta.tag, state),
248
+ style: "plain",
249
+ offset: child.offset,
250
+ length: partsCount > 1 ? endOffset - child.offset : child.length,
251
+ ...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
252
+ ...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
253
+ });
254
+ if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, child.offset);
255
+ pendingMeta = {};
256
+ items.push({
257
+ kind: "node",
258
+ node: scalar
259
+ });
260
+ i = nextIdx - 1;
261
+ continue;
262
+ }
263
+ const scalar = makeScalar(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0);
264
+ pendingMeta = {};
265
+ items.push({
266
+ kind: "node",
267
+ node: scalar
268
+ });
269
+ continue;
270
+ }
271
+ if (child.type === "alias") {
272
+ checkAnchorOnAlias(pendingMeta, child, state);
273
+ const alias = makeAlias(child, state);
274
+ pendingMeta = {};
275
+ items.push({
276
+ kind: "node",
277
+ node: alias
278
+ });
279
+ continue;
280
+ }
281
+ if (child.type === "flow-map") {
282
+ const map = composeFlowMap(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0);
283
+ pendingMeta = {};
284
+ items.push({
285
+ kind: "node",
286
+ node: map
287
+ });
288
+ continue;
289
+ }
290
+ if (child.type === "flow-seq") {
291
+ const seq = composeFlowSeq(child, state, hasMeta(pendingMeta) ? pendingMeta : void 0);
292
+ pendingMeta = {};
293
+ items.push({
294
+ kind: "node",
295
+ node: seq
296
+ });
297
+ }
298
+ }
299
+ if (hasMeta(pendingMeta)) {
300
+ const scalar = new YamlScalar({
301
+ value: resolveScalar("", "plain", pendingMeta.tag, state),
302
+ style: "plain",
303
+ offset: 0,
304
+ length: 0,
305
+ ...pendingMeta.tag !== void 0 ? { tag: pendingMeta.tag } : {},
306
+ ...pendingMeta.anchor !== void 0 ? { anchor: pendingMeta.anchor } : {}
307
+ });
308
+ if (pendingMeta.anchor) registerAnchor(scalar, pendingMeta.anchor, state, 0);
309
+ items.push({
310
+ kind: "node",
311
+ node: scalar
312
+ });
313
+ }
314
+ return items;
315
+ }
316
+ function composeFlowSeq(cst, state, meta, parentBlockColumn) {
317
+ if (!enterNesting(state, cst)) return new YamlSeq({
318
+ items: [],
319
+ style: "flow",
320
+ offset: cst.offset,
321
+ length: cst.length
322
+ });
323
+ try {
324
+ return composeFlowSeqInner(cst, state, meta, parentBlockColumn);
325
+ } finally {
326
+ exitNesting(state);
327
+ }
328
+ }
329
+ function composeFlowSeqInner(cst, state, meta, parentBlockColumn) {
330
+ const children = cst.children ?? [];
331
+ const items = [];
332
+ validateFlowContentIndent(cst, state, parentBlockColumn);
333
+ validateFlowSeparators(children, state, "[", "]");
334
+ const hasOpen = children.some((c) => c.type === "whitespace" && c.source === "[");
335
+ const hasClose = children.some((c) => c.type === "whitespace" && c.source === "]");
336
+ if (hasOpen && !hasClose) state.errors.push({
337
+ code: "MalformedFlowCollection",
338
+ message: "Unclosed flow sequence (missing `]`)",
339
+ offset: cst.offset,
340
+ length: cst.length
341
+ });
342
+ const segments = [];
343
+ let current = [];
344
+ let seenContent = false;
345
+ let lastWasComma = false;
346
+ for (const child of children) {
347
+ if (child.type === "whitespace" && (child.source === "[" || child.source === "]")) continue;
348
+ if (child.type === "whitespace" && child.source === ",") {
349
+ if (!current.some((c) => c.type !== "whitespace" && c.type !== "newline" && c.type !== "comment") && (lastWasComma || !seenContent)) state.errors.push({
350
+ code: "MalformedFlowCollection",
351
+ message: "Empty entry in flow sequence",
352
+ offset: child.offset,
353
+ length: 1
354
+ });
355
+ if (current.length > 0) segments.push(current);
356
+ current = [];
357
+ lastWasComma = true;
358
+ continue;
359
+ }
360
+ if (child.type !== "whitespace" && child.type !== "newline" && child.type !== "comment") {
361
+ seenContent = true;
362
+ lastWasComma = false;
363
+ }
364
+ current.push(child);
365
+ }
366
+ if (current.length > 0) segments.push(current);
367
+ for (const segment of segments) if (segment.some((c) => c.type === "whitespace" && c.source === ":")) {
368
+ const semItems = flattenFlowChildren(segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === "")), state);
369
+ const pairs = [];
370
+ buildPairs(semItems, pairs, state.text);
371
+ if (!segment.some((c) => c.type === "whitespace" && c.source === "?")) checkMultilineImplicitKeys(pairs, state, semItems);
372
+ const firstPair = pairs[0];
373
+ if (firstPair) {
374
+ const map = new YamlMap({
375
+ items: pairs,
376
+ style: "flow",
377
+ offset: firstPair.key.offset,
378
+ length: 0
379
+ });
380
+ items.push(map);
381
+ }
382
+ } else {
383
+ const semItems = flattenFlowChildren(segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === "")), state);
384
+ for (const si of semItems) if (si.kind === "node" && si.node) items.push(si.node);
385
+ }
386
+ const seq = new YamlSeq({
387
+ items,
388
+ style: "flow",
389
+ offset: cst.offset,
390
+ length: cst.length,
391
+ ...meta?.tag !== void 0 ? { tag: meta.tag } : {},
392
+ ...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
393
+ ...meta?.comment !== void 0 ? { comment: meta.comment } : {}
394
+ });
395
+ if (meta?.anchor) registerAnchor(seq, meta.anchor, state, cst.offset);
396
+ return seq;
397
+ }
398
+
399
+ //#endregion
400
+ export { composeFlowMap, composeFlowSeq, flattenFlowChildren };