@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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @vertekum/schema-builder
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @vertekum/core@0.10.0
9
+
3
10
  ## 0.9.0
4
11
 
5
12
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertekum/schema-builder",
3
- "version": "0.9.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.9.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.9.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
- if (hits.length === 0) {
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 hits[0];
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 (let i = node.steps.length - 1; i > 0; i--) {
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
- for (let i = 0; i < tokens.length; i++) {
72
- if (i > 0) out += separator(tokens[i - 1], tokens[i], tokens[i + 1]);
73
- out += text(tokens[i]);
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 (let i = 0; i < lines.length; i++) {
113
- const { comment } = splitComment(lines[i]);
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 section = line.match(/^\[(.+)\]$/);
188
- if (section) {
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 pair = line.match(/^([A-Za-z_]+)\s*=\s*(.+)$/);
198
- if (!pair) continue;
199
- if (pair[1] === 'indent_style') style = pair[2].trim();
200
- if (pair[1] === 'indent_size') size = pair[2].trim();
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
- if (repaired) {
263
- const raw = lines[line - 1];
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
- return this.tokens[this.index++];
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 options: Node[] = [this.path()];
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 options[0];
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 steps: Step[] = [this.step()];
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 && !steps[0].optional) return steps[0].term;
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[]): number | undefined {
74
- const declared = literals.filter((l) => l.length > 1 && l.startsWith('0'));
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 (let i = 0; i < lines.length; i++) {
88
+ for (const [i, raw] of lines.entries()) {
87
89
  const line = i + 1;
88
- let text = lines[i];
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
- const kind = single[text[0]];
201
- if (!kind) throw new DfnError(`unexpected '${text[0]}'`, line, column);
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, text[0]);
208
+ push(kind, char);
205
209
  }
206
210
 
207
211
  // Inside a block, a newline is not a statement boundary.