@vertekum/schema-builder 0.1.2 → 0.1.4
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 +16 -0
- package/README.md +3 -1
- package/grammar/dfn.language-configuration.json +11 -0
- package/grammar/dfn.tmLanguage.json +251 -0
- package/package.json +4 -3
- package/src/ast.ts +12 -3
- package/src/build.ts +40 -4
- package/src/cli.ts +11 -3
- package/src/parser.ts +60 -12
- package/src/resolve.ts +9 -4
- package/src/tokenizer.ts +23 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @vertekum/schema-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`6932b20`](https://github.com/moros-oxus/vertekum/commit/6932b20cd3c4e133de5c465bacea5dd1fada2d06) Thanks [@tschemmer](https://github.com/tschemmer)! - Imports can be aliased — `use "./palette/color.dfn" as palette` — making `<@palette>` the module's root and `<@palette/name>` a qualified reference, which resolves same-basename imports (the duplicate-import error now suggests it). Provenance stamps record a caller-supplied module label (the CLI passes the project-relative path), so nested same-named modules stamp distinguishably; `buildModule` accepts the label as its second argument.
|
|
8
|
+
|
|
9
|
+
- [`9c0f093`](https://github.com/moros-oxus/vertekum/commit/9c0f093b5016c6c0ca9cb5b5fd48598718e698d9) Thanks [@tschemmer](https://github.com/tschemmer)! - Range terms grow into scale expressions: a leading zero on a written endpoint zero-pads every emitted name (`025-100/25` → `025 050 075 100`), `min-max*factor` declares a geometric scale (fractional factors allowed), and `~quantum` rounds each step to the nearest multiple (`16-64*1.25~4` → `16 20 24 32 40 48 60`). Steps that quantize onto an earlier name are a build error.
|
|
10
|
+
- Updated dependencies [[`9c0f093`](https://github.com/moros-oxus/vertekum/commit/9c0f093b5016c6c0ca9cb5b5fd48598718e698d9)]:
|
|
11
|
+
- @vertekum/core@0.1.2
|
|
12
|
+
|
|
13
|
+
## 0.1.3
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [`67ef6d9`](https://github.com/moros-oxus/vertekum/commit/67ef6d96ad209072df270adccaa7ee879edbc30e) Thanks [@tschemmer](https://github.com/tschemmer)! - The package now ships a TextMate grammar and language configuration for `.dfn` files under `grammar/` — the single source editor integrations and documentation highlighters (e.g. Shiki) consume.
|
|
18
|
+
|
|
3
19
|
## 0.1.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ indented line continues the statement above it.
|
|
|
57
57
|
| `name = expression` | a **production**: a named fragment (a denotation, a branch, a whole subtree) |
|
|
58
58
|
| `root = expression` | the reserved production `build` materializes; one per module |
|
|
59
59
|
| `use "./other.dfn"` | import a module (relative path or package specifier) |
|
|
60
|
+
| `use "./other.dfn" as x` | alias the import: `<@x>` is its root, `<@x/name>` qualifies into it — the resolver when two imports share a basename |
|
|
60
61
|
| `id "…"` `title "…"` `description "…"` | pragmas: the built schema's `$id`, `title`, `description` |
|
|
61
62
|
| `scope "branch"` | the schema governs only its named top-level branches — the document root stays unsealed so sibling vocabularies can bind over the same files (default: `"document"`, which seals it) |
|
|
62
63
|
|
|
@@ -74,7 +75,8 @@ indented line continues the statement above it.
|
|
|
74
75
|
| `<name ![a, b]>` | **omit** — the set minus the listed members |
|
|
75
76
|
| `<name*>` / `[a \| b *]` | **open set** — additions beyond the listed names are permitted, and every member (listed or added) takes the same tail |
|
|
76
77
|
| `step?` | **optional slot** — the step may be skipped: `<role>.<emphasis>?.<state>?` grants `role`, `role.<emphasis>`, `role.<state>`, and the full path |
|
|
77
|
-
| `100-900/100` |
|
|
78
|
+
| `100-900/100` | an additive scale, inclusive both ends, enumerated at build; a leading zero on a written endpoint (`025-100/25`) zero-pads every emitted name |
|
|
79
|
+
| `16-64*1.25~4` | a geometric scale: `min-max*factor` (fractional ok), `~quantum` rounding each step to the nearest multiple; steps that collide onto an earlier name are a build error |
|
|
78
80
|
|
|
79
81
|
Pick and omit validate every listed name against the set's members — a typo is a build
|
|
80
82
|
error, and a modified set is a new set. Naming one derives a narrowed denotation:
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
3
|
+
"name": "DFN",
|
|
4
|
+
"scopeName": "source.dfn",
|
|
5
|
+
"patterns": [
|
|
6
|
+
{
|
|
7
|
+
"include": "#comment"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"include": "#pragma"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"include": "#use"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"include": "#root"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"include": "#production"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"include": "#expression"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"comment": {
|
|
27
|
+
"name": "comment.line.number-sign.dfn",
|
|
28
|
+
"match": "#.*$"
|
|
29
|
+
},
|
|
30
|
+
"string": {
|
|
31
|
+
"name": "string.quoted.double.dfn",
|
|
32
|
+
"begin": "\"",
|
|
33
|
+
"end": "\"",
|
|
34
|
+
"patterns": []
|
|
35
|
+
},
|
|
36
|
+
"pragma": {
|
|
37
|
+
"match": "^(id|title|description|scope)\\s+(?=\")",
|
|
38
|
+
"captures": {
|
|
39
|
+
"1": {
|
|
40
|
+
"name": "keyword.other.pragma.dfn"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"use": {
|
|
45
|
+
"match": "^(use)\\s+(?=\")",
|
|
46
|
+
"captures": {
|
|
47
|
+
"1": {
|
|
48
|
+
"name": "keyword.control.import.dfn"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"root": {
|
|
53
|
+
"match": "^(root)\\s*(=)",
|
|
54
|
+
"captures": {
|
|
55
|
+
"1": {
|
|
56
|
+
"name": "keyword.control.root.dfn"
|
|
57
|
+
},
|
|
58
|
+
"2": {
|
|
59
|
+
"name": "keyword.operator.assignment.dfn"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"production": {
|
|
64
|
+
"match": "^([A-Za-z][A-Za-z0-9-]*)\\s*(=)",
|
|
65
|
+
"captures": {
|
|
66
|
+
"1": {
|
|
67
|
+
"name": "entity.name.function.production.dfn"
|
|
68
|
+
},
|
|
69
|
+
"2": {
|
|
70
|
+
"name": "keyword.operator.assignment.dfn"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"expression": {
|
|
75
|
+
"patterns": [
|
|
76
|
+
{
|
|
77
|
+
"include": "#string"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"include": "#reference"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"include": "#range"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"include": "#number"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"include": "#operator"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"include": "#group"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"include": "#name"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"reference": {
|
|
100
|
+
"patterns": [
|
|
101
|
+
{
|
|
102
|
+
"include": "#reference-imported"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"include": "#reference-local"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"reference-imported": {
|
|
110
|
+
"begin": "(<)(@)",
|
|
111
|
+
"end": "(\\*)?(>)",
|
|
112
|
+
"beginCaptures": {
|
|
113
|
+
"1": {
|
|
114
|
+
"name": "punctuation.definition.reference.begin.dfn"
|
|
115
|
+
},
|
|
116
|
+
"2": {
|
|
117
|
+
"name": "punctuation.definition.imported.dfn"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"endCaptures": {
|
|
121
|
+
"1": {
|
|
122
|
+
"name": "keyword.operator.open-set.dfn"
|
|
123
|
+
},
|
|
124
|
+
"2": {
|
|
125
|
+
"name": "punctuation.definition.reference.end.dfn"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"patterns": [
|
|
129
|
+
{
|
|
130
|
+
"match": "([A-Za-z][A-Za-z0-9-]*)(/)([A-Za-z][A-Za-z0-9-]*)",
|
|
131
|
+
"captures": {
|
|
132
|
+
"1": {
|
|
133
|
+
"name": "entity.name.namespace.module.dfn"
|
|
134
|
+
},
|
|
135
|
+
"2": {
|
|
136
|
+
"name": "punctuation.separator.qualified.dfn"
|
|
137
|
+
},
|
|
138
|
+
"3": {
|
|
139
|
+
"name": "variable.other.reference.imported.dfn"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"include": "#reference-modifiers"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "variable.other.reference.imported.dfn",
|
|
148
|
+
"match": "[A-Za-z][A-Za-z0-9-]*"
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
"reference-local": {
|
|
153
|
+
"begin": "(<)",
|
|
154
|
+
"end": "(\\*)?(>)",
|
|
155
|
+
"beginCaptures": {
|
|
156
|
+
"1": {
|
|
157
|
+
"name": "punctuation.definition.reference.begin.dfn"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"endCaptures": {
|
|
161
|
+
"1": {
|
|
162
|
+
"name": "keyword.operator.open-set.dfn"
|
|
163
|
+
},
|
|
164
|
+
"2": {
|
|
165
|
+
"name": "punctuation.definition.reference.end.dfn"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"patterns": [
|
|
169
|
+
{
|
|
170
|
+
"include": "#reference-modifiers"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "variable.other.reference.dfn",
|
|
174
|
+
"match": "[A-Za-z][A-Za-z0-9-]*"
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"reference-modifiers": {
|
|
179
|
+
"patterns": [
|
|
180
|
+
{
|
|
181
|
+
"name": "keyword.operator.omit.dfn",
|
|
182
|
+
"match": "!"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"begin": "\\[",
|
|
186
|
+
"end": "\\]",
|
|
187
|
+
"beginCaptures": {
|
|
188
|
+
"0": {
|
|
189
|
+
"name": "punctuation.definition.modifier.begin.dfn"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"endCaptures": {
|
|
193
|
+
"0": {
|
|
194
|
+
"name": "punctuation.definition.modifier.end.dfn"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"patterns": [
|
|
198
|
+
{
|
|
199
|
+
"name": "variable.parameter.member.dfn",
|
|
200
|
+
"match": "[A-Za-z][A-Za-z0-9-]*|\\d+"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "punctuation.separator.comma.dfn",
|
|
204
|
+
"match": ","
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
"range": {
|
|
211
|
+
"name": "constant.numeric.range.dfn",
|
|
212
|
+
"match": "\\d+-\\d+[*/]\\d+(?:\\.\\d+)?(?:~\\d+)?"
|
|
213
|
+
},
|
|
214
|
+
"number": {
|
|
215
|
+
"name": "constant.numeric.dfn",
|
|
216
|
+
"match": "\\b\\d+\\b"
|
|
217
|
+
},
|
|
218
|
+
"operator": {
|
|
219
|
+
"patterns": [
|
|
220
|
+
{
|
|
221
|
+
"name": "keyword.operator.optional.dfn",
|
|
222
|
+
"match": "\\?"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "keyword.operator.dfn",
|
|
226
|
+
"match": "[|.=]"
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
"group": {
|
|
231
|
+
"patterns": [
|
|
232
|
+
{
|
|
233
|
+
"name": "punctuation.definition.group.begin.dfn",
|
|
234
|
+
"match": "\\["
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "keyword.operator.open-set.dfn",
|
|
238
|
+
"match": "\\*(?=\\s*\\])"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "punctuation.definition.group.end.dfn",
|
|
242
|
+
"match": "\\]"
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
"name": {
|
|
247
|
+
"name": "constant.language.name.dfn",
|
|
248
|
+
"match": "[A-Za-z][A-Za-z0-9-]*"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertekum/schema-builder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Declare token vocabularies as grammar; build them into JSON Schema files",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"bugs": "https://github.com/moros-oxus/vertekum/issues",
|
|
20
20
|
"files": [
|
|
21
21
|
"CHANGELOG.md",
|
|
22
|
+
"grammar",
|
|
22
23
|
"src",
|
|
23
24
|
"tsconfig.json",
|
|
24
25
|
"!src/**/*.test.ts"
|
|
@@ -35,13 +36,13 @@
|
|
|
35
36
|
"./cli": "./src/cli.ts"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
|
-
"@vertekum/core": "0.1.
|
|
39
|
+
"@vertekum/core": "0.1.2"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"zod": "^3.25.76"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@vertekum/core": "0.1.
|
|
45
|
+
"@vertekum/core": "0.1.2",
|
|
45
46
|
"@vertekum/schema-vocabulary": "0.1.0"
|
|
46
47
|
}
|
|
47
48
|
}
|
package/src/ast.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
/** A parsed module: imports, named productions, and the reserved `root` (absent in fragments). */
|
|
9
9
|
export interface Module {
|
|
10
|
-
/** `use "<specifier>"` statements, in order
|
|
11
|
-
uses: string
|
|
10
|
+
/** `use "<specifier>" [as <alias>]` statements, in order. */
|
|
11
|
+
uses: Array<{ spec: string; alias?: string }>;
|
|
12
12
|
/** Productions by name. `root` is never stored here. */
|
|
13
13
|
productions: Map<string, Node>;
|
|
14
14
|
/** The reserved `root` production, when declared. */
|
|
@@ -56,12 +56,21 @@ export interface Name {
|
|
|
56
56
|
value: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* A numeric name scale, enumerated at build time. `min-max/step` is additive;
|
|
61
|
+
* `min-max*factor` is geometric (fractional factors allowed), optionally quantized to the
|
|
62
|
+
* nearest multiple of `quantum` (`~4`). A leading zero on a written endpoint (`025`)
|
|
63
|
+
* declares the zero-pad width for every emitted name.
|
|
64
|
+
*/
|
|
60
65
|
export interface Range {
|
|
61
66
|
kind: 'range';
|
|
62
67
|
min: number;
|
|
63
68
|
max: number;
|
|
69
|
+
mode: 'stepped' | 'multiplied';
|
|
70
|
+
/** The additive step, or the geometric factor. */
|
|
64
71
|
step: number;
|
|
72
|
+
quantum?: number;
|
|
73
|
+
pad?: number;
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
/**
|
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateScale } from '@vertekum/core';
|
|
1
2
|
import type { Node, Ref } from './ast';
|
|
2
3
|
import { DfnError } from './error';
|
|
3
4
|
import type { ResolvedModule } from './resolve';
|
|
@@ -59,11 +60,11 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const hits: Array<{ node: Node; scope: Scope }> = [];
|
|
62
|
-
for (const imported of scope.module.imports
|
|
63
|
+
for (const [key, imported] of scope.module.imports) {
|
|
63
64
|
const importedScope: Scope = { module: imported, expanding: new Set() };
|
|
64
65
|
const production = imported.module.productions.get(ref.name);
|
|
65
66
|
if (production) hits.push({ node: production, scope: importedScope });
|
|
66
|
-
if (
|
|
67
|
+
if (key === ref.name && imported.module.root) {
|
|
67
68
|
hits.push({ node: imported.module.root, scope: importedScope });
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -93,9 +94,44 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
93
94
|
return forest;
|
|
94
95
|
}
|
|
95
96
|
case 'range': {
|
|
97
|
+
// One authority for scale names (and, later, token values): core's evaluateScale.
|
|
98
|
+
let scale: ReturnType<typeof evaluateScale>;
|
|
99
|
+
try {
|
|
100
|
+
scale = evaluateScale(
|
|
101
|
+
node.mode === 'stepped'
|
|
102
|
+
? {
|
|
103
|
+
kind: 'stepped',
|
|
104
|
+
min: node.min,
|
|
105
|
+
max: node.max,
|
|
106
|
+
step: node.step,
|
|
107
|
+
pad: node.pad,
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
kind: 'multiplied',
|
|
111
|
+
min: node.min,
|
|
112
|
+
max: node.max,
|
|
113
|
+
factor: node.step,
|
|
114
|
+
quantum: node.quantum,
|
|
115
|
+
pad: node.pad,
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new DfnError(
|
|
120
|
+
error instanceof Error ? error.message : String(error),
|
|
121
|
+
1,
|
|
122
|
+
1,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (scale.collisions.length > 0) {
|
|
126
|
+
throw new DfnError(
|
|
127
|
+
`scale steps ${scale.collisions.join(', ')} quantize onto earlier names — the quantum is too coarse for the factor`,
|
|
128
|
+
1,
|
|
129
|
+
1,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
96
132
|
const forest = leaf();
|
|
97
|
-
for (
|
|
98
|
-
forest.children.set(
|
|
133
|
+
for (const name of scale.names) {
|
|
134
|
+
forest.children.set(name, tail());
|
|
99
135
|
}
|
|
100
136
|
return forest;
|
|
101
137
|
}
|
package/src/cli.ts
CHANGED
|
@@ -25,14 +25,19 @@ function modulesUnder(dir: string): string[] {
|
|
|
25
25
|
.map((f) => join(dir, f));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function buildModule(
|
|
28
|
+
export function buildModule(
|
|
29
|
+
modulePath: string,
|
|
30
|
+
/** The module name the provenance stamp records; default: the basename. Pass a relative
|
|
31
|
+
* path when modules nest, so same-named modules stamp distinguishably. */
|
|
32
|
+
moduleLabel?: string,
|
|
33
|
+
): {
|
|
29
34
|
target: string;
|
|
30
35
|
content: string;
|
|
31
36
|
} {
|
|
32
37
|
const resolved = resolveModule(modulePath);
|
|
33
38
|
assertOpenSetsAreNameSets(resolved);
|
|
34
39
|
const tree = build(resolved);
|
|
35
|
-
const moduleFile = basename(modulePath);
|
|
40
|
+
const moduleFile = moduleLabel ?? basename(modulePath);
|
|
36
41
|
return {
|
|
37
42
|
target: join(dirname(modulePath), `${resolved.name}.json`),
|
|
38
43
|
content: emit(tree, { moduleFile, ...resolved.module.meta }),
|
|
@@ -77,7 +82,10 @@ export const schemaBuildCommand: CommandDescriptor = {
|
|
|
77
82
|
fragments.push(relative(projectDir, modulePath));
|
|
78
83
|
continue;
|
|
79
84
|
}
|
|
80
|
-
const { target, content } = buildModule(
|
|
85
|
+
const { target, content } = buildModule(
|
|
86
|
+
modulePath,
|
|
87
|
+
relative(projectDir, modulePath),
|
|
88
|
+
);
|
|
81
89
|
const existing = existsSync(target)
|
|
82
90
|
? readFileSync(target, 'utf8')
|
|
83
91
|
: undefined;
|
package/src/parser.ts
CHANGED
|
@@ -49,7 +49,7 @@ class Parser {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
module(): Module {
|
|
52
|
-
const uses: string
|
|
52
|
+
const uses: Array<{ spec: string; alias?: string }> = [];
|
|
53
53
|
const productions = new Map<string, Node>();
|
|
54
54
|
const meta: ModuleMeta = {};
|
|
55
55
|
let root: Node | undefined;
|
|
@@ -59,7 +59,13 @@ class Parser {
|
|
|
59
59
|
const token = this.peek();
|
|
60
60
|
if (token.kind === 'use') {
|
|
61
61
|
this.next();
|
|
62
|
-
|
|
62
|
+
const spec = this.expect('string', 'a quoted specifier').value;
|
|
63
|
+
let alias: string | undefined;
|
|
64
|
+
if (this.peek().kind === 'ident' && this.peek().value === 'as') {
|
|
65
|
+
this.next();
|
|
66
|
+
alias = this.expect('ident', 'an import alias').value;
|
|
67
|
+
}
|
|
68
|
+
uses.push({ spec, alias });
|
|
63
69
|
} else if (token.kind === 'ident' && this.at(1)?.kind === 'string') {
|
|
64
70
|
// `<ident> "<string>"` (no `=`) is a PRAGMA — document metadata, not a production, so
|
|
65
71
|
// nothing is reserved: `title = a | b` still declares a production named title.
|
|
@@ -168,6 +174,19 @@ class Parser {
|
|
|
168
174
|
return { term, optional };
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
/** A pick/omit list member: an identifier or a (possibly padded) number. */
|
|
178
|
+
private member(): string {
|
|
179
|
+
const token = this.next();
|
|
180
|
+
if (token.kind !== 'ident' && token.kind !== 'number') {
|
|
181
|
+
throw new DfnError(
|
|
182
|
+
`expected a member name, got '${token.value || token.kind}'`,
|
|
183
|
+
token.line,
|
|
184
|
+
token.column,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return token.value;
|
|
188
|
+
}
|
|
189
|
+
|
|
171
190
|
private term(): Step['term'] {
|
|
172
191
|
const token = this.next();
|
|
173
192
|
switch (token.kind) {
|
|
@@ -175,19 +194,48 @@ class Parser {
|
|
|
175
194
|
case 'number':
|
|
176
195
|
return { kind: 'name', value: token.value };
|
|
177
196
|
case 'range': {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
step: number;
|
|
182
|
-
};
|
|
183
|
-
if (step === 0 || max < min) {
|
|
197
|
+
const payload = token.range as NonNullable<Token['range']>;
|
|
198
|
+
const { min, max, mode, step, quantum, pad } = payload;
|
|
199
|
+
if (max < min) {
|
|
184
200
|
throw new DfnError(
|
|
185
|
-
'range needs max >= min
|
|
201
|
+
'range needs max >= min',
|
|
186
202
|
token.line,
|
|
187
203
|
token.column,
|
|
188
204
|
);
|
|
189
205
|
}
|
|
190
|
-
|
|
206
|
+
if (mode === '/') {
|
|
207
|
+
if (quantum !== undefined) {
|
|
208
|
+
throw new DfnError(
|
|
209
|
+
'~quantum belongs to multiplied scales (min-max*factor~q)',
|
|
210
|
+
token.line,
|
|
211
|
+
token.column,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (step === 0 || !Number.isInteger(step)) {
|
|
215
|
+
throw new DfnError(
|
|
216
|
+
'a stepped range needs a whole-number step greater than zero',
|
|
217
|
+
token.line,
|
|
218
|
+
token.column,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return { kind: 'range', min, max, mode: 'stepped', step, pad };
|
|
222
|
+
}
|
|
223
|
+
if (step <= 1) {
|
|
224
|
+
throw new DfnError(
|
|
225
|
+
'a multiplied range needs a factor greater than one',
|
|
226
|
+
token.line,
|
|
227
|
+
token.column,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
kind: 'range',
|
|
232
|
+
min,
|
|
233
|
+
max,
|
|
234
|
+
mode: 'multiplied',
|
|
235
|
+
step,
|
|
236
|
+
quantum,
|
|
237
|
+
pad,
|
|
238
|
+
};
|
|
191
239
|
}
|
|
192
240
|
case 'langle': {
|
|
193
241
|
const imported = this.peek().kind === 'at';
|
|
@@ -216,10 +264,10 @@ class Parser {
|
|
|
216
264
|
if (this.peek().kind === 'lbracket') {
|
|
217
265
|
const into = negated ? omit : pick;
|
|
218
266
|
this.next();
|
|
219
|
-
into.push(this.
|
|
267
|
+
into.push(this.member());
|
|
220
268
|
while (this.peek().kind === 'comma') {
|
|
221
269
|
this.next();
|
|
222
|
-
into.push(this.
|
|
270
|
+
into.push(this.member());
|
|
223
271
|
}
|
|
224
272
|
this.expect('rbracket', "']'");
|
|
225
273
|
} else if (negated) {
|
package/src/resolve.ts
CHANGED
|
@@ -58,13 +58,18 @@ export function resolveModule(
|
|
|
58
58
|
const module = parse(source);
|
|
59
59
|
|
|
60
60
|
const imports = new Map<string, ResolvedModule>();
|
|
61
|
-
for (const spec of module.uses) {
|
|
61
|
+
for (const { spec, alias } of module.uses) {
|
|
62
62
|
const target = resolveSpecifier(spec, dirname(absolute));
|
|
63
63
|
const resolved = resolveModule(target, inProgress);
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const key = alias ?? resolved.name;
|
|
65
|
+
if (imports.has(key)) {
|
|
66
|
+
throw new DfnError(
|
|
67
|
+
`two imports share the name '${key}' — alias one: use "…" as other-name`,
|
|
68
|
+
1,
|
|
69
|
+
1,
|
|
70
|
+
);
|
|
66
71
|
}
|
|
67
|
-
imports.set(
|
|
72
|
+
imports.set(key, resolved);
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
inProgress.delete(absolute);
|
package/src/tokenizer.ts
CHANGED
|
@@ -31,17 +31,33 @@ export type TokenKind =
|
|
|
31
31
|
|
|
32
32
|
export interface Token {
|
|
33
33
|
kind: TokenKind;
|
|
34
|
-
/** Source text (for ident/number/string); ranges carry
|
|
34
|
+
/** Source text (for ident/number/string); ranges carry their parsed payload instead. */
|
|
35
35
|
value: string;
|
|
36
|
-
range?: {
|
|
36
|
+
range?: {
|
|
37
|
+
min: number;
|
|
38
|
+
max: number;
|
|
39
|
+
/** `/` = stepped (additive), `*` = multiplied (geometric). */
|
|
40
|
+
mode: '/' | '*';
|
|
41
|
+
step: number;
|
|
42
|
+
quantum?: number;
|
|
43
|
+
/** Pad width inferred from a leading zero on a written endpoint (`025` → 3). */
|
|
44
|
+
pad?: number;
|
|
45
|
+
};
|
|
37
46
|
line: number;
|
|
38
47
|
column: number;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
const IDENT = /^[A-Za-z][A-Za-z0-9-]*/;
|
|
42
|
-
const RANGE = /^(\d+)-(\d+)
|
|
51
|
+
const RANGE = /^(\d+)-(\d+)([*/])(\d+(?:\.\d+)?)(?:~(\d+))?/;
|
|
43
52
|
const NUMBER = /^\d+/;
|
|
44
53
|
|
|
54
|
+
/** "Write the numbers as they appear": a leading zero declares the pad width. */
|
|
55
|
+
function padWidth(...literals: string[]): number | undefined {
|
|
56
|
+
const declared = literals.filter((l) => l.length > 1 && l.startsWith('0'));
|
|
57
|
+
if (declared.length === 0) return undefined;
|
|
58
|
+
return Math.max(...declared.map((l) => l.length));
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
export function tokenize(source: string): Token[] {
|
|
46
62
|
const tokens: Token[] = [];
|
|
47
63
|
const lines = source.split('\n');
|
|
@@ -85,7 +101,10 @@ export function tokenize(source: string): Token[] {
|
|
|
85
101
|
range: {
|
|
86
102
|
min: Number(range[1]),
|
|
87
103
|
max: Number(range[2]),
|
|
88
|
-
|
|
104
|
+
mode: range[3] as '/' | '*',
|
|
105
|
+
step: Number(range[4]),
|
|
106
|
+
quantum: range[5] ? Number(range[5]) : undefined,
|
|
107
|
+
pad: padWidth(range[1], range[2]),
|
|
89
108
|
},
|
|
90
109
|
line,
|
|
91
110
|
column,
|