@vertekum/schema-builder 0.1.4 → 0.1.5
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 +6 -0
- package/README.md +37 -89
- package/docs/build.md +81 -0
- package/docs/emission.md +129 -0
- package/docs/expressions.md +150 -0
- package/docs/language.md +95 -0
- package/docs/lint.md +59 -0
- package/docs/modules.md +105 -0
- package/docs/scales.md +88 -0
- package/package.json +2 -1
- package/src/api.ts +11 -4
- package/src/ast.ts +13 -4
- package/src/build.ts +58 -20
- package/src/cli.ts +51 -1
- package/src/error.ts +8 -2
- package/src/lint.ts +80 -0
- package/src/parser.ts +24 -3
- package/src/resolve.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @vertekum/schema-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`ecb2e95`](https://github.com/moros-oxus/vertekum/commit/ecb2e95fa860fa1aed780b29222aa69d474d68fe) Thanks [@tschemmer](https://github.com/tschemmer)! - Adds `vertekum schema lint`: validate `.dfn` modules — fragments and unused productions included — with findings collected and positioned per file. Grammar errors now point at the offending source (file, line, column) instead of `1:1`, and a misplaced `*` explains where the open-set mark belongs.
|
|
8
|
+
|
|
3
9
|
## 0.1.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,13 +18,13 @@ import { defineConfig } from '@vertekum/core';
|
|
|
18
18
|
import { schemaBuilderExtension } from '@vertekum/schema-builder';
|
|
19
19
|
|
|
20
20
|
export default defineConfig({
|
|
21
|
-
collection: './tokens',
|
|
22
21
|
extensions: [schemaBuilderExtension],
|
|
23
|
-
|
|
22
|
+
// the BUILT schema (step 3) validating a token set from the collection
|
|
23
|
+
schemas: [{ from: './schemas', use: { 'color-schema.json': 'core-tokens.json' } }],
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
2. Declare the vocabulary — `schemas/
|
|
27
|
+
2. Declare the vocabulary — `schemas/color-schema.dfn`:
|
|
28
28
|
|
|
29
29
|
```dfn
|
|
30
30
|
emphasis = subtle | bold
|
|
@@ -35,96 +35,44 @@ root = color.text.[neutral | brand | success].<emphasis>
|
|
|
35
35
|
3. Build it, then check as usual:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx vertekum schema build # writes schemas/
|
|
38
|
+
npx vertekum schema build # writes schemas/color-schema.json beside the module
|
|
39
39
|
npx vertekum check # the built schema now governs your token names
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
(
|
|
45
|
-
|
|
46
|
-
while naming one explicitly is an error.
|
|
47
|
-
|
|
48
|
-
## The definition language
|
|
49
|
-
|
|
50
|
-
A module is line-oriented: `#` comments, pragmas, `use` imports, and productions. An
|
|
51
|
-
indented line continues the statement above it.
|
|
52
|
-
|
|
53
|
-
### Statements
|
|
54
|
-
|
|
55
|
-
| statement | meaning |
|
|
56
|
-
| --- | --- |
|
|
57
|
-
| `name = expression` | a **production**: a named fragment (a denotation, a branch, a whole subtree) |
|
|
58
|
-
| `root = expression` | the reserved production `build` materializes; one per module |
|
|
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 |
|
|
61
|
-
| `id "…"` `title "…"` `description "…"` | pragmas: the built schema's `$id`, `title`, `description` |
|
|
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) |
|
|
63
|
-
|
|
64
|
-
### Expressions
|
|
65
|
-
|
|
66
|
-
| syntax | meaning |
|
|
67
|
-
| --- | --- |
|
|
68
|
-
| `a.b.c` | nesting — one name-tree level per step |
|
|
69
|
-
| `a \| b` | alternation — the set of permitted names |
|
|
70
|
-
| `[ … ]` | grouping; groups may hold full sub-paths (branches) |
|
|
71
|
-
| `<name>` | reference a local production |
|
|
72
|
-
| `<@name>` | reference an imported production; an imported module's *root* goes by its basename |
|
|
73
|
-
| `<@module/name>` | qualified: that import's production alone — the resolver when two imports share a name |
|
|
74
|
-
| `<name [a, b]>` | **pick** — only the listed members of the set |
|
|
75
|
-
| `<name ![a, b]>` | **omit** — the set minus the listed members |
|
|
76
|
-
| `<name*>` / `[a \| b *]` | **open set** — additions beyond the listed names are permitted, and every member (listed or added) takes the same tail |
|
|
77
|
-
| `step?` | **optional slot** — the step may be skipped: `<role>.<emphasis>?.<state>?` grants `role`, `role.<emphasis>`, `role.<state>`, and the full path |
|
|
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 |
|
|
80
|
-
|
|
81
|
-
Pick and omit validate every listed name against the set's members — a typo is a build
|
|
82
|
-
error, and a modified set is a new set. Naming one derives a narrowed denotation:
|
|
42
|
+
The grammar reads the way a design system talks: nesting is `.`, choice is `|`,
|
|
43
|
+
denotations are named productions referenced as `<emphasis>`, scales are enumerated
|
|
44
|
+
expressions (`100-900/100`, `16-64*1.25~4`), and `?` marks a slot as optional. One
|
|
45
|
+
expression grants a whole syntagm:
|
|
83
46
|
|
|
84
47
|
```dfn
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
limited-accents = <@accent-color [red, blue]>
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Style: the root reads as the syntagm
|
|
91
|
-
|
|
92
|
-
State the high-level shape in the root and let unused slots collapse; give forks their own
|
|
93
|
-
named productions instead of nesting:
|
|
94
|
-
|
|
95
|
-
```dfn
|
|
96
|
-
property = background | text | icon | border
|
|
97
|
-
role = brand | danger | neutral | success | warning
|
|
98
|
-
emphasis = subtle | bold
|
|
99
|
-
state = hovered | pressed
|
|
100
|
-
|
|
101
|
-
root = color.[
|
|
102
|
-
<property>.<role>?.<emphasis>?.<state>?
|
|
103
|
-
| <code>
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
code = text.code.[comments | keywords | strings]
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Built artifacts and ownership
|
|
110
|
-
|
|
111
|
-
A built file lands beside its module (`house.dfn` → `house.json`) and carries a `$comment`
|
|
112
|
-
stamp naming its source. The stamp is the ownership contract: `schema build` regenerates
|
|
113
|
-
stamped files and **never overwrites a file whose stamp was removed** — hand-edit the JSON
|
|
114
|
-
and delete the stamp to take ownership, and the builder leaves it alone from then on.
|
|
115
|
-
|
|
116
|
-
## Ejecting grammar
|
|
117
|
-
|
|
118
|
-
`vertekum schema eject` copies any resolvable file verbatim — including `.dfn` modules
|
|
119
|
-
from packages that ship them:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
npx vertekum schema eject @vertekum/schema-atlassian/color.dfn ./schemas/color.dfn
|
|
123
|
-
npx vertekum schema build
|
|
48
|
+
root = color.<property>.<role>?.<emphasis>?.<state>?
|
|
124
49
|
```
|
|
125
50
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
- [The module](./docs/language.md) — statements, pragmas (`id`, `title`, `description`,
|
|
54
|
+
`scope`), comments and continuation, `root` and fragments.
|
|
55
|
+
- [Expressions](./docs/expressions.md) — nesting, alternation, groups, references,
|
|
56
|
+
pick/omit, open sets, optional slots; the style that keeps a root readable.
|
|
57
|
+
- [Scales](./docs/scales.md) — additive and geometric ranges, zero-padding,
|
|
58
|
+
quantization, the locked semantics.
|
|
59
|
+
- [Modules and composition](./docs/modules.md) — `use` and aliasing, fragments,
|
|
60
|
+
aggregate roots, nested directories, shipping and ejecting grammar.
|
|
61
|
+
- [`schema build`](./docs/build.md) — the command, the sweep, the ownership stamp, the
|
|
62
|
+
`--check` CI gate, programmatic use.
|
|
63
|
+
- [`schema lint`](./docs/lint.md) — validate the `.dfn` sources themselves: fragments
|
|
64
|
+
included, every production, findings collected with positions.
|
|
65
|
+
- [What a build emits](./docs/emission.md) — the names-and-order schema shape, open
|
|
66
|
+
positions, `$defs`, scope; a complete dfn→JSON example.
|
|
67
|
+
|
|
68
|
+
## Ownership in one paragraph
|
|
69
|
+
|
|
70
|
+
A built file lands beside its module (`color.dfn` → `color.json`) and carries a
|
|
71
|
+
`$comment` stamp naming its source. `schema build` regenerates stamped files and
|
|
72
|
+
**never overwrites a file whose stamp was removed** — deleting the stamp line is how
|
|
73
|
+
you take ownership of the JSON and part ways with the grammar. Until then, the `.dfn`
|
|
74
|
+
is the source of truth and `--check` keeps CI honest about rebuilds.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
Apache-2.0
|
package/docs/build.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `vertekum schema build`
|
|
2
|
+
|
|
3
|
+
Build `.dfn` modules into the JSON Schema files the `schemas` config binds. The command
|
|
4
|
+
is contributed by this extension — it exists once `schemaBuilderExtension` is in the
|
|
5
|
+
project's config, and appears in `vertekum --help` and `vertekum describe` from then on.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# vertekum schema build [module] [--check] [--dry-run] [--json] [--cwd <dir>]
|
|
9
|
+
vertekum schema build # every module under ./schemas
|
|
10
|
+
vertekum schema build schemas/color.dfn # one module
|
|
11
|
+
vertekum schema build --check # CI: verify nothing is stale
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Argument / flag | What it does |
|
|
15
|
+
| --------------- | ----------------------------------------------------------------------------- |
|
|
16
|
+
| `[module]` | A `.dfn` file, relative to the working directory. Default: the sweep (below). |
|
|
17
|
+
| `--check` | Verify the built files on disk are current; write nothing. |
|
|
18
|
+
| `--dry-run` | Report what would be written without writing — owned by the runner, like every contributed command (see `@vertekum/cli`'s documentation). |
|
|
19
|
+
| `--json` | Machine-readable result (`files`, plus `skipped` and `fragments` under `data`). |
|
|
20
|
+
| `--cwd <dir>` | Project discovery starts here. |
|
|
21
|
+
|
|
22
|
+
## The sweep
|
|
23
|
+
|
|
24
|
+
With no argument, every `.dfn` under the project's `schemas/` directory builds —
|
|
25
|
+
recursively, so nested module directories are included (`node_modules` is not).
|
|
26
|
+
Modules without a `root` are **fragments**: the sweep skips them with a notice, while
|
|
27
|
+
naming one explicitly is an error — see [modules](./modules.md#fragments).
|
|
28
|
+
|
|
29
|
+
## Artifacts
|
|
30
|
+
|
|
31
|
+
Each module builds to `<module-name>.json` **beside its own file** —
|
|
32
|
+
`schemas/color.dfn` → `schemas/color.json`, `schemas/palette/color.dfn` →
|
|
33
|
+
`schemas/palette/color.json`. What the file contains is [emission](./emission.md).
|
|
34
|
+
|
|
35
|
+
Writing goes through the CLI runner: `--dry-run` lists the same files and writes
|
|
36
|
+
nothing, and an artifact path can never escape the working directory.
|
|
37
|
+
|
|
38
|
+
## Ownership: the stamp
|
|
39
|
+
|
|
40
|
+
Every built file carries a `$comment` stamp naming its source module:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
built by @vertekum/schema-builder from schemas/color.dfn — do not hand-edit; remove this comment to take ownership
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The stamp is the contract:
|
|
47
|
+
|
|
48
|
+
- A **stamped** file is the builder's — regenerated on every build.
|
|
49
|
+
- A **stampless** file at a build target has been taken over: hand-edit the JSON and
|
|
50
|
+
delete the stamp line, and the builder skips it from then on — reported
|
|
51
|
+
(`… has local edits (no stamp) — left as is`), never overwritten silently.
|
|
52
|
+
|
|
53
|
+
## `--check`: the staleness gate
|
|
54
|
+
|
|
55
|
+
`--check` rebuilds in memory and compares against disk, writing nothing. Any
|
|
56
|
+
difference is an error (exit `1`) naming the stale files:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
stale built schemas: schemas/color.json — run `vertekum schema build`
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This is the CI guard: run it wherever the built schemas are committed, and a grammar
|
|
63
|
+
edit that forgot its rebuild cannot land.
|
|
64
|
+
|
|
65
|
+
`--check` asks "are the artifacts **current**?" — the complementary question, "is the
|
|
66
|
+
grammar **sound**?" (including fragments a build never reaches), belongs to
|
|
67
|
+
[`schema lint`](./lint.md); CI wants both.
|
|
68
|
+
|
|
69
|
+
## Programmatic use
|
|
70
|
+
|
|
71
|
+
The pipeline is importable from `@vertekum/schema-builder/api` for tooling that wants
|
|
72
|
+
the pieces directly:
|
|
73
|
+
|
|
74
|
+
| Export | Does |
|
|
75
|
+
| -------------------------- | ----------------------------------------------------------------- |
|
|
76
|
+
| `parse(source)` | `.dfn` text → the module AST. |
|
|
77
|
+
| `resolveModule(path)` | Parse a file and, recursively, everything it `use`s. |
|
|
78
|
+
| `build(resolved)` | Expand a resolved module's root into its name tree. |
|
|
79
|
+
| `emit(tree, options)` | Name tree → the JSON Schema document text. |
|
|
80
|
+
| `buildModule(path, label?)`| The whole pipeline: path in, `{ target, content }` out. |
|
|
81
|
+
| `isStamped(content)` / `stamp(moduleFile)` | The ownership-stamp helpers. |
|
package/docs/emission.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# What a build emits
|
|
2
|
+
|
|
3
|
+
A built schema is ordinary JSON Schema (2020-12), in the **names-and-order** shape
|
|
4
|
+
hand-written vocabulary schemas use — so a generated file and a hand-written one are
|
|
5
|
+
interchangeable, and taking ownership of a generated file (removing the stamp) leaves
|
|
6
|
+
you with idiomatic schema, not machine soup.
|
|
7
|
+
|
|
8
|
+
## The position shape
|
|
9
|
+
|
|
10
|
+
Every position in the name tree becomes:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": { "…declared children…": {} },
|
|
16
|
+
"patternProperties": { "^\\$": true },
|
|
17
|
+
"unevaluatedProperties": false
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `properties` holds the granted names, one position schema each.
|
|
22
|
+
- `patternProperties: { "^\\$": true }` lets DTCG's structural members (`$value`,
|
|
23
|
+
`$type`, `$description`, …) pass at every level — the vocabulary governs **names**,
|
|
24
|
+
not the token format (the format schemas do that separately).
|
|
25
|
+
- `unevaluatedProperties: false` is the closure: a name outside the grant is a
|
|
26
|
+
violation, which is the entire point of a vocabulary schema.
|
|
27
|
+
|
|
28
|
+
## Open positions
|
|
29
|
+
|
|
30
|
+
A position whose set is [open](./expressions.md#open-sets) (`*`) replaces the closure
|
|
31
|
+
with a route for additions:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": { "…the listed members…": {} },
|
|
37
|
+
"patternProperties": { "^\\$": true },
|
|
38
|
+
"additionalProperties": { "…the members' shared tail…": {} }
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Additions join the set: they take the same tail every listed member has (which is
|
|
43
|
+
well-defined precisely because `*` is restricted to name-only sets).
|
|
44
|
+
|
|
45
|
+
## Denotations become `$defs`
|
|
46
|
+
|
|
47
|
+
A terminal denotation referenced from **two or more** positions is emitted once under
|
|
48
|
+
`$defs` and `$ref`'d — matching how a hand author avoids repeating themselves. A
|
|
49
|
+
single-use denotation is inlined. A [picked or omitted](./expressions.md#pick-and-omit)
|
|
50
|
+
set never shares the source's `$def`: a modified set is a different set.
|
|
51
|
+
|
|
52
|
+
## Document keys
|
|
53
|
+
|
|
54
|
+
| Key | From |
|
|
55
|
+
| ----------------------- | ----------------------------------------------------------------- |
|
|
56
|
+
| `$schema` | always `https://json-schema.org/draft/2020-12/schema` |
|
|
57
|
+
| `$id` | the [`id` pragma](./language.md#pragmas), when declared |
|
|
58
|
+
| `$comment` | the provenance stamp — the [ownership contract](./build.md#ownership-the-stamp) |
|
|
59
|
+
| `title`, `description` | their pragmas, when declared |
|
|
60
|
+
| `$defs` | shared denotations, when any |
|
|
61
|
+
| the root position | `type`, `properties`, `patternProperties`, and the seal |
|
|
62
|
+
|
|
63
|
+
## Scope
|
|
64
|
+
|
|
65
|
+
The [`scope` pragma](./language.md#pragmas) decides the **root** seal only:
|
|
66
|
+
|
|
67
|
+
- `"document"` (default): the root carries `unevaluatedProperties: false` — nothing
|
|
68
|
+
beyond the granted top-level names may exist in a bound file.
|
|
69
|
+
- `"branch"`: the root omits it — the schema governs its own branches and ignores
|
|
70
|
+
siblings, so several per-aspect schemas can bind over one collection. Every level
|
|
71
|
+
*below* the root stays sealed either way.
|
|
72
|
+
|
|
73
|
+
## A complete example
|
|
74
|
+
|
|
75
|
+
`schemas/color.dfn`:
|
|
76
|
+
|
|
77
|
+
```dfn
|
|
78
|
+
emphasis = subtle | bold
|
|
79
|
+
|
|
80
|
+
root = color.[text | icon].<emphasis>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
builds to `schemas/color.json`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
88
|
+
"$comment": "built by @vertekum/schema-builder from schemas/color.dfn — do not hand-edit; remove this comment to take ownership",
|
|
89
|
+
"$defs": {
|
|
90
|
+
"emphasis": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"subtle": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {},
|
|
96
|
+
"patternProperties": { "^\\$": true },
|
|
97
|
+
"unevaluatedProperties": false
|
|
98
|
+
},
|
|
99
|
+
"bold": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {},
|
|
102
|
+
"patternProperties": { "^\\$": true },
|
|
103
|
+
"unevaluatedProperties": false
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"patternProperties": { "^\\$": true },
|
|
107
|
+
"unevaluatedProperties": false
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"color": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"text": { "$ref": "#/$defs/emphasis" },
|
|
116
|
+
"icon": { "$ref": "#/$defs/emphasis" }
|
|
117
|
+
},
|
|
118
|
+
"patternProperties": { "^\\$": true },
|
|
119
|
+
"unevaluatedProperties": false
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"patternProperties": { "^\\$": true },
|
|
123
|
+
"unevaluatedProperties": false
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`<emphasis>` sits at two terminal positions (`color.text`, `color.icon`), so it became
|
|
128
|
+
a shared `$def`. A token file bound to this schema may hold `color.text.subtle` with
|
|
129
|
+
any `$`-members along the way — and nothing named outside the grant.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Expressions
|
|
2
|
+
|
|
3
|
+
An expression describes a **name tree**: which token names may exist beneath a
|
|
4
|
+
position, and in what order the nesting proceeds. Everything to the right of a
|
|
5
|
+
production's `=` is one of these forms, freely composed.
|
|
6
|
+
|
|
7
|
+
| Syntax | Form |
|
|
8
|
+
| ----------------------- | ---------------------------------------------------------------- |
|
|
9
|
+
| [`a.b.c`](#nesting) | nesting — one tree level per step |
|
|
10
|
+
| [`a \| b`](#alternation) | alternation — the set of permitted names |
|
|
11
|
+
| [`[ … ]`](#groups) | grouping; groups may hold full sub-paths |
|
|
12
|
+
| [`<name>`](#references) | reference a local production |
|
|
13
|
+
| [`<@name>`, `<@module/name>`](#references) | reference an imported production |
|
|
14
|
+
| [`<name [a, b]>`](#pick-and-omit) | **pick** — only the listed members |
|
|
15
|
+
| [`<name ![a, b]>`](#pick-and-omit) | **omit** — the set minus the listed members |
|
|
16
|
+
| [`<name*>`, `[a \| b *]`](#open-sets) | **open set** — additions are permitted |
|
|
17
|
+
| [`step?`](#optional-slots) | **optional slot** — the step may be skipped |
|
|
18
|
+
| [`100-900/100`, `16-64*1.25~4`](./scales.md) | a numeric scale, enumerated at build — see [scales](./scales.md) |
|
|
19
|
+
|
|
20
|
+
## Nesting
|
|
21
|
+
|
|
22
|
+
`a.b.c` — each `.` step is one level of the name tree: the token path `a.b.c` and
|
|
23
|
+
nothing beside it.
|
|
24
|
+
|
|
25
|
+
```dfn
|
|
26
|
+
root = color.text.base
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
grants exactly one path: `color.text.base`.
|
|
30
|
+
|
|
31
|
+
## Alternation
|
|
32
|
+
|
|
33
|
+
`a | b | c` — the union: this position permits these names.
|
|
34
|
+
|
|
35
|
+
```dfn
|
|
36
|
+
role = brand | danger | neutral | success | warning
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Groups
|
|
40
|
+
|
|
41
|
+
`[ … ]` groups an expression, usually to alternate inside a path:
|
|
42
|
+
|
|
43
|
+
```dfn
|
|
44
|
+
root = color.[text | icon].base
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
grants `color.text.base` and `color.icon.base`.
|
|
48
|
+
|
|
49
|
+
A group's options may be full **sub-paths**, which is how one root states branches with
|
|
50
|
+
different shapes:
|
|
51
|
+
|
|
52
|
+
```dfn
|
|
53
|
+
root = color.[
|
|
54
|
+
<property>.<role>
|
|
55
|
+
| text.code.[comments | keywords | strings]
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## References
|
|
60
|
+
|
|
61
|
+
| Form | Resolves to |
|
|
62
|
+
| ------------------- | ------------------------------------------------------------------------------ |
|
|
63
|
+
| `<name>` | the local production `name`. |
|
|
64
|
+
| `<@name>` | an imported production: searched across every import; an imported module's **root** goes by the module's key (its basename, or its `use … as` alias). |
|
|
65
|
+
| `<@module/name>` | qualified — that import's production alone. |
|
|
66
|
+
|
|
67
|
+
An unqualified `<@name>` that two imports could satisfy is an error naming the fix:
|
|
68
|
+
qualify it as `<@module/name>`. The import model — keys, aliasing, fragments — is
|
|
69
|
+
[modules](./modules.md).
|
|
70
|
+
|
|
71
|
+
## Pick and omit
|
|
72
|
+
|
|
73
|
+
A reference may narrow the set it names: `[a, b]` **picks** only the listed members,
|
|
74
|
+
`![a, b]` **omits** them. Every listed member is validated against the set — a typo is
|
|
75
|
+
a build error (`'<scale>' has no member '600' to omit`), and numbers are valid members:
|
|
76
|
+
|
|
77
|
+
```dfn
|
|
78
|
+
use "./space.dfn"
|
|
79
|
+
|
|
80
|
+
root = space.[<scale> | negative.<scale ![0, 1000]>]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A modified set is a **new** set: it never shares the source denotation's `$def` in the
|
|
84
|
+
built schema — narrowing a set and changing the set are the same act. Naming the
|
|
85
|
+
narrowed form derives a new denotation:
|
|
86
|
+
|
|
87
|
+
```dfn
|
|
88
|
+
limited-accents = <@accent-color [red, blue]>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Open sets
|
|
92
|
+
|
|
93
|
+
`*` marks a position's set as **open**: names beyond the listed ones are permitted, and
|
|
94
|
+
every member — listed or added — takes the same tail.
|
|
95
|
+
|
|
96
|
+
```dfn
|
|
97
|
+
root = color.<color-role*>.value
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
grants `color.<any color-role, or any addition>.value`. The trailing `*` also works
|
|
101
|
+
inside a group: `[small | medium | large *]`.
|
|
102
|
+
|
|
103
|
+
`*` opens a set of **names**: the production (or group) it marks must be name-only —
|
|
104
|
+
names, scales, and alternations of them. Opening a set of sub-paths is a build error,
|
|
105
|
+
because an addition would have no well-defined shape to take. The shared tail is
|
|
106
|
+
exactly what makes additions meaningful; the emitted schema routes them accordingly
|
|
107
|
+
([emission](./emission.md#open-positions)).
|
|
108
|
+
|
|
109
|
+
## Optional slots
|
|
110
|
+
|
|
111
|
+
`?` marks a path step as skippable. The grant is the **slot-collapse lattice** — every
|
|
112
|
+
combination of present and skipped optional steps:
|
|
113
|
+
|
|
114
|
+
```dfn
|
|
115
|
+
root = <role>.<emphasis>?.<state>?
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
grants four shapes:
|
|
119
|
+
|
|
120
|
+
- `<role>`
|
|
121
|
+
- `<role>.<emphasis>`
|
|
122
|
+
- `<role>.<state>`
|
|
123
|
+
- `<role>.<emphasis>.<state>`
|
|
124
|
+
|
|
125
|
+
Order is preserved — `<state>` always follows `<emphasis>` when both appear; skipping
|
|
126
|
+
is the only freedom. This is how a vocabulary states "emphasis and state are optional
|
|
127
|
+
refinements" in one line instead of enumerating the lattice.
|
|
128
|
+
|
|
129
|
+
## Style: the root reads as the syntagm
|
|
130
|
+
|
|
131
|
+
State the high-level shape in the root and let unused slots collapse; give forks their
|
|
132
|
+
own named productions instead of nesting them inline:
|
|
133
|
+
|
|
134
|
+
```dfn
|
|
135
|
+
property = background | text | icon | border
|
|
136
|
+
role = brand | danger | neutral | success | warning
|
|
137
|
+
emphasis = subtle | bold
|
|
138
|
+
state = hovered | pressed
|
|
139
|
+
|
|
140
|
+
root = color.[
|
|
141
|
+
<property>.<role>?.<emphasis>?.<state>?
|
|
142
|
+
| <code>
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
code = text.code.[comments | keywords | strings]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The root is the sentence — a reader sees the whole grammar of the vocabulary at the top
|
|
149
|
+
of the file; the productions below are its glossary. Deep inline nesting states the
|
|
150
|
+
same tree while hiding its shape.
|
package/docs/language.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# The module
|
|
2
|
+
|
|
3
|
+
A `.dfn` file is a **module**: a line-oriented text file declaring a vocabulary. This
|
|
4
|
+
page is the module's structure — statements, pragmas, and imports; the expression forms
|
|
5
|
+
that appear to the right of `=` are [expressions](./expressions.md).
|
|
6
|
+
|
|
7
|
+
```dfn
|
|
8
|
+
# color.dfn — the colour vocabulary this project enforces
|
|
9
|
+
title "Colour vocabulary"
|
|
10
|
+
|
|
11
|
+
use "./denotations/emphasis.dfn"
|
|
12
|
+
|
|
13
|
+
role = neutral | brand | success
|
|
14
|
+
|
|
15
|
+
root = color.[text | icon].<role>.<@emphasis>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Lexical rules
|
|
19
|
+
|
|
20
|
+
- **Statements are lines.** One statement per line; a line that starts with whitespace
|
|
21
|
+
**continues** the statement above it, so a long expression wraps without any
|
|
22
|
+
continuation character:
|
|
23
|
+
|
|
24
|
+
```dfn
|
|
25
|
+
root = color.[
|
|
26
|
+
<property>.<role>?.<emphasis>?
|
|
27
|
+
| <code>
|
|
28
|
+
]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **`#` starts a comment**, running to the end of the line.
|
|
32
|
+
- **Identifiers** are `[A-Za-z][A-Za-z0-9-]*` — letters, digits, hyphens
|
|
33
|
+
(`color-role`). A term that starts with a digit is numeric: a bare number (`950`) or
|
|
34
|
+
a [scale expression](./scales.md) (`100-300/50`) — which is why hyphens inside names
|
|
35
|
+
never collide with range syntax.
|
|
36
|
+
- **Strings** (pragma and `use` arguments) are double-quoted.
|
|
37
|
+
|
|
38
|
+
## Statements
|
|
39
|
+
|
|
40
|
+
| Statement | What it is |
|
|
41
|
+
| ------------------------ | ----------------------------------------------------------------- |
|
|
42
|
+
| [`name = expression`](#productions) | A **production**: a named fragment of the vocabulary. |
|
|
43
|
+
| [`root = expression`](#root) | The reserved production a build materializes. |
|
|
44
|
+
| [`use "…" [as name]`](#use) | Import another module. |
|
|
45
|
+
| [`id` / `title` / `description` / `scope` `"…"`](#pragmas) | Document metadata pragmas. |
|
|
46
|
+
|
|
47
|
+
### Productions
|
|
48
|
+
|
|
49
|
+
`name = expression` binds a name to an expression — a denotation (a set of names), a
|
|
50
|
+
branch, or a whole subtree. Productions exist to be referenced (`<name>`) from other
|
|
51
|
+
productions or the root; an unreferenced production builds nothing on its own. A
|
|
52
|
+
production may not expand through itself — a reference cycle is a build error.
|
|
53
|
+
|
|
54
|
+
### `root`
|
|
55
|
+
|
|
56
|
+
`root = expression` is the reserved production the build materializes: the module's
|
|
57
|
+
whole name tree grows from it. One per module.
|
|
58
|
+
|
|
59
|
+
A module **without** a root is a **fragment** — a library of productions that exists to
|
|
60
|
+
be `use`d by other modules. The build sweep skips fragments (with a notice); naming one
|
|
61
|
+
explicitly as the build argument is an error, so a typo'd `root` cannot pass silently.
|
|
62
|
+
See [modules](./modules.md#fragments).
|
|
63
|
+
|
|
64
|
+
### `use`
|
|
65
|
+
|
|
66
|
+
`use "<specifier>"` imports another module; `as <name>` renames the import. Specifier
|
|
67
|
+
forms, import accessors, and collision handling are [modules](./modules.md).
|
|
68
|
+
|
|
69
|
+
### Pragmas
|
|
70
|
+
|
|
71
|
+
A pragma is an identifier followed by a string, each usable at most once per module:
|
|
72
|
+
|
|
73
|
+
| Pragma | Value | Lands in the built schema as |
|
|
74
|
+
| ------------- | ---------------------------- | ----------------------------------------- |
|
|
75
|
+
| `id` | any string (usually a URI) | `$id` |
|
|
76
|
+
| `title` | any string | `title` |
|
|
77
|
+
| `description` | any string | `description` |
|
|
78
|
+
| `scope` | `"document"` \| `"branch"` | whether the document root is sealed |
|
|
79
|
+
|
|
80
|
+
`scope` is the one with semantics, and exactly two values:
|
|
81
|
+
|
|
82
|
+
| Value | Meaning |
|
|
83
|
+
| ------------ | ---------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `"document"` | Default. The schema **seals** the document root: nothing beyond the granted names may exist. |
|
|
85
|
+
| `"branch"` | The schema governs only the top-level branches it names — the root stays **unsealed**, so sibling vocabularies (a colour module, a spacing module) can each bind over the same token files without rejecting each other's branches. |
|
|
86
|
+
|
|
87
|
+
A single whole-vocabulary schema wants the default; per-aspect schemas that co-validate
|
|
88
|
+
one collection want `"branch"`. The exact effect on the emitted JSON is
|
|
89
|
+
[emission](./emission.md#scope).
|
|
90
|
+
|
|
91
|
+
## Editor support
|
|
92
|
+
|
|
93
|
+
The package ships a TextMate grammar and language configuration for `.dfn` under
|
|
94
|
+
`grammar/` — editors and tooling that consume TextMate grammars can pick them up from
|
|
95
|
+
there.
|
package/docs/lint.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# `vertekum schema lint`
|
|
2
|
+
|
|
3
|
+
Validate the `.dfn` sources themselves — fragments included, every production evaluated —
|
|
4
|
+
without building anything.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
# vertekum schema lint [module] [--json] [--cwd <dir>]
|
|
8
|
+
vertekum schema lint # every module under ./schemas
|
|
9
|
+
vertekum schema lint schemas/color.dfn # one module — fragment or rooted
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Argument / flag | What it does |
|
|
13
|
+
| --------------- | ----------------------------------------------------------------------------- |
|
|
14
|
+
| `[module]` | A `.dfn` file, relative to the working directory. Default: every module under `./schemas`, recursively — **fragments included**. |
|
|
15
|
+
| `--json` | Machine-readable result. |
|
|
16
|
+
| `--cwd <dir>` | Project discovery starts here. |
|
|
17
|
+
|
|
18
|
+
Exit `0` when the grammar is sound, `1` with findings — one per line, positioned:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
2 problem(s) in the .dfn modules:
|
|
22
|
+
schemas/emphasis.dfn:1:21 unknown production '<missing>'
|
|
23
|
+
schemas/scale.dfn:1:14 '*' marks a set open and sits inside the reference or group it opens — <name*> or [a | b *]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What lint checks that a build does not
|
|
27
|
+
|
|
28
|
+
A build expands from `root`, so its validation reaches exactly what a root reaches.
|
|
29
|
+
Lint's contract is the source:
|
|
30
|
+
|
|
31
|
+
- **Fragments** — a module with no `root` never builds, but lint evaluates its
|
|
32
|
+
productions, so a broken denotation is caught in the file that declares it, not
|
|
33
|
+
months later in the first module that imports it.
|
|
34
|
+
- **Unused productions** — a rooted module's production that nothing references yet is
|
|
35
|
+
still checked.
|
|
36
|
+
- **Everything a build would refuse** — unknown or ambiguous references, pick/omit
|
|
37
|
+
members that don't exist, scale collisions, `*` on a non-name set, reference cycles —
|
|
38
|
+
reported the same way, at the same positions.
|
|
39
|
+
|
|
40
|
+
Findings **collect**: one broken production does not hide the next, so a module with
|
|
41
|
+
three mistakes is one lint run, not three.
|
|
42
|
+
|
|
43
|
+
## Sound vs. current
|
|
44
|
+
|
|
45
|
+
`lint` and [`build --check`](./build.md#--check-the-staleness-gate) answer different
|
|
46
|
+
questions:
|
|
47
|
+
|
|
48
|
+
| Verb | Question | Covers |
|
|
49
|
+
| --------------- | --------------------------- | ------------------------------------------ |
|
|
50
|
+
| `schema lint` | Is the grammar sound? | Every `.dfn`, fragments and all; no artifacts touched. |
|
|
51
|
+
| `schema build --check` | Are the artifacts current? | Rooted modules' built `.json` vs. disk. |
|
|
52
|
+
|
|
53
|
+
CI wants both — lint fails a broken fragment the staleness gate cannot see, and the
|
|
54
|
+
staleness gate fails a sound grammar whose rebuild was forgotten.
|
|
55
|
+
|
|
56
|
+
## Programmatic use
|
|
57
|
+
|
|
58
|
+
`lintModule(path)` (from `@vertekum/schema-builder/api`) returns the findings as
|
|
59
|
+
`{ file, line, column, message }` records — an empty array means sound.
|