@vertekum/schema-builder 0.9.0 → 0.10.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/CHANGELOG.md +7 -0
- package/package.json +3 -3
- package/src/build.ts +8 -5
- package/src/format.ts +18 -14
- package/src/parser.ts +17 -6
- package/src/tokenizer.ts +11 -7
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertekum/schema-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Declare token vocabularies as grammar; build them into JSON Schema files",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"./cli": "./src/cli.ts"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@vertekum/core": "0.
|
|
40
|
+
"@vertekum/core": "0.10.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"zod": "^3.25.76"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@vertekum/core": "0.
|
|
46
|
+
"@vertekum/core": "0.10.0",
|
|
47
47
|
"@vertekum/schema-vocabulary": "0.3.0"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/build.ts
CHANGED
|
@@ -169,7 +169,8 @@ function target(
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
const [hit] = hits;
|
|
173
|
+
if (hit === undefined) {
|
|
173
174
|
throw new DfnError(
|
|
174
175
|
`no import provides '<@${ref.name}>'`,
|
|
175
176
|
ref.line,
|
|
@@ -185,7 +186,7 @@ function target(
|
|
|
185
186
|
file,
|
|
186
187
|
);
|
|
187
188
|
}
|
|
188
|
-
return
|
|
189
|
+
return hit;
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
/**
|
|
@@ -276,9 +277,12 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
276
277
|
// Steps expand right to left: each step becomes the tail of the one before it. An
|
|
277
278
|
// optional step (`?`) may be SKIPPED — its tail also attaches directly, so
|
|
278
279
|
// `<role>.<emphasis>?.<interaction>?` grants the whole slot-collapse lattice.
|
|
280
|
+
const [first, ...rest] = node.steps;
|
|
281
|
+
if (first === undefined) {
|
|
282
|
+
throw new Error('internal: a path has at least one step');
|
|
283
|
+
}
|
|
279
284
|
let next = tail;
|
|
280
|
-
for (
|
|
281
|
-
const step = node.steps[i];
|
|
285
|
+
for (const step of rest.reverse()) {
|
|
282
286
|
const after = next;
|
|
283
287
|
next = step.optional
|
|
284
288
|
? () => {
|
|
@@ -288,7 +292,6 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
288
292
|
}
|
|
289
293
|
: () => evaluate(step.term, scope, after);
|
|
290
294
|
}
|
|
291
|
-
const first = node.steps[0];
|
|
292
295
|
const forest = evaluate(first.term, scope, next);
|
|
293
296
|
if (first.optional) merge(forest, next());
|
|
294
297
|
return forest;
|
package/src/format.ts
CHANGED
|
@@ -68,9 +68,11 @@ function separator(prev: Token, cur: Token, next: Token | undefined): string {
|
|
|
68
68
|
/** Re-emit one line's tokens under the canonical spacing rules. */
|
|
69
69
|
function printTokens(tokens: Token[]): string {
|
|
70
70
|
let out = '';
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
out +=
|
|
71
|
+
let prev: Token | undefined;
|
|
72
|
+
for (const [i, token] of tokens.entries()) {
|
|
73
|
+
if (prev) out += separator(prev, token, tokens[i + 1]);
|
|
74
|
+
out += text(token);
|
|
75
|
+
prev = token;
|
|
74
76
|
}
|
|
75
77
|
return out;
|
|
76
78
|
}
|
|
@@ -109,8 +111,8 @@ export function formatSource(
|
|
|
109
111
|
let level = 0;
|
|
110
112
|
let blankRun = 0;
|
|
111
113
|
|
|
112
|
-
for (
|
|
113
|
-
const { comment } = splitComment(
|
|
114
|
+
for (const [i, raw] of lines.entries()) {
|
|
115
|
+
const { comment } = splitComment(raw);
|
|
114
116
|
const lineTokens = byLine.get(i + 1) ?? [];
|
|
115
117
|
|
|
116
118
|
if (lineTokens.length === 0 && comment === null) {
|
|
@@ -184,9 +186,8 @@ export function resolveIndent(
|
|
|
184
186
|
let applies = false;
|
|
185
187
|
for (const raw of content.split('\n')) {
|
|
186
188
|
const line = raw.trim();
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
const glob = section[1];
|
|
189
|
+
const glob = line.match(/^\[(.+)\]$/)?.[1];
|
|
190
|
+
if (glob !== undefined) {
|
|
190
191
|
applies =
|
|
191
192
|
glob === '*' ||
|
|
192
193
|
glob === '**' ||
|
|
@@ -194,10 +195,10 @@ export function resolveIndent(
|
|
|
194
195
|
continue;
|
|
195
196
|
}
|
|
196
197
|
if (!applies) continue;
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
if (
|
|
200
|
-
if (
|
|
198
|
+
const [, key, value] = line.match(/^([A-Za-z_]+)\s*=\s*(.+)$/) ?? [];
|
|
199
|
+
if (key === undefined || value === undefined) continue;
|
|
200
|
+
if (key === 'indent_style') style = value.trim();
|
|
201
|
+
if (key === 'indent_size') size = value.trim();
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -245,6 +246,8 @@ export function fixSource(source: string): {
|
|
|
245
246
|
for (let i = 0; i + 1 < lineTokens.length; i++) {
|
|
246
247
|
const closer = lineTokens[i];
|
|
247
248
|
const star = lineTokens[i + 1];
|
|
249
|
+
// The loop bound keeps both indices in range; the guard only narrows.
|
|
250
|
+
if (closer === undefined || star === undefined) break;
|
|
248
251
|
if (
|
|
249
252
|
(closer.kind === 'rangle' || closer.kind === 'rbracket') &&
|
|
250
253
|
star.kind === 'star'
|
|
@@ -259,8 +262,9 @@ export function fixSource(source: string): {
|
|
|
259
262
|
});
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
const raw = lines[line - 1];
|
|
266
|
+
// Every token's line exists in the source it was lexed from.
|
|
267
|
+
if (repaired && raw !== undefined) {
|
|
264
268
|
const { comment } = splitComment(raw);
|
|
265
269
|
const leading = raw.match(/^\s*/)?.[0] ?? '';
|
|
266
270
|
const code = printTokens(lineTokens);
|
package/src/parser.ts
CHANGED
|
@@ -14,14 +14,21 @@ export function parse(source: string): Module {
|
|
|
14
14
|
|
|
15
15
|
class Parser {
|
|
16
16
|
private readonly tokens: Token[];
|
|
17
|
+
/** The stream's closing `eof` — what the cursor sees once it runs past the end. */
|
|
18
|
+
private readonly eof: Token;
|
|
17
19
|
private index = 0;
|
|
18
20
|
|
|
19
21
|
constructor(tokens: Token[]) {
|
|
22
|
+
const last = tokens[tokens.length - 1];
|
|
23
|
+
if (last?.kind !== 'eof') {
|
|
24
|
+
throw new Error('internal: a token stream ends with eof');
|
|
25
|
+
}
|
|
20
26
|
this.tokens = tokens;
|
|
27
|
+
this.eof = last;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
private peek(): Token {
|
|
24
|
-
return this.tokens[this.index];
|
|
31
|
+
return this.tokens[this.index] ?? this.eof;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
private at(offset: number): Token | undefined {
|
|
@@ -29,7 +36,9 @@ class Parser {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
private next(): Token {
|
|
32
|
-
|
|
39
|
+
const token = this.peek();
|
|
40
|
+
this.index++;
|
|
41
|
+
return token;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
private expect(kind: Token['kind'], what: string): Token {
|
|
@@ -204,24 +213,26 @@ class Parser {
|
|
|
204
213
|
|
|
205
214
|
/** `a | b | c` — a single option collapses to itself. */
|
|
206
215
|
private alternation(): Node {
|
|
207
|
-
const
|
|
216
|
+
const first = this.path();
|
|
217
|
+
const options: Node[] = [first];
|
|
208
218
|
while (this.peek().kind === 'pipe') {
|
|
209
219
|
this.next();
|
|
210
220
|
options.push(this.path());
|
|
211
221
|
}
|
|
212
|
-
if (options.length === 1) return
|
|
222
|
+
if (options.length === 1) return first;
|
|
213
223
|
const alt: Alt = { kind: 'alt', options };
|
|
214
224
|
return alt;
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
/** `a.b.c` — a single step with no `?` collapses to its term. */
|
|
218
228
|
private path(): Node {
|
|
219
|
-
const
|
|
229
|
+
const first = this.step();
|
|
230
|
+
const steps: Step[] = [first];
|
|
220
231
|
while (this.peek().kind === 'dot') {
|
|
221
232
|
this.next();
|
|
222
233
|
steps.push(this.step());
|
|
223
234
|
}
|
|
224
|
-
if (steps.length === 1 && !
|
|
235
|
+
if (steps.length === 1 && !first.optional) return first.term;
|
|
225
236
|
const path: Path = { kind: 'path', steps };
|
|
226
237
|
return path;
|
|
227
238
|
}
|
package/src/tokenizer.ts
CHANGED
|
@@ -70,8 +70,10 @@ const WORD = /^\d+[A-Za-z][A-Za-z0-9-]*/;
|
|
|
70
70
|
const NUMBER = /^\d+/;
|
|
71
71
|
|
|
72
72
|
/** "Write the numbers as they appear": a leading zero declares the pad width. */
|
|
73
|
-
function padWidth(...literals: string
|
|
74
|
-
const declared = literals.filter(
|
|
73
|
+
function padWidth(...literals: Array<string | undefined>): number | undefined {
|
|
74
|
+
const declared = literals.filter(
|
|
75
|
+
(l): l is string => l !== undefined && l.length > 1 && l.startsWith('0'),
|
|
76
|
+
);
|
|
75
77
|
if (declared.length === 0) return undefined;
|
|
76
78
|
return Math.max(...declared.map((l) => l.length));
|
|
77
79
|
}
|
|
@@ -83,9 +85,9 @@ export function tokenize(source: string): Token[] {
|
|
|
83
85
|
// this stack is non-empty a group is a BLOCK — newlines inside it are insignificant.
|
|
84
86
|
const openBrackets: Array<{ line: number; column: number }> = [];
|
|
85
87
|
|
|
86
|
-
for (
|
|
88
|
+
for (const [i, raw] of lines.entries()) {
|
|
87
89
|
const line = i + 1;
|
|
88
|
-
let text =
|
|
90
|
+
let text = raw;
|
|
89
91
|
const commentAt = text.indexOf('#');
|
|
90
92
|
if (commentAt !== -1) text = text.slice(0, commentAt);
|
|
91
93
|
|
|
@@ -197,11 +199,13 @@ export function tokenize(source: string): Token[] {
|
|
|
197
199
|
'/': 'slash',
|
|
198
200
|
':': 'colon',
|
|
199
201
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
// The loop condition guarantees a non-empty `text`, so `charAt(0)` is its first character.
|
|
203
|
+
const char = text.charAt(0);
|
|
204
|
+
const kind = single[char];
|
|
205
|
+
if (!kind) throw new DfnError(`unexpected '${char}'`, line, column);
|
|
202
206
|
if (kind === 'lbracket') openBrackets.push({ line, column });
|
|
203
207
|
if (kind === 'rbracket') openBrackets.pop();
|
|
204
|
-
push(kind,
|
|
208
|
+
push(kind, char);
|
|
205
209
|
}
|
|
206
210
|
|
|
207
211
|
// Inside a block, a newline is not a statement boundary.
|