@intentius/chant 0.0.22 → 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/package.json +1 -1
- package/src/cli/commands/init-lexicon/templates/codegen.ts +188 -0
- package/src/cli/commands/init-lexicon/templates/docs.ts +81 -0
- package/src/cli/commands/init-lexicon/templates/examples.ts +35 -0
- package/src/cli/commands/init-lexicon/templates/lint.ts +30 -0
- package/src/cli/commands/init-lexicon/templates/lsp.ts +39 -0
- package/src/cli/commands/init-lexicon/templates/plugin.ts +110 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +182 -0
- package/src/cli/commands/init-lexicon/templates/spec.ts +57 -0
- package/src/cli/commands/init-lexicon/templates/tests.ts +70 -0
- package/src/cli/commands/init-lexicon.ts +12 -774
- package/src/cli/conflict-check.test.ts +43 -0
- package/src/cli/main.ts +1 -1
- package/src/cli/mcp/resource-handlers.ts +227 -0
- package/src/cli/mcp/server.ts +20 -409
- package/src/cli/mcp/state-tools.ts +138 -0
- package/src/cli/mcp/types.ts +45 -0
- package/src/codegen/docs-file-markers.ts +69 -0
- package/src/codegen/docs-rule-scanning.ts +159 -0
- package/src/codegen/docs-sections.ts +159 -0
- package/src/codegen/docs-sidebar.ts +56 -0
- package/src/codegen/docs-types.ts +79 -0
- package/src/codegen/docs.ts +9 -495
- package/src/codegen/typecheck.ts +13 -0
- package/src/composite.test.ts +75 -0
- package/src/composite.ts +37 -0
- package/src/discovery/collect.test.ts +34 -0
- package/src/discovery/collect.ts +25 -0
- package/src/lexicon-plugin-helpers.ts +130 -0
- package/src/toml-emit.ts +182 -0
- package/src/toml-parse.ts +370 -0
- package/src/toml-utils.ts +60 -0
- package/src/toml.ts +5 -602
- package/src/yaml.ts +7 -2
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOML parser.
|
|
3
|
+
*
|
|
4
|
+
* Handles the TOML subset used by Chant lexicons: tables, arrays of tables,
|
|
5
|
+
* key-value pairs, inline tables, inline arrays, strings, numbers, booleans.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { unescapeString, stripInlineComment } from "./toml-utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a TOML document string into a plain object.
|
|
12
|
+
*
|
|
13
|
+
* Uses a built-in parser that handles the TOML subset used by Flyway
|
|
14
|
+
* configuration files.
|
|
15
|
+
*/
|
|
16
|
+
export function parseTOML(content: string): Record<string, unknown> {
|
|
17
|
+
const result: Record<string, unknown> = {};
|
|
18
|
+
const lines = content.split("\n");
|
|
19
|
+
let currentPath: string[] = [];
|
|
20
|
+
let isArrayOfTables = false;
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i].trim();
|
|
24
|
+
|
|
25
|
+
// Skip empty lines and comments
|
|
26
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
27
|
+
|
|
28
|
+
// Array of tables: [[section.path]]
|
|
29
|
+
const aotMatch = line.match(/^\[\[([^\]]+)\]\]\s*(?:#.*)?$/);
|
|
30
|
+
if (aotMatch) {
|
|
31
|
+
currentPath = parseDottedKey(aotMatch[1].trim());
|
|
32
|
+
isArrayOfTables = true;
|
|
33
|
+
// Ensure the array exists and add a new entry
|
|
34
|
+
const arr = ensureArrayAt(result, currentPath);
|
|
35
|
+
arr.push({});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Table header: [section.path]
|
|
40
|
+
const tableMatch = line.match(/^\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
41
|
+
if (tableMatch) {
|
|
42
|
+
currentPath = parseDottedKey(tableMatch[1].trim());
|
|
43
|
+
isArrayOfTables = false;
|
|
44
|
+
ensureTableAt(result, currentPath);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Key-value pair
|
|
49
|
+
const kvResult = parseKeyValue(line);
|
|
50
|
+
if (kvResult) {
|
|
51
|
+
const target = isArrayOfTables
|
|
52
|
+
? getLastArrayEntry(result, currentPath)
|
|
53
|
+
: getTableAt(result, currentPath);
|
|
54
|
+
if (target) {
|
|
55
|
+
setNestedValue(target, kvResult.key, kvResult.value);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Key-value parsing ─────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
interface KeyValueResult {
|
|
67
|
+
key: string[];
|
|
68
|
+
value: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a key = value line.
|
|
73
|
+
*/
|
|
74
|
+
function parseKeyValue(line: string): KeyValueResult | null {
|
|
75
|
+
// Find the = sign (not inside quotes)
|
|
76
|
+
const eqIndex = findEquals(line);
|
|
77
|
+
if (eqIndex === -1) return null;
|
|
78
|
+
|
|
79
|
+
const rawKey = line.slice(0, eqIndex).trim();
|
|
80
|
+
const rawValue = line.slice(eqIndex + 1).trim();
|
|
81
|
+
|
|
82
|
+
const key = parseDottedKey(rawKey);
|
|
83
|
+
const value = parseTOMLValue(rawValue);
|
|
84
|
+
|
|
85
|
+
return { key, value };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find the index of the first `=` not inside quotes.
|
|
90
|
+
*/
|
|
91
|
+
function findEquals(line: string): number {
|
|
92
|
+
let inSingleQuote = false;
|
|
93
|
+
let inDoubleQuote = false;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < line.length; i++) {
|
|
96
|
+
const ch = line[i];
|
|
97
|
+
if (ch === "'" && !inDoubleQuote) {
|
|
98
|
+
inSingleQuote = !inSingleQuote;
|
|
99
|
+
} else if (ch === '"' && !inSingleQuote && (i === 0 || line[i - 1] !== "\\")) {
|
|
100
|
+
inDoubleQuote = !inDoubleQuote;
|
|
101
|
+
} else if (ch === "=" && !inSingleQuote && !inDoubleQuote) {
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse a dotted key like `flyway.placeholders.name` into path segments.
|
|
110
|
+
*/
|
|
111
|
+
function parseDottedKey(raw: string): string[] {
|
|
112
|
+
const parts: string[] = [];
|
|
113
|
+
let current = "";
|
|
114
|
+
let inQuote = false;
|
|
115
|
+
let quoteChar = "";
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < raw.length; i++) {
|
|
118
|
+
const ch = raw[i];
|
|
119
|
+
if (inQuote) {
|
|
120
|
+
if (ch === quoteChar) {
|
|
121
|
+
inQuote = false;
|
|
122
|
+
} else {
|
|
123
|
+
current += ch;
|
|
124
|
+
}
|
|
125
|
+
} else if (ch === '"' || ch === "'") {
|
|
126
|
+
inQuote = true;
|
|
127
|
+
quoteChar = ch;
|
|
128
|
+
} else if (ch === ".") {
|
|
129
|
+
parts.push(current.trim());
|
|
130
|
+
current = "";
|
|
131
|
+
} else {
|
|
132
|
+
current += ch;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (current.trim()) parts.push(current.trim());
|
|
136
|
+
return parts;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Value parsing ─────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse a TOML value string.
|
|
143
|
+
*/
|
|
144
|
+
function parseTOMLValue(raw: string): unknown {
|
|
145
|
+
// Strip inline comments (not inside strings)
|
|
146
|
+
const value = stripInlineComment(raw);
|
|
147
|
+
|
|
148
|
+
if (value === "true") return true;
|
|
149
|
+
if (value === "false") return false;
|
|
150
|
+
|
|
151
|
+
// String values
|
|
152
|
+
if (value.startsWith('"""')) {
|
|
153
|
+
const end = value.indexOf('"""', 3);
|
|
154
|
+
return end !== -1 ? value.slice(3, end) : value.slice(3);
|
|
155
|
+
}
|
|
156
|
+
if (value.startsWith("'''")) {
|
|
157
|
+
const end = value.indexOf("'''", 3);
|
|
158
|
+
return end !== -1 ? value.slice(3, end) : value.slice(3);
|
|
159
|
+
}
|
|
160
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
161
|
+
return unescapeString(value.slice(1, -1));
|
|
162
|
+
}
|
|
163
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
164
|
+
return value.slice(1, -1); // Literal string, no escaping
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Array
|
|
168
|
+
if (value.startsWith("[")) {
|
|
169
|
+
return parseTOMLArray(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Inline table
|
|
173
|
+
if (value.startsWith("{")) {
|
|
174
|
+
return parseTOMLInlineTable(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Number
|
|
178
|
+
const num = Number(value);
|
|
179
|
+
if (!isNaN(num) && value !== "") return num;
|
|
180
|
+
|
|
181
|
+
// Bare string / date (return as string)
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parse a TOML inline array.
|
|
187
|
+
*/
|
|
188
|
+
function parseTOMLArray(raw: string): unknown[] {
|
|
189
|
+
// Remove outer brackets
|
|
190
|
+
const inner = raw.slice(1, raw.lastIndexOf("]")).trim();
|
|
191
|
+
if (inner === "") return [];
|
|
192
|
+
|
|
193
|
+
const items: unknown[] = [];
|
|
194
|
+
let current = "";
|
|
195
|
+
let depth = 0;
|
|
196
|
+
let inString = false;
|
|
197
|
+
let stringChar = "";
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < inner.length; i++) {
|
|
200
|
+
const ch = inner[i];
|
|
201
|
+
if (inString) {
|
|
202
|
+
current += ch;
|
|
203
|
+
if (ch === stringChar && inner[i - 1] !== "\\") {
|
|
204
|
+
inString = false;
|
|
205
|
+
}
|
|
206
|
+
} else if (ch === '"' || ch === "'") {
|
|
207
|
+
inString = true;
|
|
208
|
+
stringChar = ch;
|
|
209
|
+
current += ch;
|
|
210
|
+
} else if (ch === "[" || ch === "{") {
|
|
211
|
+
depth++;
|
|
212
|
+
current += ch;
|
|
213
|
+
} else if (ch === "]" || ch === "}") {
|
|
214
|
+
depth--;
|
|
215
|
+
current += ch;
|
|
216
|
+
} else if (ch === "," && depth === 0) {
|
|
217
|
+
const trimmed = current.trim();
|
|
218
|
+
if (trimmed !== "") items.push(parseTOMLValue(trimmed));
|
|
219
|
+
current = "";
|
|
220
|
+
} else {
|
|
221
|
+
current += ch;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const trimmed = current.trim();
|
|
226
|
+
if (trimmed !== "") items.push(parseTOMLValue(trimmed));
|
|
227
|
+
|
|
228
|
+
return items;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Parse a TOML inline table.
|
|
233
|
+
*/
|
|
234
|
+
function parseTOMLInlineTable(raw: string): Record<string, unknown> {
|
|
235
|
+
const inner = raw.slice(1, raw.lastIndexOf("}")).trim();
|
|
236
|
+
if (inner === "") return {};
|
|
237
|
+
|
|
238
|
+
const result: Record<string, unknown> = {};
|
|
239
|
+
let current = "";
|
|
240
|
+
let depth = 0;
|
|
241
|
+
let inString = false;
|
|
242
|
+
let stringChar = "";
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < inner.length; i++) {
|
|
245
|
+
const ch = inner[i];
|
|
246
|
+
if (inString) {
|
|
247
|
+
current += ch;
|
|
248
|
+
if (ch === stringChar && inner[i - 1] !== "\\") {
|
|
249
|
+
inString = false;
|
|
250
|
+
}
|
|
251
|
+
} else if (ch === '"' || ch === "'") {
|
|
252
|
+
inString = true;
|
|
253
|
+
stringChar = ch;
|
|
254
|
+
current += ch;
|
|
255
|
+
} else if (ch === "[" || ch === "{") {
|
|
256
|
+
depth++;
|
|
257
|
+
current += ch;
|
|
258
|
+
} else if (ch === "]" || ch === "}") {
|
|
259
|
+
depth--;
|
|
260
|
+
current += ch;
|
|
261
|
+
} else if (ch === "," && depth === 0) {
|
|
262
|
+
parseInlineTableEntry(current.trim(), result);
|
|
263
|
+
current = "";
|
|
264
|
+
} else {
|
|
265
|
+
current += ch;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (current.trim()) parseInlineTableEntry(current.trim(), result);
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseInlineTableEntry(entry: string, target: Record<string, unknown>): void {
|
|
274
|
+
const kv = parseKeyValue(entry);
|
|
275
|
+
if (kv) {
|
|
276
|
+
setNestedValue(target, kv.key, kv.value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Object path helpers ───────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
function ensureTableAt(root: Record<string, unknown>, path: string[]): Record<string, unknown> {
|
|
283
|
+
let current = root;
|
|
284
|
+
for (const segment of path) {
|
|
285
|
+
if (!(segment in current)) {
|
|
286
|
+
current[segment] = {};
|
|
287
|
+
}
|
|
288
|
+
const next = current[segment];
|
|
289
|
+
if (Array.isArray(next)) {
|
|
290
|
+
// Navigate into last array entry
|
|
291
|
+
current = next[next.length - 1] as Record<string, unknown>;
|
|
292
|
+
} else if (typeof next === "object" && next !== null) {
|
|
293
|
+
current = next as Record<string, unknown>;
|
|
294
|
+
} else {
|
|
295
|
+
const obj: Record<string, unknown> = {};
|
|
296
|
+
current[segment] = obj;
|
|
297
|
+
current = obj;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return current;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function ensureArrayAt(root: Record<string, unknown>, path: string[]): unknown[] {
|
|
304
|
+
let current = root;
|
|
305
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
306
|
+
const segment = path[i];
|
|
307
|
+
if (!(segment in current)) {
|
|
308
|
+
current[segment] = {};
|
|
309
|
+
}
|
|
310
|
+
const next = current[segment];
|
|
311
|
+
if (Array.isArray(next)) {
|
|
312
|
+
current = next[next.length - 1] as Record<string, unknown>;
|
|
313
|
+
} else if (typeof next === "object" && next !== null) {
|
|
314
|
+
current = next as Record<string, unknown>;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const lastKey = path[path.length - 1];
|
|
319
|
+
if (!(lastKey in current) || !Array.isArray(current[lastKey])) {
|
|
320
|
+
current[lastKey] = [];
|
|
321
|
+
}
|
|
322
|
+
return current[lastKey] as unknown[];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getTableAt(root: Record<string, unknown>, path: string[]): Record<string, unknown> {
|
|
326
|
+
let current = root;
|
|
327
|
+
for (const segment of path) {
|
|
328
|
+
const next = current[segment];
|
|
329
|
+
if (Array.isArray(next)) {
|
|
330
|
+
current = next[next.length - 1] as Record<string, unknown>;
|
|
331
|
+
} else if (typeof next === "object" && next !== null) {
|
|
332
|
+
current = next as Record<string, unknown>;
|
|
333
|
+
} else {
|
|
334
|
+
return current;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return current;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getLastArrayEntry(root: Record<string, unknown>, path: string[]): Record<string, unknown> | null {
|
|
341
|
+
let current = root;
|
|
342
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
343
|
+
const next = current[path[i]];
|
|
344
|
+
if (Array.isArray(next)) {
|
|
345
|
+
current = next[next.length - 1] as Record<string, unknown>;
|
|
346
|
+
} else if (typeof next === "object" && next !== null) {
|
|
347
|
+
current = next as Record<string, unknown>;
|
|
348
|
+
} else {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const lastKey = path[path.length - 1];
|
|
354
|
+
const arr = current[lastKey];
|
|
355
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
356
|
+
return arr[arr.length - 1] as Record<string, unknown>;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function setNestedValue(target: Record<string, unknown>, key: string[], value: unknown): void {
|
|
362
|
+
let current = target;
|
|
363
|
+
for (let i = 0; i < key.length - 1; i++) {
|
|
364
|
+
if (!(key[i] in current)) {
|
|
365
|
+
current[key[i]] = {};
|
|
366
|
+
}
|
|
367
|
+
current = current[key[i]] as Record<string, unknown>;
|
|
368
|
+
}
|
|
369
|
+
current[key[key.length - 1]] = value;
|
|
370
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TOML utilities — key escaping, string unescaping, etc.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escape a TOML key if it contains special characters.
|
|
7
|
+
*/
|
|
8
|
+
export function escapeKey(key: string): string {
|
|
9
|
+
if (/^[A-Za-z0-9_-]+$/.test(key)) return key;
|
|
10
|
+
return `"${key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Unescape a TOML basic string.
|
|
15
|
+
*/
|
|
16
|
+
export function unescapeString(val: string): string {
|
|
17
|
+
return val
|
|
18
|
+
.replace(/\\n/g, "\n")
|
|
19
|
+
.replace(/\\t/g, "\t")
|
|
20
|
+
.replace(/\\r/g, "\r")
|
|
21
|
+
.replace(/\\"/g, '"')
|
|
22
|
+
.replace(/\\\\/g, "\\");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sort keys according to a preferred order, with unmatched keys at the end.
|
|
27
|
+
*/
|
|
28
|
+
export function sortKeys(keys: string[], keyOrder?: string[]): string[] {
|
|
29
|
+
if (!keyOrder || keyOrder.length === 0) return keys;
|
|
30
|
+
const orderMap = new Map(keyOrder.map((k, i) => [k, i]));
|
|
31
|
+
return [...keys].sort((a, b) => {
|
|
32
|
+
const ai = orderMap.get(a) ?? Infinity;
|
|
33
|
+
const bi = orderMap.get(b) ?? Infinity;
|
|
34
|
+
if (ai !== bi) return ai - bi;
|
|
35
|
+
return 0; // preserve original order for unmatched
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip inline comment from a value string.
|
|
41
|
+
*/
|
|
42
|
+
export function stripInlineComment(raw: string): string {
|
|
43
|
+
let inString = false;
|
|
44
|
+
let stringChar = "";
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < raw.length; i++) {
|
|
47
|
+
const ch = raw[i];
|
|
48
|
+
if (inString) {
|
|
49
|
+
if (ch === stringChar && raw[i - 1] !== "\\") {
|
|
50
|
+
inString = false;
|
|
51
|
+
}
|
|
52
|
+
} else if (ch === '"' || ch === "'") {
|
|
53
|
+
inString = true;
|
|
54
|
+
stringChar = ch;
|
|
55
|
+
} else if (ch === "#") {
|
|
56
|
+
return raw.slice(0, i).trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return raw.trim();
|
|
60
|
+
}
|