@untangle-code/context-artifacts 0.0.0-stage → 0.1.1
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 +661 -0
- package/NOTICE +90 -0
- package/README.md +247 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/adapters/asyncapi.d.ts +20 -0
- package/dist/adapters/asyncapi.d.ts.map +1 -0
- package/dist/adapters/asyncapi.js +95 -0
- package/dist/adapters/asyncapi.js.map +1 -0
- package/dist/adapters/docker.d.ts +43 -0
- package/dist/adapters/docker.d.ts.map +1 -0
- package/dist/adapters/docker.js +97 -0
- package/dist/adapters/docker.js.map +1 -0
- package/dist/adapters/kubernetes.d.ts +22 -0
- package/dist/adapters/kubernetes.d.ts.map +1 -0
- package/dist/adapters/kubernetes.js +56 -0
- package/dist/adapters/kubernetes.js.map +1 -0
- package/dist/adapters/markdown.d.ts +35 -0
- package/dist/adapters/markdown.d.ts.map +1 -0
- package/dist/adapters/markdown.js +36 -0
- package/dist/adapters/markdown.js.map +1 -0
- package/dist/adapters/openapi.d.ts +21 -0
- package/dist/adapters/openapi.d.ts.map +1 -0
- package/dist/adapters/openapi.js +101 -0
- package/dist/adapters/openapi.js.map +1 -0
- package/dist/adapters/protobuf.d.ts +34 -0
- package/dist/adapters/protobuf.d.ts.map +1 -0
- package/dist/adapters/protobuf.js +221 -0
- package/dist/adapters/protobuf.js.map +1 -0
- package/dist/adapters/shared.d.ts +65 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +95 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/sql.d.ts +104 -0
- package/dist/adapters/sql.d.ts.map +1 -0
- package/dist/adapters/sql.js +427 -0
- package/dist/adapters/sql.js.map +1 -0
- package/dist/adapters/terraform.d.ts +40 -0
- package/dist/adapters/terraform.d.ts.map +1 -0
- package/dist/adapters/terraform.js +186 -0
- package/dist/adapters/terraform.js.map +1 -0
- package/dist/adapters/text.d.ts +24 -0
- package/dist/adapters/text.d.ts.map +1 -0
- package/dist/adapters/text.js +31 -0
- package/dist/adapters/text.js.map +1 -0
- package/dist/config-validation.d.ts +18 -0
- package/dist/config-validation.d.ts.map +1 -0
- package/dist/config-validation.js +30 -0
- package/dist/config-validation.js.map +1 -0
- package/dist/coverage.d.ts +114 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +0 -0
- package/dist/coverage.js.map +1 -0
- package/dist/ids.d.ts +12 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +42 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +51 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +86 -0
- package/dist/registry.js.map +1 -0
- package/dist/staleness.d.ts +44 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +35 -0
- package/dist/staleness.js.map +1 -0
- package/dist/text/delimiter-balance.d.ts +95 -0
- package/dist/text/delimiter-balance.d.ts.map +1 -0
- package/dist/text/delimiter-balance.js +209 -0
- package/dist/text/delimiter-balance.js.map +1 -0
- package/dist/text/line-index.d.ts +61 -0
- package/dist/text/line-index.d.ts.map +1 -0
- package/dist/text/line-index.js +75 -0
- package/dist/text/line-index.js.map +1 -0
- package/dist/text/yaml-lite.d.ts +73 -0
- package/dist/text/yaml-lite.d.ts.map +1 -0
- package/dist/text/yaml-lite.js +486 -0
- package/dist/text/yaml-lite.js.map +1 -0
- package/package.json +61 -3
- package/src/adapters/__golden__/adr-0001-sections.json +68 -0
- package/src/adapters/__golden__/app-compose-services.json +24 -0
- package/src/adapters/__golden__/app-image-stages.json +24 -0
- package/src/adapters/__golden__/infra-main-sections.json +46 -0
- package/src/adapters/__golden__/orders-events-operations.json +24 -0
- package/src/adapters/__golden__/orders-proto-sections.json +57 -0
- package/src/adapters/__golden__/orders-schema-statements.json +68 -0
- package/src/adapters/__golden__/users-api-operations.json +46 -0
- package/src/adapters/__golden__/web-manifests-resources.json +24 -0
- package/src/adapters/asyncapi.ts +147 -0
- package/src/adapters/docker.ts +170 -0
- package/src/adapters/kubernetes.ts +103 -0
- package/src/adapters/markdown.ts +88 -0
- package/src/adapters/openapi.ts +156 -0
- package/src/adapters/protobuf.ts +286 -0
- package/src/adapters/shared.ts +151 -0
- package/src/adapters/sql.ts +555 -0
- package/src/adapters/terraform.ts +261 -0
- package/src/adapters/text.ts +72 -0
- package/src/config-validation.ts +41 -0
- package/src/coverage.ts +336 -0
- package/src/ids.ts +57 -0
- package/src/index.ts +105 -0
- package/src/registry.ts +155 -0
- package/src/staleness.ts +69 -0
- package/src/text/delimiter-balance.ts +295 -0
- package/src/text/line-index.ts +108 -0
- package/src/text/yaml-lite.ts +565 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal block-style YAML subset parser.
|
|
3
|
+
*
|
|
4
|
+
* No YAML/JSON library is added here deliberately (see README
|
|
5
|
+
* "Entscheidungen"): this package's assigned paths cannot touch the
|
|
6
|
+
* workspace root lockfile, so pulling in an external dependency mid-wave
|
|
7
|
+
* would require an edit no other agent could safely race against. Instead
|
|
8
|
+
* this module implements exactly the YAML subset OpenAPI/AsyncAPI/
|
|
9
|
+
* Kubernetes/Compose documents actually use in practice: block mappings
|
|
10
|
+
* and sequences via indentation, single-line flow collections (`{a: 1}`,
|
|
11
|
+
* `[a, b]`), plain/single/double-quoted scalars, `|`/`>` block scalars,
|
|
12
|
+
* `#` comments, and `---`/`...` document separators. NOT supported:
|
|
13
|
+
* anchors/aliases, tags, multi-line flow collections, complex mapping
|
|
14
|
+
* keys. A document using one of those fails to parse — `parseYamlNode`
|
|
15
|
+
* throws `YamlParseError`, which every adapter using this module catches
|
|
16
|
+
* and turns into a visible degrade (architecture.md §22.2: "Parserfehler
|
|
17
|
+
* degradieren auf Textanalyse"), never a crash.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type YamlScalarValue = string | number | boolean | null;
|
|
21
|
+
|
|
22
|
+
export interface YamlScalarNode {
|
|
23
|
+
kind: "scalar";
|
|
24
|
+
startLine: number;
|
|
25
|
+
endLine: number;
|
|
26
|
+
value: YamlScalarValue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface YamlMapEntry {
|
|
30
|
+
key: string;
|
|
31
|
+
keyLine: number;
|
|
32
|
+
value: YamlNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface YamlMapNode {
|
|
36
|
+
kind: "map";
|
|
37
|
+
startLine: number;
|
|
38
|
+
endLine: number;
|
|
39
|
+
entries: YamlMapEntry[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface YamlSeqNode {
|
|
43
|
+
kind: "seq";
|
|
44
|
+
startLine: number;
|
|
45
|
+
endLine: number;
|
|
46
|
+
items: YamlNode[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type YamlNode = YamlScalarNode | YamlMapNode | YamlSeqNode;
|
|
50
|
+
|
|
51
|
+
export class YamlParseError extends Error {
|
|
52
|
+
constructor(
|
|
53
|
+
message: string,
|
|
54
|
+
public readonly line: number,
|
|
55
|
+
) {
|
|
56
|
+
super(`line ${line}: ${message}`);
|
|
57
|
+
this.name = "YamlParseError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One `---`-separated document within a (possibly multi-document) YAML file. */
|
|
62
|
+
export interface YamlDocumentSpan {
|
|
63
|
+
startLine: number;
|
|
64
|
+
endLine: number;
|
|
65
|
+
text: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Splits raw content into `---`-separated document spans (Kubernetes
|
|
70
|
+
* manifests, spec.md §19 "je Dokument eine Ressource"). A leading `---` is
|
|
71
|
+
* just the start of the first document, not an empty document before it. A
|
|
72
|
+
* trailing `...` end-marker line is treated the same as `---` for the
|
|
73
|
+
* purpose of ending a document.
|
|
74
|
+
*/
|
|
75
|
+
export function splitYamlDocuments(content: string): YamlDocumentSpan[] {
|
|
76
|
+
const lines = content.split("\n");
|
|
77
|
+
const spans: YamlDocumentSpan[] = [];
|
|
78
|
+
let start = 0; // 0-based index into `lines`
|
|
79
|
+
const flush = (endExclusive: number): void => {
|
|
80
|
+
const body = lines.slice(start, endExclusive).join("\n");
|
|
81
|
+
if (body.trim().length > 0) {
|
|
82
|
+
spans.push({ startLine: start + 1, endLine: endExclusive, text: body });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
86
|
+
if (/^---\s*$/.test(lines[i] ?? "") || /^\.\.\.\s*$/.test(lines[i] ?? "")) {
|
|
87
|
+
flush(i);
|
|
88
|
+
start = i + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
flush(lines.length);
|
|
92
|
+
return spans;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface PreparedLine {
|
|
96
|
+
/** 1-based line number in the ORIGINAL document text passed to `parseYamlNode`. */
|
|
97
|
+
number: number;
|
|
98
|
+
indent: number;
|
|
99
|
+
/** Content after leading indentation, trailing comment stripped, trailing whitespace trimmed. */
|
|
100
|
+
text: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stripTrailingComment(text: string): string {
|
|
104
|
+
let inSingle = false;
|
|
105
|
+
let inDouble = false;
|
|
106
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
107
|
+
const c = text[i];
|
|
108
|
+
if (inSingle) {
|
|
109
|
+
if (c === "'") inSingle = false;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (inDouble) {
|
|
113
|
+
if (c === '"' && text[i - 1] !== "\\") inDouble = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (c === "'") {
|
|
117
|
+
inSingle = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (c === '"') {
|
|
121
|
+
inDouble = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (c === "#" && (i === 0 || /\s/.test(text[i - 1] ?? ""))) {
|
|
125
|
+
return text.slice(0, i).replace(/\s+$/, "");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return text;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function prepareLines(text: string, lineOffset: number): PreparedLine[] {
|
|
132
|
+
const rawLines = text.split("\n");
|
|
133
|
+
const prepared: PreparedLine[] = [];
|
|
134
|
+
for (const [index, rawLine] of rawLines.entries()) {
|
|
135
|
+
const lineNumber = lineOffset + index + 1;
|
|
136
|
+
if (rawLine.trim().length === 0) continue;
|
|
137
|
+
const leadingMatch = /^[ ]*/.exec(rawLine);
|
|
138
|
+
const indent = leadingMatch ? leadingMatch[0].length : 0;
|
|
139
|
+
if (/^[ ]*\t/.test(rawLine)) {
|
|
140
|
+
throw new YamlParseError("tab characters are not allowed in indentation", lineNumber);
|
|
141
|
+
}
|
|
142
|
+
const withoutIndent = rawLine.slice(indent);
|
|
143
|
+
if (withoutIndent.startsWith("#")) continue; // full-line comment
|
|
144
|
+
const stripped = stripTrailingComment(withoutIndent).replace(/[ \t]+$/, "");
|
|
145
|
+
if (stripped.length === 0) continue; // comment-only line
|
|
146
|
+
prepared.push({ number: lineNumber, indent, text: stripped });
|
|
147
|
+
}
|
|
148
|
+
return prepared;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseScalarText(raw: string, line: number): YamlScalarValue {
|
|
152
|
+
const text = raw.trim();
|
|
153
|
+
if (text.length === 0 || text === "~" || text === "null" || text === "Null" || text === "NULL") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) {
|
|
157
|
+
return unescapeDoubleQuoted(text.slice(1, -1), line);
|
|
158
|
+
}
|
|
159
|
+
if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) {
|
|
160
|
+
return text.slice(1, -1).replace(/''/g, "'");
|
|
161
|
+
}
|
|
162
|
+
if (text === "true" || text === "True" || text === "TRUE") return true;
|
|
163
|
+
if (text === "false" || text === "False" || text === "FALSE") return false;
|
|
164
|
+
if (/^-?\d+$/.test(text)) return Number.parseInt(text, 10);
|
|
165
|
+
if (/^-?\d+\.\d+$/.test(text)) return Number.parseFloat(text);
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function unescapeDoubleQuoted(text: string, line: number): string {
|
|
170
|
+
let result = "";
|
|
171
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
172
|
+
const c = text[i];
|
|
173
|
+
if (c === "\\") {
|
|
174
|
+
const next = text[i + 1];
|
|
175
|
+
if (next === undefined) {
|
|
176
|
+
throw new YamlParseError("dangling escape at end of double-quoted scalar", line);
|
|
177
|
+
}
|
|
178
|
+
const map: Record<string, string> = { n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" };
|
|
179
|
+
result += map[next] ?? next;
|
|
180
|
+
i += 1;
|
|
181
|
+
} else {
|
|
182
|
+
result += c;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Finds the top-level (unquoted, unbracketed) `:` that separates a mapping key from its value. */
|
|
189
|
+
function findKeyColon(text: string, line: number): number {
|
|
190
|
+
let inSingle = false;
|
|
191
|
+
let inDouble = false;
|
|
192
|
+
let depth = 0;
|
|
193
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
194
|
+
const c = text[i];
|
|
195
|
+
if (inSingle) {
|
|
196
|
+
if (c === "'") inSingle = false;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (inDouble) {
|
|
200
|
+
if (c === '"' && text[i - 1] !== "\\") inDouble = false;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (c === "'") {
|
|
204
|
+
inSingle = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (c === '"') {
|
|
208
|
+
inDouble = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (c === "{" || c === "[") {
|
|
212
|
+
depth += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (c === "}" || c === "]") {
|
|
216
|
+
depth -= 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (c === ":" && depth === 0 && (i + 1 === text.length || /\s/.test(text[i + 1] ?? ""))) {
|
|
220
|
+
return i;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new YamlParseError(`expected a mapping "key:" or a sequence "- item", got "${text}"`, line);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseKeyText(rawKey: string): string {
|
|
227
|
+
const key = rawKey.trim();
|
|
228
|
+
if (key.startsWith('"') && key.endsWith('"') && key.length >= 2) return key.slice(1, -1);
|
|
229
|
+
if (key.startsWith("'") && key.endsWith("'") && key.length >= 2) return key.slice(1, -1);
|
|
230
|
+
return key;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Flow collections (`{...}` / `[...]`), single physical line only. ---
|
|
234
|
+
|
|
235
|
+
interface FlowCursor {
|
|
236
|
+
text: string;
|
|
237
|
+
pos: number;
|
|
238
|
+
line: number;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function skipFlowSpace(cursor: FlowCursor): void {
|
|
242
|
+
while (cursor.pos < cursor.text.length && /\s/.test(cursor.text[cursor.pos] ?? "")) {
|
|
243
|
+
cursor.pos += 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function parseFlowValue(cursor: FlowCursor): YamlNode {
|
|
248
|
+
skipFlowSpace(cursor);
|
|
249
|
+
const c = cursor.text[cursor.pos];
|
|
250
|
+
if (c === "{") return parseFlowMap(cursor);
|
|
251
|
+
if (c === "[") return parseFlowSeq(cursor);
|
|
252
|
+
return parseFlowScalar(cursor);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseFlowScalar(cursor: FlowCursor): YamlScalarNode {
|
|
256
|
+
const start = cursor.pos;
|
|
257
|
+
if (cursor.text[cursor.pos] === '"' || cursor.text[cursor.pos] === "'") {
|
|
258
|
+
const quote: string = cursor.text[cursor.pos]!;
|
|
259
|
+
cursor.pos += 1;
|
|
260
|
+
let raw = quote;
|
|
261
|
+
while (cursor.pos < cursor.text.length && cursor.text[cursor.pos] !== quote) {
|
|
262
|
+
if (cursor.text[cursor.pos] === "\\" && quote === '"') {
|
|
263
|
+
raw += cursor.text.slice(cursor.pos, cursor.pos + 2);
|
|
264
|
+
cursor.pos += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
raw += cursor.text[cursor.pos] ?? "";
|
|
268
|
+
cursor.pos += 1;
|
|
269
|
+
}
|
|
270
|
+
raw += quote;
|
|
271
|
+
cursor.pos += 1;
|
|
272
|
+
return {
|
|
273
|
+
kind: "scalar",
|
|
274
|
+
startLine: cursor.line,
|
|
275
|
+
endLine: cursor.line,
|
|
276
|
+
value: parseScalarText(raw, cursor.line),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
while (cursor.pos < cursor.text.length && !",]}".includes(cursor.text[cursor.pos] ?? "")) {
|
|
280
|
+
cursor.pos += 1;
|
|
281
|
+
}
|
|
282
|
+
const raw = cursor.text.slice(start, cursor.pos).trim();
|
|
283
|
+
return {
|
|
284
|
+
kind: "scalar",
|
|
285
|
+
startLine: cursor.line,
|
|
286
|
+
endLine: cursor.line,
|
|
287
|
+
value: parseScalarText(raw, cursor.line),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseFlowSeq(cursor: FlowCursor): YamlSeqNode {
|
|
292
|
+
const startLine = cursor.line;
|
|
293
|
+
cursor.pos += 1; // consume '['
|
|
294
|
+
const items: YamlNode[] = [];
|
|
295
|
+
skipFlowSpace(cursor);
|
|
296
|
+
while (cursor.pos < cursor.text.length && cursor.text[cursor.pos] !== "]") {
|
|
297
|
+
items.push(parseFlowValue(cursor));
|
|
298
|
+
skipFlowSpace(cursor);
|
|
299
|
+
if (cursor.text[cursor.pos] === ",") {
|
|
300
|
+
cursor.pos += 1;
|
|
301
|
+
skipFlowSpace(cursor);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (cursor.text[cursor.pos] !== "]") {
|
|
305
|
+
throw new YamlParseError("unterminated flow sequence (missing ']')", cursor.line);
|
|
306
|
+
}
|
|
307
|
+
cursor.pos += 1;
|
|
308
|
+
return { kind: "seq", startLine, endLine: cursor.line, items };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseFlowMap(cursor: FlowCursor): YamlMapNode {
|
|
312
|
+
const startLine = cursor.line;
|
|
313
|
+
cursor.pos += 1; // consume '{'
|
|
314
|
+
const entries: YamlMapEntry[] = [];
|
|
315
|
+
skipFlowSpace(cursor);
|
|
316
|
+
while (cursor.pos < cursor.text.length && cursor.text[cursor.pos] !== "}") {
|
|
317
|
+
const keyStart = cursor.pos;
|
|
318
|
+
while (cursor.pos < cursor.text.length && cursor.text[cursor.pos] !== ":") {
|
|
319
|
+
cursor.pos += 1;
|
|
320
|
+
}
|
|
321
|
+
const key = parseKeyText(cursor.text.slice(keyStart, cursor.pos));
|
|
322
|
+
if (cursor.text[cursor.pos] !== ":") {
|
|
323
|
+
throw new YamlParseError("expected ':' in flow mapping entry", cursor.line);
|
|
324
|
+
}
|
|
325
|
+
cursor.pos += 1;
|
|
326
|
+
const value = parseFlowValue(cursor);
|
|
327
|
+
entries.push({ key, keyLine: cursor.line, value });
|
|
328
|
+
skipFlowSpace(cursor);
|
|
329
|
+
if (cursor.text[cursor.pos] === ",") {
|
|
330
|
+
cursor.pos += 1;
|
|
331
|
+
skipFlowSpace(cursor);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (cursor.text[cursor.pos] !== "}") {
|
|
335
|
+
throw new YamlParseError("unterminated flow mapping (missing '}')", cursor.line);
|
|
336
|
+
}
|
|
337
|
+
cursor.pos += 1;
|
|
338
|
+
return { kind: "map", startLine, endLine: cursor.line, entries };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseFlow(text: string, line: number): YamlNode {
|
|
342
|
+
const cursor: FlowCursor = { text, pos: 0, line };
|
|
343
|
+
const node = parseFlowValue(cursor);
|
|
344
|
+
skipFlowSpace(cursor);
|
|
345
|
+
if (cursor.pos !== text.length) {
|
|
346
|
+
throw new YamlParseError(
|
|
347
|
+
"trailing content after flow collection (multi-line flow is not supported)",
|
|
348
|
+
line,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return node;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Block parsing. ---
|
|
355
|
+
|
|
356
|
+
interface BlockResult {
|
|
357
|
+
node: YamlNode;
|
|
358
|
+
nextIndex: number;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function parseBlock(lines: PreparedLine[], startIndex: number, indent: number): BlockResult {
|
|
362
|
+
const first = lines[startIndex];
|
|
363
|
+
if (first === undefined) {
|
|
364
|
+
throw new YamlParseError("unexpected end of document", lines[startIndex - 1]?.number ?? 0);
|
|
365
|
+
}
|
|
366
|
+
if (first.text === "-" || first.text.startsWith("- ")) {
|
|
367
|
+
return parseSequence(lines, startIndex, indent);
|
|
368
|
+
}
|
|
369
|
+
return parseMapping(lines, startIndex, indent);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function parseSequence(lines: PreparedLine[], startIndex: number, indent: number): BlockResult {
|
|
373
|
+
const items: YamlNode[] = [];
|
|
374
|
+
let i = startIndex;
|
|
375
|
+
const startLine = lines[startIndex]!.number;
|
|
376
|
+
let endLine = startLine;
|
|
377
|
+
while (i < lines.length) {
|
|
378
|
+
const line = lines[i];
|
|
379
|
+
if (
|
|
380
|
+
line === undefined ||
|
|
381
|
+
line.indent !== indent ||
|
|
382
|
+
!(line.text === "-" || line.text.startsWith("- "))
|
|
383
|
+
) {
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
const rest = line.text === "-" ? "" : line.text.slice(2);
|
|
387
|
+
if (rest.trim().length === 0) {
|
|
388
|
+
i += 1;
|
|
389
|
+
const next = lines[i];
|
|
390
|
+
if (next !== undefined && next.indent > indent) {
|
|
391
|
+
const nested = parseBlock(lines, i, next.indent);
|
|
392
|
+
items.push(nested.node);
|
|
393
|
+
endLine = nested.node.endLine;
|
|
394
|
+
i = nested.nextIndex;
|
|
395
|
+
} else {
|
|
396
|
+
items.push({ kind: "scalar", startLine: line.number, endLine: line.number, value: null });
|
|
397
|
+
endLine = line.number;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (rest.trimStart().startsWith("{") || rest.trimStart().startsWith("[")) {
|
|
402
|
+
const node = parseFlow(rest.trim(), line.number);
|
|
403
|
+
items.push(node);
|
|
404
|
+
endLine = node.endLine;
|
|
405
|
+
i += 1;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
// "- key: value" (mapping shorthand) — rewrite as a mapping continued by
|
|
409
|
+
// any following lines indented to align with where "key" started.
|
|
410
|
+
const virtualIndent = indent + (line.text.length - rest.length);
|
|
411
|
+
const isMapEntry = looksLikeMapKey(rest);
|
|
412
|
+
if (isMapEntry) {
|
|
413
|
+
const rewritten: PreparedLine[] = [
|
|
414
|
+
{ number: line.number, indent: virtualIndent, text: rest },
|
|
415
|
+
...lines.slice(i + 1),
|
|
416
|
+
];
|
|
417
|
+
const nested = parseMapping(rewritten, 0, virtualIndent);
|
|
418
|
+
items.push(nested.node);
|
|
419
|
+
endLine = nested.node.endLine;
|
|
420
|
+
i = i + nested.nextIndex; // rewritten[0] === original index i
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const scalar = parseScalarNode(rest, line.number);
|
|
424
|
+
items.push(scalar);
|
|
425
|
+
endLine = scalar.endLine;
|
|
426
|
+
i += 1;
|
|
427
|
+
}
|
|
428
|
+
return { node: { kind: "seq", startLine, endLine, items }, nextIndex: i };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function looksLikeMapKey(text: string): boolean {
|
|
432
|
+
try {
|
|
433
|
+
findKeyColon(text, 0);
|
|
434
|
+
return true;
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function parseScalarNode(rest: string, line: number): YamlScalarNode {
|
|
441
|
+
const trimmed = rest.trim();
|
|
442
|
+
if (trimmed.startsWith("|") || trimmed.startsWith(">")) {
|
|
443
|
+
// A block scalar indicator with no following indented lines here just
|
|
444
|
+
// degrades to an empty string — the multi-line case is handled in
|
|
445
|
+
// parseMapping, which owns the following-lines lookahead.
|
|
446
|
+
return { kind: "scalar", startLine: line, endLine: line, value: "" };
|
|
447
|
+
}
|
|
448
|
+
return { kind: "scalar", startLine: line, endLine: line, value: parseScalarText(trimmed, line) };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function parseMapping(lines: PreparedLine[], startIndex: number, indent: number): BlockResult {
|
|
452
|
+
const entries: YamlMapEntry[] = [];
|
|
453
|
+
let i = startIndex;
|
|
454
|
+
const startLine = lines[startIndex]!.number;
|
|
455
|
+
let endLine = startLine;
|
|
456
|
+
while (i < lines.length) {
|
|
457
|
+
const line = lines[i];
|
|
458
|
+
if (
|
|
459
|
+
line === undefined ||
|
|
460
|
+
line.indent !== indent ||
|
|
461
|
+
line.text === "-" ||
|
|
462
|
+
line.text.startsWith("- ")
|
|
463
|
+
) {
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
const colonIndex = findKeyColon(line.text, line.number);
|
|
467
|
+
const key = parseKeyText(line.text.slice(0, colonIndex));
|
|
468
|
+
const rawValue = line.text.slice(colonIndex + 1).trim();
|
|
469
|
+
|
|
470
|
+
if (rawValue.length === 0) {
|
|
471
|
+
i += 1;
|
|
472
|
+
const next = lines[i];
|
|
473
|
+
if (next !== undefined && next.indent > indent) {
|
|
474
|
+
const nested = parseBlock(lines, i, next.indent);
|
|
475
|
+
entries.push({ key, keyLine: line.number, value: nested.node });
|
|
476
|
+
endLine = nested.node.endLine;
|
|
477
|
+
i = nested.nextIndex;
|
|
478
|
+
} else {
|
|
479
|
+
entries.push({
|
|
480
|
+
key,
|
|
481
|
+
keyLine: line.number,
|
|
482
|
+
value: { kind: "scalar", startLine: line.number, endLine: line.number, value: null },
|
|
483
|
+
});
|
|
484
|
+
endLine = line.number;
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (rawValue.startsWith("|") || rawValue.startsWith(">")) {
|
|
490
|
+
const folded = rawValue.startsWith(">");
|
|
491
|
+
i += 1;
|
|
492
|
+
const blockIndent = lines[i]?.indent;
|
|
493
|
+
const collected: string[] = [];
|
|
494
|
+
let lastLine = line.number;
|
|
495
|
+
while (
|
|
496
|
+
i < lines.length &&
|
|
497
|
+
blockIndent !== undefined &&
|
|
498
|
+
(lines[i]?.indent ?? -1) >= blockIndent
|
|
499
|
+
) {
|
|
500
|
+
const current = lines[i]!;
|
|
501
|
+
collected.push(current.text);
|
|
502
|
+
lastLine = current.number;
|
|
503
|
+
i += 1;
|
|
504
|
+
}
|
|
505
|
+
const value = folded ? collected.join(" ") : collected.join("\n");
|
|
506
|
+
entries.push({
|
|
507
|
+
key,
|
|
508
|
+
keyLine: line.number,
|
|
509
|
+
value: { kind: "scalar", startLine: line.number, endLine: lastLine, value },
|
|
510
|
+
});
|
|
511
|
+
endLine = lastLine;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (rawValue.startsWith("{") || rawValue.startsWith("[")) {
|
|
516
|
+
const node = parseFlow(rawValue, line.number);
|
|
517
|
+
entries.push({ key, keyLine: line.number, value: node });
|
|
518
|
+
endLine = node.endLine;
|
|
519
|
+
i += 1;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const scalar = parseScalarNode(rawValue, line.number);
|
|
524
|
+
entries.push({ key, keyLine: line.number, value: scalar });
|
|
525
|
+
endLine = scalar.endLine;
|
|
526
|
+
i += 1;
|
|
527
|
+
}
|
|
528
|
+
return { node: { kind: "map", startLine, endLine, entries }, nextIndex: i };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Parses one YAML document's full text (e.g. one `splitYamlDocuments()`
|
|
533
|
+
* span) into a `YamlNode` tree. Line numbers on every node are 1-based
|
|
534
|
+
* WITHIN `text` — add `lineOffset` (a span's `startLine - 1`) yourself when
|
|
535
|
+
* mapping back to the original multi-document file. Returns `null` for a
|
|
536
|
+
* document that is empty or comments-only. Throws `YamlParseError` (never
|
|
537
|
+
* any other error type) on anything outside the supported subset.
|
|
538
|
+
*/
|
|
539
|
+
export function parseYamlNode(text: string, lineOffset = 0): YamlNode | null {
|
|
540
|
+
const lines = prepareLines(text, lineOffset);
|
|
541
|
+
if (lines.length === 0) return null;
|
|
542
|
+
const result = parseBlock(lines, 0, lines[0]!.indent);
|
|
543
|
+
if (result.nextIndex !== lines.length) {
|
|
544
|
+
const trailing = lines[result.nextIndex];
|
|
545
|
+
throw new YamlParseError(
|
|
546
|
+
"unexpected indentation / trailing content at document top level",
|
|
547
|
+
trailing?.number ?? lines.at(-1)!.number,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return result.node;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- Small walk helpers used by adapters. ---
|
|
554
|
+
|
|
555
|
+
export function mapGet(node: YamlNode | null | undefined, key: string): YamlNode | undefined {
|
|
556
|
+
if (node === undefined || node === null || node.kind !== "map") return undefined;
|
|
557
|
+
return node.entries.find((entry) => entry.key === key)?.value;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function scalarString(node: YamlNode | null | undefined): string | undefined {
|
|
561
|
+
if (node === undefined || node === null || node.kind !== "scalar" || node.value === null) {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
return String(node.value);
|
|
565
|
+
}
|