@intentius/chant 0.0.11 → 0.0.12
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/package.json +1 -1
- package/src/lsp/lexicon-providers.ts +1 -0
- package/src/yaml.test.ts +192 -0
- package/src/yaml.ts +308 -0
package/package.json
CHANGED
|
@@ -108,6 +108,7 @@ export function lexiconCompletions(
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Inside constructor props — look for `new ClassName({` pattern
|
|
111
|
+
if (!ctx.content) return [];
|
|
111
112
|
const constructorMatch = ctx.content.slice(0, ctx.content.split("\n").slice(0, ctx.position.line + 1).join("\n").length)
|
|
112
113
|
.match(/\bnew\s+(\w+)\s*\(\s*(?:["'][^"']*["']\s*,\s*)?{[^}]*$/s);
|
|
113
114
|
|
package/src/yaml.test.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { emitYAML, parseYAML, parseScalar } from "./yaml";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// emitYAML
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
describe("emitYAML", () => {
|
|
8
|
+
test("null", () => {
|
|
9
|
+
expect(emitYAML(null, 0)).toBe("null");
|
|
10
|
+
expect(emitYAML(undefined, 0)).toBe("null");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("booleans", () => {
|
|
14
|
+
expect(emitYAML(true, 0)).toBe("true");
|
|
15
|
+
expect(emitYAML(false, 0)).toBe("false");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("numbers", () => {
|
|
19
|
+
expect(emitYAML(42, 0)).toBe("42");
|
|
20
|
+
expect(emitYAML(3.14, 0)).toBe("3.14");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("plain strings", () => {
|
|
24
|
+
expect(emitYAML("hello", 0)).toBe("hello");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("strings requiring quoting", () => {
|
|
28
|
+
// boolean-like
|
|
29
|
+
expect(emitYAML("true", 0)).toBe("'true'");
|
|
30
|
+
expect(emitYAML("yes", 0)).toBe("'yes'");
|
|
31
|
+
// colon-space
|
|
32
|
+
expect(emitYAML("key: value", 0)).toBe("'key: value'");
|
|
33
|
+
// hash
|
|
34
|
+
expect(emitYAML("a # comment", 0)).toBe("'a # comment'");
|
|
35
|
+
// leading special chars
|
|
36
|
+
expect(emitYAML("$VAR", 0)).toBe("'$VAR'");
|
|
37
|
+
expect(emitYAML("!ref", 0)).toBe("'!ref'");
|
|
38
|
+
expect(emitYAML("*alias", 0)).toBe("'*alias'");
|
|
39
|
+
expect(emitYAML("{obj}", 0)).toBe("'{obj}'");
|
|
40
|
+
expect(emitYAML("[arr]", 0)).toBe("'[arr]'");
|
|
41
|
+
// leading digit
|
|
42
|
+
expect(emitYAML("123abc", 0)).toBe("'123abc'");
|
|
43
|
+
// empty string
|
|
44
|
+
expect(emitYAML("", 0)).toBe("''");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("single-quote escaping", () => {
|
|
48
|
+
// A string that needs quoting (leading digit) AND contains a single quote
|
|
49
|
+
expect(emitYAML("1's", 0)).toBe("'1''s'");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("empty array", () => {
|
|
53
|
+
expect(emitYAML([], 0)).toBe("[]");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("simple array", () => {
|
|
57
|
+
const result = emitYAML(["a", "b"], 0);
|
|
58
|
+
expect(result).toBe("\n- a\n- b");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("array of objects inlines first key on dash line", () => {
|
|
62
|
+
const result = emitYAML([{ name: "x", value: 1 }], 0);
|
|
63
|
+
expect(result).toContain("- name: x");
|
|
64
|
+
expect(result).toContain(" value: 1");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("empty object", () => {
|
|
68
|
+
expect(emitYAML({}, 0)).toBe("{}");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("nested object", () => {
|
|
72
|
+
const result = emitYAML({ a: { b: 1 } }, 0);
|
|
73
|
+
expect(result).toContain("a:");
|
|
74
|
+
expect(result).toContain(" b: 1");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("tagged value with array", () => {
|
|
78
|
+
const result = emitYAML({ tag: "!reference", value: [".base", "script"] }, 0);
|
|
79
|
+
expect(result).toBe("!reference [.base, script]");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("tagged value with scalar", () => {
|
|
83
|
+
const result = emitYAML({ tag: "!include", value: "file.yml" }, 0);
|
|
84
|
+
expect(result).toBe("!include file.yml");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("indentation at depth", () => {
|
|
88
|
+
const result = emitYAML({ key: "val" }, 1);
|
|
89
|
+
expect(result).toBe("\n key: val");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// parseYAML
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
describe("parseYAML", () => {
|
|
97
|
+
test("JSON passthrough", () => {
|
|
98
|
+
const result = parseYAML('{"a": 1}');
|
|
99
|
+
expect(result).toEqual({ a: 1 });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("simple key-value", () => {
|
|
103
|
+
const result = parseYAML("name: hello\ncount: 42");
|
|
104
|
+
expect(result).toEqual({ name: "hello", count: 42 });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("nested object", () => {
|
|
108
|
+
const result = parseYAML("parent:\n child: value");
|
|
109
|
+
expect(result).toEqual({ parent: { child: "value" } });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("block array", () => {
|
|
113
|
+
const result = parseYAML("items:\n - a\n - b\n - c");
|
|
114
|
+
expect(result).toEqual({ items: ["a", "b", "c"] });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("inline array", () => {
|
|
118
|
+
const result = parseYAML('items: ["a", "b"]');
|
|
119
|
+
expect(result).toEqual({ items: ["a", "b"] });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("inline object", () => {
|
|
123
|
+
const result = parseYAML('data: {"x": 1}');
|
|
124
|
+
expect(result).toEqual({ data: { x: 1 } });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("comments and blank lines are skipped", () => {
|
|
128
|
+
const result = parseYAML("# comment\na: 1\n\n# another\nb: 2");
|
|
129
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("scalar coercion", () => {
|
|
133
|
+
const result = parseYAML("a: true\nb: false\nc: null\nd: yes\ne: no\nf: ~");
|
|
134
|
+
expect(result).toEqual({ a: true, b: false, c: null, d: true, e: false, f: null });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("quoted strings preserve value", () => {
|
|
138
|
+
const result = parseYAML("a: 'true'\nb: \"42\"");
|
|
139
|
+
expect(result).toEqual({ a: "true", b: "42" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("handles CRLF line endings", () => {
|
|
143
|
+
const result = parseYAML("apiVersion: v1\r\nkind: Pod\r\nmetadata:\r\n name: test\r\n");
|
|
144
|
+
expect(result).toEqual({
|
|
145
|
+
apiVersion: "v1",
|
|
146
|
+
kind: "Pod",
|
|
147
|
+
metadata: { name: "test" },
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("handles bare CR line endings", () => {
|
|
152
|
+
const result = parseYAML("a: 1\rb: 2\r");
|
|
153
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("array of objects", () => {
|
|
157
|
+
const result = parseYAML("items:\n - name: x\n value: 1\n - name: y\n value: 2");
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
items: [
|
|
160
|
+
{ name: "x", value: 1 },
|
|
161
|
+
{ name: "y", value: 2 },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// parseScalar
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
describe("parseScalar", () => {
|
|
171
|
+
test("null variants", () => {
|
|
172
|
+
expect(parseScalar("")).toBe(null);
|
|
173
|
+
expect(parseScalar("~")).toBe(null);
|
|
174
|
+
expect(parseScalar("null")).toBe(null);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("boolean variants", () => {
|
|
178
|
+
expect(parseScalar("true")).toBe(true);
|
|
179
|
+
expect(parseScalar("yes")).toBe(true);
|
|
180
|
+
expect(parseScalar("false")).toBe(false);
|
|
181
|
+
expect(parseScalar("no")).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("numbers", () => {
|
|
185
|
+
expect(parseScalar("42")).toBe(42);
|
|
186
|
+
expect(parseScalar("3.14")).toBe(3.14);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("plain strings", () => {
|
|
190
|
+
expect(parseScalar("hello")).toBe("hello");
|
|
191
|
+
});
|
|
192
|
+
});
|
package/src/yaml.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight YAML emitter and parser.
|
|
3
|
+
*
|
|
4
|
+
* Covers the subset of YAML used by Chant lexicons (scalars, block arrays,
|
|
5
|
+
* nested objects, tagged values). Not a full YAML implementation — use a
|
|
6
|
+
* dedicated library if you need anchors, multi-document streams, or
|
|
7
|
+
* block scalars.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Emitter
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emit a YAML value with proper indentation.
|
|
16
|
+
*
|
|
17
|
+
* - Primitives render inline.
|
|
18
|
+
* - Arrays and objects render as block YAML, returning a string that starts
|
|
19
|
+
* with `\n` so the caller can append it after a key.
|
|
20
|
+
* - Tagged values `{ tag, value }` emit `!tag [...]` or `!tag scalar`.
|
|
21
|
+
*/
|
|
22
|
+
export function emitYAML(value: unknown, indent: number): string {
|
|
23
|
+
const prefix = " ".repeat(indent);
|
|
24
|
+
|
|
25
|
+
if (value === null || value === undefined) {
|
|
26
|
+
return "null";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof value === "boolean") {
|
|
30
|
+
return value ? "true" : "false";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
// Quote strings that could be misinterpreted
|
|
39
|
+
if (
|
|
40
|
+
value === "" ||
|
|
41
|
+
value === "true" ||
|
|
42
|
+
value === "false" ||
|
|
43
|
+
value === "null" ||
|
|
44
|
+
value === "yes" ||
|
|
45
|
+
value === "no" ||
|
|
46
|
+
value.includes(": ") ||
|
|
47
|
+
value.includes("#") ||
|
|
48
|
+
value.startsWith("*") ||
|
|
49
|
+
value.startsWith("&") ||
|
|
50
|
+
value.startsWith("!") ||
|
|
51
|
+
value.startsWith("{") ||
|
|
52
|
+
value.startsWith("[") ||
|
|
53
|
+
value.startsWith("'") ||
|
|
54
|
+
value.startsWith('"') ||
|
|
55
|
+
value.startsWith("$") ||
|
|
56
|
+
/^\d/.test(value)
|
|
57
|
+
) {
|
|
58
|
+
// Use single quotes, escaping internal single quotes
|
|
59
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
if (value.length === 0) return "[]";
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
for (const item of value) {
|
|
68
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
69
|
+
// Object items in arrays
|
|
70
|
+
const entries = Object.entries(item as Record<string, unknown>);
|
|
71
|
+
if (entries.length > 0) {
|
|
72
|
+
const [firstKey, firstVal] = entries[0];
|
|
73
|
+
const firstEmitted = emitYAML(firstVal, indent + 2);
|
|
74
|
+
if (firstEmitted.startsWith("\n")) {
|
|
75
|
+
lines.push(`${prefix}- ${firstKey}:${firstEmitted}`);
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(`${prefix}- ${firstKey}: ${firstEmitted}`);
|
|
78
|
+
}
|
|
79
|
+
for (let i = 1; i < entries.length; i++) {
|
|
80
|
+
const [key, val] = entries[i];
|
|
81
|
+
const emitted = emitYAML(val, indent + 2);
|
|
82
|
+
if (emitted.startsWith("\n")) {
|
|
83
|
+
lines.push(`${prefix} ${key}:${emitted}`);
|
|
84
|
+
} else {
|
|
85
|
+
lines.push(`${prefix} ${key}: ${emitted}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
lines.push(`${prefix}- ${emitYAML(item, indent + 1).trimStart()}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return "\n" + lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof value === "object") {
|
|
97
|
+
const obj = value as Record<string, unknown>;
|
|
98
|
+
|
|
99
|
+
// Handle tagged values (e.g. { tag: "!reference", value: [...] })
|
|
100
|
+
if ("tag" in obj && "value" in obj && typeof obj.tag === "string") {
|
|
101
|
+
if (Array.isArray(obj.value)) {
|
|
102
|
+
return `${obj.tag} [${(obj.value as unknown[]).map(String).join(", ")}]`;
|
|
103
|
+
}
|
|
104
|
+
return `${obj.tag} ${emitYAML(obj.value, indent)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const entries = Object.entries(obj);
|
|
108
|
+
if (entries.length === 0) return "{}";
|
|
109
|
+
const lines: string[] = [];
|
|
110
|
+
for (const [key, val] of entries) {
|
|
111
|
+
const emitted = emitYAML(val, indent + 1);
|
|
112
|
+
if (emitted.startsWith("\n")) {
|
|
113
|
+
lines.push(`${prefix}${key}:${emitted}`);
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(`${prefix}${key}: ${emitted}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return "\n" + lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return String(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Parser
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/** Result of parsing a YAML block. */
|
|
129
|
+
export interface ParseResult {
|
|
130
|
+
value: unknown;
|
|
131
|
+
endIndex: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a YAML document (or JSON document) into a plain object.
|
|
136
|
+
*
|
|
137
|
+
* Tries `JSON.parse` first; falls back to a line-based YAML parser that
|
|
138
|
+
* handles the subset of YAML commonly found in CI configuration files.
|
|
139
|
+
*/
|
|
140
|
+
export function parseYAML(content: string): Record<string, unknown> {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(content);
|
|
143
|
+
} catch {
|
|
144
|
+
// Fall through to YAML parsing
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
148
|
+
return parseYAMLLines(lines, 0, 0).value as Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse indentation-based YAML lines into a key-value object.
|
|
153
|
+
*/
|
|
154
|
+
export function parseYAMLLines(
|
|
155
|
+
lines: string[],
|
|
156
|
+
startIndex: number,
|
|
157
|
+
baseIndent: number,
|
|
158
|
+
): ParseResult {
|
|
159
|
+
const result: Record<string, unknown> = {};
|
|
160
|
+
let i = startIndex;
|
|
161
|
+
|
|
162
|
+
while (i < lines.length) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
// Skip empty lines and comments
|
|
165
|
+
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const indent = line.search(/\S/);
|
|
171
|
+
if (indent < baseIndent) break; // Dedented — done with this block
|
|
172
|
+
if (indent > baseIndent && startIndex > 0) break; // Unexpected indent
|
|
173
|
+
|
|
174
|
+
const keyMatch = line.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
175
|
+
if (keyMatch) {
|
|
176
|
+
const key = keyMatch[2].trim();
|
|
177
|
+
const inlineValue = keyMatch[3].trim();
|
|
178
|
+
|
|
179
|
+
if (inlineValue === "" || inlineValue.startsWith("#")) {
|
|
180
|
+
// Check next line for array or nested object
|
|
181
|
+
if (i + 1 < lines.length) {
|
|
182
|
+
const nextLine = lines[i + 1];
|
|
183
|
+
const nextIndent = nextLine.search(/\S/);
|
|
184
|
+
if (nextIndent > indent && nextLine.trimStart().startsWith("- ")) {
|
|
185
|
+
const arr = parseYAMLArray(lines, i + 1, nextIndent);
|
|
186
|
+
result[key] = arr.value;
|
|
187
|
+
i = arr.endIndex;
|
|
188
|
+
continue;
|
|
189
|
+
} else if (nextIndent > indent) {
|
|
190
|
+
const nested = parseYAMLLines(lines, i + 1, nextIndent);
|
|
191
|
+
result[key] = nested.value;
|
|
192
|
+
i = nested.endIndex;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
result[key] = null;
|
|
197
|
+
i++;
|
|
198
|
+
} else if (inlineValue.startsWith("[")) {
|
|
199
|
+
// Inline array
|
|
200
|
+
try {
|
|
201
|
+
result[key] = JSON.parse(inlineValue);
|
|
202
|
+
} catch {
|
|
203
|
+
result[key] = inlineValue;
|
|
204
|
+
}
|
|
205
|
+
i++;
|
|
206
|
+
} else if (inlineValue.startsWith("{")) {
|
|
207
|
+
// Inline object
|
|
208
|
+
try {
|
|
209
|
+
result[key] = JSON.parse(inlineValue);
|
|
210
|
+
} catch {
|
|
211
|
+
result[key] = inlineValue;
|
|
212
|
+
}
|
|
213
|
+
i++;
|
|
214
|
+
} else {
|
|
215
|
+
result[key] = parseScalar(inlineValue);
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
} else if (line.trimStart().startsWith("- ")) {
|
|
219
|
+
break;
|
|
220
|
+
} else {
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { value: result, endIndex: i };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Parse a block array (lines starting with `- `).
|
|
230
|
+
*/
|
|
231
|
+
export function parseYAMLArray(
|
|
232
|
+
lines: string[],
|
|
233
|
+
startIndex: number,
|
|
234
|
+
baseIndent: number,
|
|
235
|
+
): ParseResult {
|
|
236
|
+
const result: unknown[] = [];
|
|
237
|
+
let i = startIndex;
|
|
238
|
+
|
|
239
|
+
while (i < lines.length) {
|
|
240
|
+
const line = lines[i];
|
|
241
|
+
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const indent = line.search(/\S/);
|
|
247
|
+
if (indent < baseIndent) break;
|
|
248
|
+
|
|
249
|
+
const itemMatch = line.match(/^(\s*)- (.*)$/);
|
|
250
|
+
if (itemMatch && indent === baseIndent) {
|
|
251
|
+
const itemValue = itemMatch[2].trim();
|
|
252
|
+
// Check if it's a key-value pair (object item in array)
|
|
253
|
+
const kvMatch = itemValue.match(/^([^\s:][^:]*?):\s*(.*)$/);
|
|
254
|
+
if (kvMatch) {
|
|
255
|
+
const obj: Record<string, unknown> = {};
|
|
256
|
+
obj[kvMatch[1].trim()] = parseScalar(kvMatch[2].trim());
|
|
257
|
+
// Check for more keys at indent+2
|
|
258
|
+
const nextIndent = indent + 2;
|
|
259
|
+
let j = i + 1;
|
|
260
|
+
while (j < lines.length) {
|
|
261
|
+
const nextLine = lines[j];
|
|
262
|
+
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
|
|
263
|
+
j++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const ni = nextLine.search(/\S/);
|
|
267
|
+
if (ni !== nextIndent) break;
|
|
268
|
+
const nextKV = nextLine.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
269
|
+
if (nextKV) {
|
|
270
|
+
obj[nextKV[2].trim()] = parseScalar(nextKV[3].trim());
|
|
271
|
+
j++;
|
|
272
|
+
} else {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
result.push(obj);
|
|
277
|
+
i = j;
|
|
278
|
+
} else {
|
|
279
|
+
result.push(parseScalar(itemValue));
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { value: result, endIndex: i };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Coerce a scalar string to a typed value.
|
|
292
|
+
*/
|
|
293
|
+
export function parseScalar(value: string): unknown {
|
|
294
|
+
if (value === "" || value === "~" || value === "null") return null;
|
|
295
|
+
if (value === "true" || value === "yes") return true;
|
|
296
|
+
if (value === "false" || value === "no") return false;
|
|
297
|
+
// Strip quotes
|
|
298
|
+
if (
|
|
299
|
+
(value.startsWith("'") && value.endsWith("'")) ||
|
|
300
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
301
|
+
) {
|
|
302
|
+
return value.slice(1, -1);
|
|
303
|
+
}
|
|
304
|
+
// Number
|
|
305
|
+
const num = Number(value);
|
|
306
|
+
if (!isNaN(num) && value !== "") return num;
|
|
307
|
+
return value;
|
|
308
|
+
}
|