reladraw 0.1.0 → 0.2.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/README.md +25 -23
- package/SYNTAX.md +137 -54
- package/dist/ast.d.ts +73 -5
- package/dist/ast.js +99 -6
- package/dist/constants.d.ts +8 -5
- package/dist/constants.js +14 -12
- package/dist/grammar.d.ts +7 -7
- package/dist/grammar.js +9 -9
- package/dist/icons.d.ts +3 -3
- package/dist/icons.js +2 -2
- package/dist/lexer.d.ts +1 -1
- package/dist/lexer.js +1 -1
- package/dist/parser.js +47 -6
- package/dist/render.js +97 -81
- package/dist/resolve.js +144 -26
- package/package.json +1 -1
package/dist/ast.js
CHANGED
|
@@ -16,13 +16,13 @@ export function isDirection(word) {
|
|
|
16
16
|
return DIRECTIONS.includes(word);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Which edge of the target a `level with` shares. `
|
|
19
|
+
* Which edge of the target a `level with` shares. `center` is the plain form;
|
|
20
20
|
* the rest are written in front of it, as in `top level with media`.
|
|
21
21
|
*/
|
|
22
|
-
export const EDGES = ['
|
|
22
|
+
export const EDGES = ['center', 'top', 'bottom', 'left', 'right'];
|
|
23
23
|
/** An edge belongs to one axis, so an alignment never has to say which. */
|
|
24
24
|
export const EDGE_AXIS = {
|
|
25
|
-
|
|
25
|
+
center: 'y',
|
|
26
26
|
top: 'y',
|
|
27
27
|
bottom: 'y',
|
|
28
28
|
left: 'x',
|
|
@@ -30,7 +30,7 @@ export const EDGE_AXIS = {
|
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* The modifiers a placement understands, in brackets after its targets. Refused
|
|
33
|
-
* by name when
|
|
33
|
+
* by name when unrecognized, for the reason `DIAGRAM_KEYS` are: a modifier that
|
|
34
34
|
* silently does nothing looks like a bug in the tool rather than a typo.
|
|
35
35
|
*/
|
|
36
36
|
export const PLACEMENT_KEYS = ['gap'];
|
|
@@ -38,7 +38,7 @@ export const PLACEMENT_KEYS = ['gap'];
|
|
|
38
38
|
export function describePlacement(placement) {
|
|
39
39
|
const targets = listTargets(placement.targets);
|
|
40
40
|
if (placement.kind === 'align') {
|
|
41
|
-
const edge = placement.edge === '
|
|
41
|
+
const edge = placement.edge === 'center' ? '' : `${placement.edge} `;
|
|
42
42
|
return `${edge}level with ${targets}`;
|
|
43
43
|
}
|
|
44
44
|
// "left of X" and "above X" are both good English; "above of X" is not.
|
|
@@ -65,7 +65,7 @@ export function describeAxis(axis) {
|
|
|
65
65
|
return axis === 'y' ? 'vertically' : 'horizontally';
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* What a label's brackets may say: `"Docker" (at: bottom, align:
|
|
68
|
+
* What a label's brackets may say: `"Docker" (at: bottom, align: center)`.
|
|
69
69
|
*
|
|
70
70
|
* They are bracketed onto the label rather than written among the node's
|
|
71
71
|
* attributes for the same reason a gap is bracketed onto its placement — they
|
|
@@ -76,3 +76,96 @@ export function describeAxis(axis) {
|
|
|
76
76
|
export const LABEL_KEYS = ['at', 'align'];
|
|
77
77
|
/** The attributes a `diagram` statement understands. */
|
|
78
78
|
export const DIAGRAM_KEYS = ['background'];
|
|
79
|
+
/**
|
|
80
|
+
* The attributes whose value is a color rather than text. A color is written
|
|
81
|
+
* as the viewer will receive it and the renderer keeps no list of color words
|
|
82
|
+
* of its own, so there is nothing to check a value *against* — but quoting is
|
|
83
|
+
* the author saying "this is text", and an unquoted value cannot hold a space,
|
|
84
|
+
* so prose has to be quoted to get in at all. Refusing a quoted color is
|
|
85
|
+
* therefore the whole of what can be checked here, and it happens to be the
|
|
86
|
+
* mistake people actually make: `subtext: "medium-fine"` reads as the text
|
|
87
|
+
* that goes underneath, and was accepted and dropped in silence.
|
|
88
|
+
*/
|
|
89
|
+
export const COLOR_KEYS = [
|
|
90
|
+
'fill',
|
|
91
|
+
'border',
|
|
92
|
+
'text',
|
|
93
|
+
'line',
|
|
94
|
+
'subtext',
|
|
95
|
+
'background',
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* A color attribute names the *part* it colors, and a part exists only on the
|
|
99
|
+
* kinds that have one. A box has a border and text; a note and a glyph body are
|
|
100
|
+
* text and nothing else; a link is a line and its label.
|
|
101
|
+
*
|
|
102
|
+
* This table is what makes the words checkable. `border:` on a note is refused
|
|
103
|
+
* by name rather than ignored — the same rule as an unknown `diagram` key, and
|
|
104
|
+
* for the same reason: an attribute that silently does nothing looks like the
|
|
105
|
+
* tool being broken.
|
|
106
|
+
*
|
|
107
|
+
* A style spanning kinds writes one key per kind — `border: #d2904e line:
|
|
108
|
+
* #d2904e` — since a style contributes a part only to the kinds that have it.
|
|
109
|
+
* That is what replaced `stroke:`, which named no part and so could never be
|
|
110
|
+
* wrong, and which is why a box's text had no word of its own until now.
|
|
111
|
+
*/
|
|
112
|
+
export const COLOR_PARTS = {
|
|
113
|
+
box: ['fill', 'border', 'text', 'subtext'],
|
|
114
|
+
note: ['text'],
|
|
115
|
+
glyph: ['text', 'subtext'],
|
|
116
|
+
link: ['line', 'text'],
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Every attribute each kind understands. An attribute a kind has no use for is
|
|
120
|
+
* refused by name rather than dropped, the same rule as an unknown `diagram`
|
|
121
|
+
* key, a `PLACEMENT_KEYS` modifier or a color part — and for the same reason,
|
|
122
|
+
* which the color parts only closed one level down: a key that silently does
|
|
123
|
+
* nothing looks like the tool being broken rather than like a typo.
|
|
124
|
+
*
|
|
125
|
+
* The color entries repeat `COLOR_PARTS` and must agree with it. They are
|
|
126
|
+
* written out rather than spliced in because this table is the answer to "what
|
|
127
|
+
* may I write here", and a reader of it should not have to assemble the list
|
|
128
|
+
* from two places.
|
|
129
|
+
*
|
|
130
|
+
* Three of the exclusions are the whole of what this table decides, and each is
|
|
131
|
+
* a place the old silence hid something:
|
|
132
|
+
*
|
|
133
|
+
* - A glyph takes no `icon:`. It is drawn *as* a picture and has no box for a
|
|
134
|
+
* second one to sit in; `sizeNode` returns before it would ever be read.
|
|
135
|
+
* - A glyph and a note take no `align:`, which widens a node's children, and
|
|
136
|
+
* neither may have any.
|
|
137
|
+
* - A link takes no `gap:` or `overlap:`. Those are about where a box sits, and
|
|
138
|
+
* a link is not placed — it joins two things that are.
|
|
139
|
+
*/
|
|
140
|
+
export const ATTR_KEYS = {
|
|
141
|
+
box: [
|
|
142
|
+
'style',
|
|
143
|
+
'size',
|
|
144
|
+
'gap',
|
|
145
|
+
'overlap',
|
|
146
|
+
'align',
|
|
147
|
+
'wrap',
|
|
148
|
+
'icon',
|
|
149
|
+
'shape',
|
|
150
|
+
'fill',
|
|
151
|
+
'border',
|
|
152
|
+
'text',
|
|
153
|
+
'subtext',
|
|
154
|
+
],
|
|
155
|
+
note: ['style', 'size', 'gap', 'overlap', 'wrap', 'text'],
|
|
156
|
+
glyph: ['style', 'size', 'gap', 'overlap', 'wrap', 'shape', 'text', 'subtext'],
|
|
157
|
+
link: ['style', 'size', 'from', 'to', 'line', 'text'],
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Every word that is an attribute *somewhere*, which is what separates a
|
|
161
|
+
* misspelling from a key written on the wrong kind of thing. The two deserve
|
|
162
|
+
* different errors: one has no remedy but the spelling, the other has a real
|
|
163
|
+
* meaning somewhere else in the file.
|
|
164
|
+
*
|
|
165
|
+
* `DIAGRAM_KEYS` is in here so that `background:` on a box is understood to be
|
|
166
|
+
* a real word in the wrong place — that mistake wants to be pointed at `fill:`,
|
|
167
|
+
* not told the word does not exist.
|
|
168
|
+
*/
|
|
169
|
+
export const ALL_ATTR_KEYS = [
|
|
170
|
+
...new Set([...Object.values(ATTR_KEYS).flat(), ...DIAGRAM_KEYS]),
|
|
171
|
+
];
|
package/dist/constants.d.ts
CHANGED
|
@@ -74,8 +74,8 @@ export declare const LABEL_CLEARANCE = 20;
|
|
|
74
74
|
* renderer, which spaces the lanes of a channel by it, so the two cannot
|
|
75
75
|
* disagree about how much room a label needs. The two ask different questions of
|
|
76
76
|
* it and both are right: the resolver measures *along* the run, so a link
|
|
77
|
-
*
|
|
78
|
-
* the channel, so a link
|
|
77
|
+
* traveling horizontally needs the label's width; the renderer measures *across*
|
|
78
|
+
* the channel, so a link traveling horizontally down one needs its height.
|
|
79
79
|
*/
|
|
80
80
|
export declare function labelExtent(label: string, appearance: Attrs, axis: 'x' | 'y', measurer: Measurer, fontSize: number, line: number): number;
|
|
81
81
|
/**
|
|
@@ -123,8 +123,11 @@ export declare const DEFAULT_MARGIN = 40;
|
|
|
123
123
|
* rather than process" and a reader decodes it, while "lower down" means only
|
|
124
124
|
* lower down.
|
|
125
125
|
*
|
|
126
|
-
* A leaf has no band
|
|
127
|
-
*
|
|
126
|
+
* A leaf has no band, so `at` says nothing about one and is refused there. But
|
|
127
|
+
* `align` is not about the band: a label of more than one line has lines of
|
|
128
|
+
* unequal length whatever kind of box it is in, and how those sit across each
|
|
129
|
+
* other is a real question anywhere. A leaf's default is centered rather than
|
|
130
|
+
* ranged left, which is why the fallback is a parameter.
|
|
128
131
|
*/
|
|
129
132
|
export declare const LABEL_ENDS: readonly ["top", "bottom"];
|
|
130
133
|
export type LabelEnd = (typeof LABEL_ENDS)[number];
|
|
@@ -139,4 +142,4 @@ export interface LabelStyle {
|
|
|
139
142
|
* contents away from the band, and the renderer, which draws into it, so the two
|
|
140
143
|
* cannot disagree about which end the band is at.
|
|
141
144
|
*/
|
|
142
|
-
export declare function labelStyleFor(label: Attrs, line: number): LabelStyle;
|
|
145
|
+
export declare function labelStyleFor(label: Attrs, line: number, fallbackAlign?: 'start' | 'middle'): LabelStyle;
|
package/dist/constants.js
CHANGED
|
@@ -79,8 +79,8 @@ export const LABEL_CLEARANCE = 20;
|
|
|
79
79
|
* renderer, which spaces the lanes of a channel by it, so the two cannot
|
|
80
80
|
* disagree about how much room a label needs. The two ask different questions of
|
|
81
81
|
* it and both are right: the resolver measures *along* the run, so a link
|
|
82
|
-
*
|
|
83
|
-
* the channel, so a link
|
|
82
|
+
* traveling horizontally needs the label's width; the renderer measures *across*
|
|
83
|
+
* the channel, so a link traveling horizontally down one needs its height.
|
|
84
84
|
*/
|
|
85
85
|
export function labelExtent(label, appearance, axis, measurer, fontSize, line) {
|
|
86
86
|
const size = fontSizeFor('link', appearance, fontSize, line);
|
|
@@ -121,7 +121,7 @@ export const TEXT_SIZES = {
|
|
|
121
121
|
* the diagram rather than being part of it, and at the size of a box label an
|
|
122
122
|
* aside reads as a statement — so `note` starts small and says so by being a
|
|
123
123
|
* note. This is a default and not a ceiling: `size:` overrides it, the same way
|
|
124
|
-
* `fill:` overrides the theme's
|
|
124
|
+
* `fill:` overrides the theme's color.
|
|
125
125
|
*/
|
|
126
126
|
const DEFAULT_TEXT_SIZE = { note: 'small' };
|
|
127
127
|
/**
|
|
@@ -151,18 +151,20 @@ export const DEFAULT_MARGIN = 40;
|
|
|
151
151
|
* rather than process" and a reader decodes it, while "lower down" means only
|
|
152
152
|
* lower down.
|
|
153
153
|
*
|
|
154
|
-
* A leaf has no band
|
|
155
|
-
*
|
|
154
|
+
* A leaf has no band, so `at` says nothing about one and is refused there. But
|
|
155
|
+
* `align` is not about the band: a label of more than one line has lines of
|
|
156
|
+
* unequal length whatever kind of box it is in, and how those sit across each
|
|
157
|
+
* other is a real question anywhere. A leaf's default is centered rather than
|
|
158
|
+
* ranged left, which is why the fallback is a parameter.
|
|
156
159
|
*/
|
|
157
160
|
export const LABEL_ENDS = ['top', 'bottom'];
|
|
158
161
|
/**
|
|
159
|
-
* Author's word to the SVG's.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
+
* Author's word to the SVG's. One spelling of each, per the rule that refuses
|
|
163
|
+
* synonyms for `box` and `link`: an alias is a variant a reader has to learn,
|
|
164
|
+
* and every document and example has to pick one of them anyway.
|
|
162
165
|
*/
|
|
163
166
|
const LABEL_ALIGNMENTS = {
|
|
164
167
|
left: 'start',
|
|
165
|
-
centre: 'middle',
|
|
166
168
|
center: 'middle',
|
|
167
169
|
right: 'end',
|
|
168
170
|
};
|
|
@@ -171,17 +173,17 @@ const LABEL_ALIGNMENTS = {
|
|
|
171
173
|
* contents away from the band, and the renderer, which draws into it, so the two
|
|
172
174
|
* cannot disagree about which end the band is at.
|
|
173
175
|
*/
|
|
174
|
-
export function labelStyleFor(label, line) {
|
|
176
|
+
export function labelStyleFor(label, line, fallbackAlign = 'start') {
|
|
175
177
|
const at = label['at'];
|
|
176
178
|
if (at !== undefined && !LABEL_ENDS.includes(at)) {
|
|
177
179
|
throw new SourceError(`a label's at takes ${LABEL_ENDS.join(' or ')}, not "${at}"`, line);
|
|
178
180
|
}
|
|
179
181
|
const align = label['align'];
|
|
180
182
|
if (align !== undefined && LABEL_ALIGNMENTS[align] === undefined) {
|
|
181
|
-
throw new SourceError(`a label's align takes left,
|
|
183
|
+
throw new SourceError(`a label's align takes left, center or right, not "${align}"`, line);
|
|
182
184
|
}
|
|
183
185
|
return {
|
|
184
186
|
at: at ?? 'top',
|
|
185
|
-
align: align === undefined ?
|
|
187
|
+
align: align === undefined ? fallbackAlign : LABEL_ALIGNMENTS[align],
|
|
186
188
|
};
|
|
187
189
|
}
|
package/dist/grammar.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The language's *lexical* vocabulary, and a scanner that classifies one line of
|
|
3
|
-
* source into
|
|
3
|
+
* source into colored spans.
|
|
4
4
|
*
|
|
5
5
|
* This is deliberately separate from `parser.ts`, and it is not a second parser.
|
|
6
6
|
* The parser answers "what does this file mean" and refuses anything it cannot
|
|
7
|
-
* answer for; a highlighter has to
|
|
7
|
+
* answer for; a highlighter has to color a half-typed line without complaint,
|
|
8
8
|
* so it answers only "what kind of word is this" and never fails. Every rule
|
|
9
9
|
* below is a regex applied to a single line, in priority order, with one bit of
|
|
10
10
|
* carried state (whether anything has been seen on the line yet, and whether the
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* closes on one, and there are no blocks. That is the property those editor
|
|
24
24
|
* formats need and the reason a `.reladraw` grammar is small in all of them.
|
|
25
25
|
*/
|
|
26
|
-
/** What a span of source is, for
|
|
26
|
+
/** What a span of source is, for coloring. */
|
|
27
27
|
export type TokenKind =
|
|
28
28
|
/** `// to the end of the line` */
|
|
29
29
|
'comment'
|
|
@@ -42,7 +42,7 @@ export type TokenKind =
|
|
|
42
42
|
/** The single word an attribute takes. */
|
|
43
43
|
| 'value'
|
|
44
44
|
/** `#14532d`, wherever it appears. */
|
|
45
|
-
| '
|
|
45
|
+
| 'color'
|
|
46
46
|
/** `(` and `)` around a placement's or a label's modifiers. */
|
|
47
47
|
| 'bracket'
|
|
48
48
|
/** Everything else: node names being referred to, and whitespace. */
|
|
@@ -56,14 +56,14 @@ export interface Span {
|
|
|
56
56
|
/**
|
|
57
57
|
* The words a statement may open with. `parseStatement` in `parser.ts` is the
|
|
58
58
|
* authority — its switch is what actually accepts them — and this list mirrors
|
|
59
|
-
* it. A word missing here is a word that draws in the plain
|
|
59
|
+
* it. A word missing here is a word that draws in the plain color, which is a
|
|
60
60
|
* dull page rather than a wrong one.
|
|
61
61
|
*/
|
|
62
62
|
export declare const STATEMENT_KEYWORDS: readonly ["box", "note", "link", "deck", "style", "diagram"];
|
|
63
63
|
/**
|
|
64
64
|
* Every word that says something about where a thing goes. Assembled from the
|
|
65
65
|
* lists the parser itself reads, so a direction or a passage axis added there
|
|
66
|
-
*
|
|
66
|
+
* colors here without anybody remembering to come back.
|
|
67
67
|
*/
|
|
68
68
|
export declare const RELATION_WORDS: string[];
|
|
69
69
|
/**
|
|
@@ -80,7 +80,7 @@ export declare const PATTERNS: {
|
|
|
80
80
|
readonly keyword: `(?:${string})\\b`;
|
|
81
81
|
readonly arrow: "<->|->|<-";
|
|
82
82
|
readonly attribute: "[A-Za-z][A-Za-z0-9_-]*:";
|
|
83
|
-
readonly
|
|
83
|
+
readonly color: "#[0-9A-Fa-f]{3,8}\\b";
|
|
84
84
|
readonly relation: `(?:${string})\\b`;
|
|
85
85
|
readonly bracket: "[()]";
|
|
86
86
|
readonly word: "(?:[^\\s()\"\\/]|\\/(?!\\/))(?:[^\\s\"\\/]|\\/(?!\\/))*";
|
package/dist/grammar.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The language's *lexical* vocabulary, and a scanner that classifies one line of
|
|
3
|
-
* source into
|
|
3
|
+
* source into colored spans.
|
|
4
4
|
*
|
|
5
5
|
* This is deliberately separate from `parser.ts`, and it is not a second parser.
|
|
6
6
|
* The parser answers "what does this file mean" and refuses anything it cannot
|
|
7
|
-
* answer for; a highlighter has to
|
|
7
|
+
* answer for; a highlighter has to color a half-typed line without complaint,
|
|
8
8
|
* so it answers only "what kind of word is this" and never fails. Every rule
|
|
9
9
|
* below is a regex applied to a single line, in priority order, with one bit of
|
|
10
10
|
* carried state (whether anything has been seen on the line yet, and whether the
|
|
@@ -27,7 +27,7 @@ import { DIRECTIONS, EDGES, PASSAGE_AXES } from './ast.js';
|
|
|
27
27
|
/**
|
|
28
28
|
* The words a statement may open with. `parseStatement` in `parser.ts` is the
|
|
29
29
|
* authority — its switch is what actually accepts them — and this list mirrors
|
|
30
|
-
* it. A word missing here is a word that draws in the plain
|
|
30
|
+
* it. A word missing here is a word that draws in the plain color, which is a
|
|
31
31
|
* dull page rather than a wrong one.
|
|
32
32
|
*/
|
|
33
33
|
export const STATEMENT_KEYWORDS = ['box', 'note', 'link', 'deck', 'style', 'diagram'];
|
|
@@ -36,7 +36,7 @@ const DECLARES_NAME = ['box', 'note', 'deck', 'style'];
|
|
|
36
36
|
/**
|
|
37
37
|
* Every word that says something about where a thing goes. Assembled from the
|
|
38
38
|
* lists the parser itself reads, so a direction or a passage axis added there
|
|
39
|
-
*
|
|
39
|
+
* colors here without anybody remembering to come back.
|
|
40
40
|
*/
|
|
41
41
|
export const RELATION_WORDS = [
|
|
42
42
|
...DIRECTIONS,
|
|
@@ -72,7 +72,7 @@ export const PATTERNS = {
|
|
|
72
72
|
// `<->` first, or `<-` would match its opening half and leave a stray `>`.
|
|
73
73
|
arrow: '<->|->|<-',
|
|
74
74
|
attribute: '[A-Za-z][A-Za-z0-9_-]*:',
|
|
75
|
-
|
|
75
|
+
color: '#[0-9A-Fa-f]{3,8}\\b',
|
|
76
76
|
relation: `(?:${alternation(RELATION_WORDS)})\\b`,
|
|
77
77
|
bracket: '[()]',
|
|
78
78
|
// Matches what `tokenizeLine` treats as one bare token, and the awkwardness is
|
|
@@ -102,7 +102,7 @@ const RULES = [
|
|
|
102
102
|
// Before `relation`, because the trailing colon is what tells `left: …` from
|
|
103
103
|
// the `left` of a placement, and after `arrow` so `->` is never a word.
|
|
104
104
|
{ kind: 'attribute', re: sticky(PATTERNS.attribute) },
|
|
105
|
-
{ kind: '
|
|
105
|
+
{ kind: 'color', re: sticky(PATTERNS.color) },
|
|
106
106
|
{ kind: 'relation', re: sticky(PATTERNS.relation) },
|
|
107
107
|
];
|
|
108
108
|
const SPACE = sticky('[ \\t]+');
|
|
@@ -159,9 +159,9 @@ export function highlightLine(line) {
|
|
|
159
159
|
if (space !== null) {
|
|
160
160
|
push('plain', at + space.length);
|
|
161
161
|
const name = match(WORD, line, at);
|
|
162
|
-
// `style backup
|
|
162
|
+
// `style backup border: …` declares a name; `box fill: red` is a
|
|
163
163
|
// half-typed line whose second word is already an attribute, and
|
|
164
|
-
//
|
|
164
|
+
// coloring that as a name would be a lie about what it is.
|
|
165
165
|
if (name !== null && !name.endsWith(':'))
|
|
166
166
|
push('name', at + name.length);
|
|
167
167
|
}
|
|
@@ -194,7 +194,7 @@ export function highlightLine(line) {
|
|
|
194
194
|
// An attribute's value is whatever single token follows it, whatever it
|
|
195
195
|
// would otherwise have been called: `to: right` is a value, not a
|
|
196
196
|
// direction, and `style: wide` is a style name, not a gap.
|
|
197
|
-
const kind = expectingValue && rule.kind !== 'string' && rule.kind !== '
|
|
197
|
+
const kind = expectingValue && rule.kind !== 'string' && rule.kind !== 'color' && rule.kind !== 'bracket'
|
|
198
198
|
? 'value'
|
|
199
199
|
: rule.kind;
|
|
200
200
|
expectingValue = rule.kind === 'attribute';
|
package/dist/icons.d.ts
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* when a diagram asks for a distinction it cannot otherwise make.
|
|
20
20
|
*/
|
|
21
21
|
/**
|
|
22
|
-
* Icons carry three tones rather than
|
|
22
|
+
* Icons carry three tones rather than colors. `ink` is the drawn line, `shade`
|
|
23
23
|
* the body it encloses, and `void` is the page showing through — the same
|
|
24
|
-
* knockout a link label already uses. Naming tones instead of
|
|
24
|
+
* knockout a link label already uses. Naming tones instead of colors is what
|
|
25
25
|
* lets one glyph sit correctly on a dark theme and a light one.
|
|
26
26
|
*/
|
|
27
27
|
export type IconTone = 'ink' | 'shade' | 'void';
|
|
@@ -56,7 +56,7 @@ export declare const ICON_NAMES: string[];
|
|
|
56
56
|
*
|
|
57
57
|
* Named for what a node *is*, never for the geometry, which is the same rule the
|
|
58
58
|
* icon names follow: `document` and not `folded-corner`. A shape carrying a
|
|
59
|
-
* conventional meaning is a second channel alongside
|
|
59
|
+
* conventional meaning is a second channel alongside color, and a stronger one
|
|
60
60
|
* — a fill is whatever the author assigned and has to be learnt from the
|
|
61
61
|
* diagram, while a folded corner has meant "an artifact, not a process" in
|
|
62
62
|
* flowcharts for decades and reads with no legend at all.
|
package/dist/icons.js
CHANGED
|
@@ -26,7 +26,7 @@ export const ICON_STROKE = 1.1;
|
|
|
26
26
|
function circle(cx, cy, r) {
|
|
27
27
|
return `M${cx - r} ${cy} a${r} ${r} 0 1 0 ${r * 2} 0 a${r} ${r} 0 1 0 ${-r * 2} 0 Z`;
|
|
28
28
|
}
|
|
29
|
-
/** The three visible faces of an isometric cube, top face
|
|
29
|
+
/** The three visible faces of an isometric cube, top face centered on `cx, cy`. */
|
|
30
30
|
function cube(cx, cy, s) {
|
|
31
31
|
const half = s / 2;
|
|
32
32
|
return [
|
|
@@ -123,7 +123,7 @@ export const ICON_NAMES = Object.keys(ICONS);
|
|
|
123
123
|
*
|
|
124
124
|
* Named for what a node *is*, never for the geometry, which is the same rule the
|
|
125
125
|
* icon names follow: `document` and not `folded-corner`. A shape carrying a
|
|
126
|
-
* conventional meaning is a second channel alongside
|
|
126
|
+
* conventional meaning is a second channel alongside color, and a stronger one
|
|
127
127
|
* — a fill is whatever the author assigned and has to be learnt from the
|
|
128
128
|
* diagram, while a folded corner has meant "an artifact, not a process" in
|
|
129
129
|
* flowcharts for decades and reads with no legend at all.
|
package/dist/lexer.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface Token {
|
|
|
9
9
|
* runs to the end of the line, so a comment may trail a statement.
|
|
10
10
|
*
|
|
11
11
|
* A lone `/` is an ordinary character, which keeps a path or a ratio writable
|
|
12
|
-
* unquoted. `#` is ordinary too: it opens a hex
|
|
12
|
+
* unquoted. `#` is ordinary too: it opens a hex color, which is why comments
|
|
13
13
|
* are spelled `//` rather than the `#` an earlier version used.
|
|
14
14
|
*
|
|
15
15
|
* Parentheses group the modifiers on a placement — `left of hub (gap: wide)` —
|
package/dist/lexer.js
CHANGED
|
@@ -5,7 +5,7 @@ import { SourceError } from './errors.js';
|
|
|
5
5
|
* runs to the end of the line, so a comment may trail a statement.
|
|
6
6
|
*
|
|
7
7
|
* A lone `/` is an ordinary character, which keeps a path or a ratio writable
|
|
8
|
-
* unquoted. `#` is ordinary too: it opens a hex
|
|
8
|
+
* unquoted. `#` is ordinary too: it opens a hex color, which is why comments
|
|
9
9
|
* are spelled `//` rather than the `#` an earlier version used.
|
|
10
10
|
*
|
|
11
11
|
* Parentheses group the modifiers on a placement — `left of hub (gap: wide)` —
|
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DIAGRAM_KEYS, EDGE_AXIS, EDGES, PASSAGE_AXES, LABEL_KEYS, PLACEMENT_KEYS, isDirection, listTargets, } from './ast.js';
|
|
1
|
+
import { COLOR_KEYS, DIAGRAM_KEYS, EDGE_AXIS, EDGES, PASSAGE_AXES, LABEL_KEYS, PLACEMENT_KEYS, isDirection, listTargets, } from './ast.js';
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
3
|
import { isAttrKey, tokenizeLine } from './lexer.js';
|
|
4
4
|
/** Parse a whole source file. One statement per line; blanks and comments drop out. */
|
|
@@ -57,6 +57,10 @@ function attributesBegin(tokens) {
|
|
|
57
57
|
}
|
|
58
58
|
return -1;
|
|
59
59
|
}
|
|
60
|
+
/** The value as the author would have to write it back into a label. */
|
|
61
|
+
function quoteOf(text) {
|
|
62
|
+
return `"${text.replace(/"/g, '\\"')}"`;
|
|
63
|
+
}
|
|
60
64
|
function parseAttrs(tokens, line) {
|
|
61
65
|
const attrs = {};
|
|
62
66
|
let i = 0;
|
|
@@ -78,6 +82,43 @@ function parseAttrs(tokens, line) {
|
|
|
78
82
|
if (isAttrKey(valueToken)) {
|
|
79
83
|
throw new SourceError(`attribute "${key}" has no value`, line);
|
|
80
84
|
}
|
|
85
|
+
if (key === 'stroke') {
|
|
86
|
+
// Removed 2026-09-09. It meant a different part on every kind — the
|
|
87
|
+
// border of a box, the text of a note or a glyph body, the line of a
|
|
88
|
+
// link — so it could never be wrong, and a box's text had no word at all.
|
|
89
|
+
// Refused by name rather than ignored: an older file must be told what
|
|
90
|
+
// to write, not silently drawn without its colors.
|
|
91
|
+
throw new SourceError('`stroke:` has been replaced by the part it colors — `border:` on a box, `text:` on a note or a glyph body, `line:` on a link. A style shared between boxes and links writes both, as in `border: #d2904e line: #d2904e`', line);
|
|
92
|
+
}
|
|
93
|
+
if (key === 'width') {
|
|
94
|
+
// Renamed 2026-09-09. It folds a label every n *characters* and never
|
|
95
|
+
// said how wide anything is, so `width: 200` meaning units was accepted,
|
|
96
|
+
// wrapped at 200 characters, and did nothing visible — the silent drop
|
|
97
|
+
// this vocabulary is otherwise free of. Refused by name for the reason
|
|
98
|
+
// `stroke:` is: an older file must be told, not quietly drawn unwrapped.
|
|
99
|
+
throw new SourceError('`width:` is now `wrap:`, because it folds the text every n characters and says nothing about how wide anything is', line);
|
|
100
|
+
}
|
|
101
|
+
if (valueToken.quoted && COLOR_KEYS.includes(key)) {
|
|
102
|
+
// A quoted value is the author saying "this is text", and every one of
|
|
103
|
+
// these keys takes a color. Without this the string is passed through as
|
|
104
|
+
// a color, turns out not to be one, and nothing is drawn and nothing is
|
|
105
|
+
// said. Name the likely intent rather than only the rule: the qualifier
|
|
106
|
+
// under a name is a second label line, not a `subtext` value.
|
|
107
|
+
if (valueToken.text.startsWith('#')) {
|
|
108
|
+
// A hex color that was merely quoted. The author wrote a color and the
|
|
109
|
+
// remedy is punctuation, so say that rather than that it is not one.
|
|
110
|
+
throw new SourceError(`a color is written without quotes — "${key}: ${valueToken.text}"`, line);
|
|
111
|
+
}
|
|
112
|
+
// `text:` is the color of a label, not the label itself, and that is a
|
|
113
|
+
// mistake worth naming rather than only refusing — the word invites it.
|
|
114
|
+
if (key === 'text') {
|
|
115
|
+
throw new SourceError(`\`text:\` is the color of a label, not the label — write the words in quotes after the name, as in \`box name ${quoteOf(valueToken.text)}\``, line);
|
|
116
|
+
}
|
|
117
|
+
if (key === 'subtext') {
|
|
118
|
+
throw new SourceError(`\`subtext:\` is the color of a label's later lines, not the words — write them into the label, as in \`"Name / ${valueToken.text}"\`, and \`subtext: muted\` to make them quieter`, line);
|
|
119
|
+
}
|
|
120
|
+
throw new SourceError(`"${key}" takes a color and a quoted value is text — drop the quotes if ${valueToken.text} is a color`, line);
|
|
121
|
+
}
|
|
81
122
|
attrs[key] = valueToken.text;
|
|
82
123
|
i += 2;
|
|
83
124
|
}
|
|
@@ -268,7 +309,7 @@ function parsePlacements(tokens, line, subject) {
|
|
|
268
309
|
if (word.quoted) {
|
|
269
310
|
throw new SourceError(`${subject}: unexpected text "${word.text}"`, line);
|
|
270
311
|
}
|
|
271
|
-
// `top level with media` names an edge rather than the
|
|
312
|
+
// `top level with media` names an edge rather than the center line. `left`
|
|
272
313
|
// and `right` are edges as well as directions, so it is the word after them
|
|
273
314
|
// that says which was meant — "left of bup_hd" against "left level with bup_hd".
|
|
274
315
|
const edge = isEdgeWord(word.text) && follows(tokens, i + 1, 'level') ? word.text : undefined;
|
|
@@ -290,8 +331,8 @@ function parsePlacements(tokens, line, subject) {
|
|
|
290
331
|
}
|
|
291
332
|
placements.push({
|
|
292
333
|
kind: 'align',
|
|
293
|
-
axis: EDGE_AXIS[edge ?? '
|
|
294
|
-
edge: edge ?? '
|
|
334
|
+
axis: EDGE_AXIS[edge ?? 'center'],
|
|
335
|
+
edge: edge ?? 'center',
|
|
295
336
|
targets: read.targets,
|
|
296
337
|
line,
|
|
297
338
|
});
|
|
@@ -368,7 +409,7 @@ function readBracket(tokens, start, keys, about) {
|
|
|
368
409
|
throw new SourceError(`${about.subject}: "${key}" has no value`, about.line);
|
|
369
410
|
}
|
|
370
411
|
// A comma between modifiers is punctuation, exactly as it is between the
|
|
371
|
-
// targets of a placement. `(at: bottom, align:
|
|
412
|
+
// targets of a placement. `(at: bottom, align: center)` and the same without
|
|
372
413
|
// the comma are the same statement.
|
|
373
414
|
const value = valueToken.text;
|
|
374
415
|
values[key] = value.endsWith(',') && value.length > 1 ? value.slice(0, -1) : value;
|
|
@@ -389,7 +430,7 @@ function startsPlacement(token) {
|
|
|
389
430
|
EDGES.includes(token.text));
|
|
390
431
|
}
|
|
391
432
|
function isEdgeWord(word) {
|
|
392
|
-
return word !== '
|
|
433
|
+
return word !== 'center' && EDGES.includes(word);
|
|
393
434
|
}
|
|
394
435
|
/**
|
|
395
436
|
* One target, or several joined by `and` — `right of borg and bare`, or
|