reladraw 0.14.4 → 0.15.1
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 +1 -1
- package/SYNTAX.md +45 -5
- package/dist/ast.d.ts +17 -1
- package/dist/cli.js +7 -1
- package/dist/grammar.d.ts +2 -2
- package/dist/grammar.js +5 -3
- package/dist/icons.d.ts +32 -5
- package/dist/icons.js +94 -7
- package/dist/lexer.d.ts +13 -0
- package/dist/lexer.js +21 -0
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js +66 -10
- package/dist/render.js +15 -1
- package/dist/resolve.d.ts +6 -0
- package/dist/resolve.js +35 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
-
Version 0.
|
|
85
|
+
Version 0.15.1. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG, and an element that does the same inside a web page.
|
|
86
86
|
|
|
87
87
|
The language isn't stable yet, so expect the syntax to change.
|
|
88
88
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.15.1
|
|
2
2
|
|
|
3
3
|
What the language accepts. The parser, resolver and SVG renderer implement all of it; the sections at the end record what is defective, unchecked or undecided.
|
|
4
4
|
|
|
@@ -212,7 +212,7 @@ A circle is an ordinary node in every other way, and the file means the same thi
|
|
|
212
212
|
|
|
213
213
|
A name says what the thing *is*, never what the picture looks like: naming the meaning is what lets the drawing be improved later without every diagram that uses it changing sense. `document`, not `folded-corner`. The one place that rule stops is a picture with no single meaning — the cube stands for a container in one diagram, a VM in another, a service in a third — which is why it is called `cube` and not `instance`. An unrecognized name is an error listing the whole set, rather than a node that quietly draws nothing.
|
|
214
214
|
|
|
215
|
-
The icon set is small on purpose, and it is not the trade a drawing tool makes. There you pick a shape out of a visual palette and hundreds are browsable; here you type the word from memory, which caps the useful vocabulary at what fits in a head.
|
|
215
|
+
The icon set is small on purpose, and it is not the trade a drawing tool makes. There you pick a shape out of a visual palette and hundreds are browsable; here you type the word from memory, which caps the useful vocabulary at what fits in a head. A picture outside it is one you declare yourself — see [Your own icons](#your-own-icons).
|
|
216
216
|
|
|
217
217
|
`cubes` draws three whatever the number, because it is the symbol for "several" and not a count. Where the number matters — where one of them is the end of an arrow — they are separate nodes, each with `icon: cube`.
|
|
218
218
|
|
|
@@ -220,7 +220,7 @@ A node drawn as a picture is an ordinary node in every other way. It takes place
|
|
|
220
220
|
|
|
221
221
|
**A picture with no text of its own shows none.** Everywhere else a node with no text is labelled with its name, because `node a` and `node b right of a` mean the two boxes to read "a" and "b". A picture usually *is* the statement, so the default flips: `node svc icon: cube` draws the cube and no caption, and a row of five of them does not come out reading a, b, c, d, e. Write the text if you want one.
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
The built-in icons are drawn from path data inside the tool, never from a font or a linked file. The output is a standalone SVG and has to stay one — an icon font renders as blank boxes on a machine that does not have it, and a linked image has to travel beside the file.
|
|
224
224
|
|
|
225
225
|
### Badges
|
|
226
226
|
|
|
@@ -258,6 +258,36 @@ style store fill: #142814 border: #486544 badge: database
|
|
|
258
258
|
node records "Records" style: store
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
+
### Your own icons
|
|
262
|
+
|
|
263
|
+
`icon <name>` declares a picture of your own, and from then on `icon: <name>` and `badge: <name>` use it exactly as they would a built-in one. The picture is SVG, pasted between `"""` marks or read from a file:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
icon crate """
|
|
267
|
+
<svg viewBox="0 0 24 24">
|
|
268
|
+
<path d="M3 7 L12 3 L21 7 L21 17 L12 21 L3 17 Z" fill="none" stroke="currentColor"/>
|
|
269
|
+
</svg>
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
icon rack "./rack.svg"
|
|
273
|
+
|
|
274
|
+
node store "Warehouse" badge: crate
|
|
275
|
+
node servers icon: rack right of store
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Between `"""` marks everything is taken as written, double quotes and line breaks included, so an SVG can be pasted in unchanged. The opening `"""` may start the next line rather than end the `icon` line. A `"""` string is accepted only in an `icon` declaration for now.
|
|
279
|
+
|
|
280
|
+
A file is found relative to the diagram's own file. Only the command-line tool can read one; in the playground or a web page, paste the SVG instead.
|
|
281
|
+
|
|
282
|
+
- **It keeps its own colors.** A pasted drawing looks the same on every theme. What it leaves to `currentColor` takes the theme's icon color, which is how a one-color icon follows the theme.
|
|
283
|
+
- **It is fitted to the icon's square,** in proportion and centered, by its `viewBox`. With no `viewBox`, its `width` and `height` stand in; with neither it is refused, since nothing says how big the drawing is.
|
|
284
|
+
- **Its ids and class names are renamed** on the way in, so two icons that both call a gradient `a` don't pick up each other's.
|
|
285
|
+
- **Script is refused:** a `<script>` element or an `on…` attribute.
|
|
286
|
+
|
|
287
|
+
An icon of your own with a built-in's name replaces the built-in throughout the diagram, so `icon disk "./my-disk.svg"` redraws every disk. Declaring one name twice is an error.
|
|
288
|
+
|
|
289
|
+
The drawing is written into the output, so the SVG stays standalone however the icon was supplied.
|
|
290
|
+
|
|
261
291
|
## Placement
|
|
262
292
|
|
|
263
293
|
A *placement* says one thing about where a node goes. A node carries as many as it needs.
|
|
@@ -1066,8 +1096,6 @@ Designed, decided, and absent from the code. Written down so the next version ha
|
|
|
1066
1096
|
|
|
1067
1097
|
**Nothing keeps an edge clear of a node on its own.** Non-overlap applies to nodes only. A line may still cut across a node it has nothing to do with, and an edge text may still land on top of one. `between` and `below <node>` are how you say where a line goes when that matters, and nothing checks the ones where you have not said. A check belongs on the diagnostics list, but finding a route by itself does not — see "What the language refuses".
|
|
1068
1098
|
|
|
1069
|
-
**An icon outside the built-in seven.** The set is closed, and a diagram wanting a picture that is not in it has nowhere to go. The two shapes this could take are a declaration in the file, `icon <name> "<path data>"` beside `style`, and `icon: ./thing.svg` inlined by the tool at render time. Either keeps the output standalone, which is the constraint any answer has to meet.
|
|
1070
|
-
|
|
1071
1099
|
**A named channel cannot make room for itself.** Lines through a `between` gap too narrow for them crowd together silently, in exactly the way attachments on a too-short side do. An edge with text *does* now open the gap between its own two ends — see "A text makes room for itself" — and it does so by the measure-then-constrain route that region alignments already use, which is the route this wants too. What is missing is the harder case: several edges sharing a channel between two nodes neither of them is an end of, where the room needed is the whole stack of lanes rather than one text.
|
|
1072
1100
|
|
|
1073
1101
|
## Known to be wrong
|
|
@@ -1112,6 +1140,18 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1112
1140
|
|
|
1113
1141
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal below is refused by name with the replacement quoted, rather than dropped in silence — an older file stops with an error saying what to write instead.
|
|
1114
1142
|
|
|
1143
|
+
**0.15.1**
|
|
1144
|
+
|
|
1145
|
+
No syntax changes, and no file that worked is refused. No example diagram draws differently.
|
|
1146
|
+
|
|
1147
|
+
- A fix: many copies of one line slowed down sharply past fifty-five. With fifty-six or more copies of `edge a -> b from: left to: right`, the ends on each side sat under half a pixel apart, and every copy was then routed on its own instead of being drawn as a copy, so sixty took five and a half seconds. They now draw as copies however close their ends sit, and sixty take under a second.
|
|
1148
|
+
|
|
1149
|
+
**0.15.0**
|
|
1150
|
+
|
|
1151
|
+
No file that worked is refused. No example diagram draws differently.
|
|
1152
|
+
|
|
1153
|
+
- Icons of your own. `icon crate """<svg …>…</svg>"""` declares one from pasted SVG, or `icon crate "./crate.svg"` from a file, and `icon: crate` and `badge: crate` then use it like a built-in. One with a built-in's name replaces it. See [Your own icons](#your-own-icons).
|
|
1154
|
+
|
|
1115
1155
|
**0.14.4**
|
|
1116
1156
|
|
|
1117
1157
|
No syntax changes, and no file that worked is refused. No example diagram draws differently.
|
package/dist/ast.d.ts
CHANGED
|
@@ -442,7 +442,23 @@ export interface StyleStmt {
|
|
|
442
442
|
attrs: Attrs;
|
|
443
443
|
line: number;
|
|
444
444
|
}
|
|
445
|
-
|
|
445
|
+
/**
|
|
446
|
+
* `icon <name> <picture>`: a picture of the author's own, used by name wherever
|
|
447
|
+
* a built-in one could be. `source` is as written — the SVG itself, or the path
|
|
448
|
+
* of a file holding it.
|
|
449
|
+
*/
|
|
450
|
+
export interface IconStmt {
|
|
451
|
+
kind: 'icon';
|
|
452
|
+
name: string;
|
|
453
|
+
source: string;
|
|
454
|
+
/**
|
|
455
|
+
* True when `source` is the SVG itself: written between `"""` marks, or
|
|
456
|
+
* starting with `<`. Otherwise it names a file.
|
|
457
|
+
*/
|
|
458
|
+
pasted: boolean;
|
|
459
|
+
line: number;
|
|
460
|
+
}
|
|
461
|
+
export type Stmt = NodeStmt | EdgeStmt | StyleStmt | DiagramStmt | DefaultStmt | IconStmt;
|
|
446
462
|
export interface Document {
|
|
447
463
|
statements: Stmt[];
|
|
448
464
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, resolve as resolvePath } from 'node:path';
|
|
4
5
|
import { SourceError } from './errors.js';
|
|
@@ -63,7 +64,12 @@ async function main(argv) {
|
|
|
63
64
|
const source = await readFile(input, 'utf8');
|
|
64
65
|
let svg;
|
|
65
66
|
try {
|
|
66
|
-
svg = compile(source,
|
|
67
|
+
svg = compile(source, {
|
|
68
|
+
...(theme === undefined ? {} : { theme: THEMES[theme] }),
|
|
69
|
+
// An icon's file is found from the diagram, not from wherever the tool
|
|
70
|
+
// happens to be run.
|
|
71
|
+
readIconFile: (path) => readFileSync(resolvePath(dirname(input), path), 'utf8'),
|
|
72
|
+
});
|
|
67
73
|
}
|
|
68
74
|
catch (error) {
|
|
69
75
|
if (error instanceof SourceError) {
|
package/dist/grammar.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface Span {
|
|
|
64
64
|
* it. A word missing here is a word that draws in the plain color, which is a
|
|
65
65
|
* dull page rather than a wrong one.
|
|
66
66
|
*/
|
|
67
|
-
export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "style", "diagram", "default"];
|
|
67
|
+
export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "style", "diagram", "default", "icon"];
|
|
68
68
|
/**
|
|
69
69
|
* Every word that says something about where a thing goes. Assembled from the
|
|
70
70
|
* lists the parser itself reads, so a direction or a passage axis added there
|
|
@@ -74,7 +74,7 @@ export declare const RELATION_WORDS: string[];
|
|
|
74
74
|
export declare const PATTERNS: {
|
|
75
75
|
readonly comment: "\\/\\/.*";
|
|
76
76
|
readonly string: "\"(?:\\\\.|[^\"\\\\])*\"?";
|
|
77
|
-
readonly keyword: `(?:${string})\\b`;
|
|
77
|
+
readonly keyword: `(?:${string})\\b(?!:)`;
|
|
78
78
|
readonly arrow: `${string}?-{1,2}${string}?(?=[ \\t]|$)`;
|
|
79
79
|
readonly attribute: "[A-Za-z][A-Za-z0-9_-]*:";
|
|
80
80
|
readonly color: "#[0-9A-Fa-f]{3,8}\\b";
|
package/dist/grammar.js
CHANGED
|
@@ -33,9 +33,9 @@ import { DIRECTIONS, MARKS, POSITIONS, SIDES, PASSAGE_AXES } from './ast.js';
|
|
|
33
33
|
* it. A word missing here is a word that draws in the plain color, which is a
|
|
34
34
|
* dull page rather than a wrong one.
|
|
35
35
|
*/
|
|
36
|
-
export const STATEMENT_KEYWORDS = ['node', 'edge', 'style', 'diagram', 'default'];
|
|
36
|
+
export const STATEMENT_KEYWORDS = ['node', 'edge', 'style', 'diagram', 'default', 'icon'];
|
|
37
37
|
/** Statements whose second word declares a name. `diagram` has none. */
|
|
38
|
-
const DECLARES_NAME = ['node', 'style'];
|
|
38
|
+
const DECLARES_NAME = ['node', 'style', 'icon'];
|
|
39
39
|
/**
|
|
40
40
|
* Every word that says something about where a thing goes. Assembled from the
|
|
41
41
|
* lists the parser itself reads, so a direction or a passage axis added there
|
|
@@ -80,7 +80,9 @@ export const PATTERNS = {
|
|
|
80
80
|
// it is being typed, and a highlighter that waits for the quote repaints the
|
|
81
81
|
// rest of the file on every keystroke.
|
|
82
82
|
string: '"(?:\\\\.|[^"\\\\])*"?',
|
|
83
|
-
|
|
83
|
+
// Not before a colon: `icon` is a keyword and `icon:` an attribute, and an
|
|
84
|
+
// indented line may open with the attribute.
|
|
85
|
+
keyword: `(?:${alternation(STATEMENT_KEYWORDS)})\\b(?!:)`,
|
|
84
86
|
// A mark, one or two dashes, a mark — `->`, `o--odiamond`, `[dot]-`. It must
|
|
85
87
|
// end the token, so a hyphenated name stays a word. A mark written apart in
|
|
86
88
|
// brackets, `[dot] -- [arrow]`, colors as plain words either side of `--`.
|
package/dist/icons.d.ts
CHANGED
|
@@ -30,11 +30,25 @@ export interface IconPath {
|
|
|
30
30
|
fill?: IconTone;
|
|
31
31
|
stroke?: IconTone;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
/** A built-in picture, drawn in the theme's tones. */
|
|
34
|
+
export interface DrawnIcon {
|
|
34
35
|
/** Side of the square the paths are drawn on. Scaled to the drawn size. */
|
|
35
36
|
readonly grid: number;
|
|
36
37
|
readonly paths: readonly IconPath[];
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A picture the author supplied as SVG, written into the output as a nested
|
|
41
|
+
* `<svg>` fitted to the icon's square. It keeps its own colors: a pasted
|
|
42
|
+
* drawing is drawn as it was drawn, on every theme. What it leaves to
|
|
43
|
+
* `currentColor` takes the theme's icon line color, which is how a
|
|
44
|
+
* one-color icon set follows the theme anyway.
|
|
45
|
+
*/
|
|
46
|
+
export interface PastedIcon {
|
|
47
|
+
/** The root element's attributes, less the ones the placement sets. */
|
|
48
|
+
readonly attrs: string;
|
|
49
|
+
readonly inner: string;
|
|
50
|
+
}
|
|
51
|
+
export type Icon = DrawnIcon | PastedIcon;
|
|
38
52
|
/** Line width on the 24-unit grid, scaled with everything else. */
|
|
39
53
|
export declare const ICON_STROKE = 1.1;
|
|
40
54
|
export declare const ICONS: Record<string, Icon>;
|
|
@@ -93,7 +107,7 @@ export type Body = {
|
|
|
93
107
|
* has overridden is unused rather than wrong. Both keys from one level is a
|
|
94
108
|
* genuine contradiction and is refused by name.
|
|
95
109
|
*/
|
|
96
|
-
export declare function bodyFor(attrs: Record<string, string>, appearance: Record<string, string>, line: number): Body;
|
|
110
|
+
export declare function bodyFor(attrs: Record<string, string>, appearance: Record<string, string>, line: number, declared?: ReadonlyMap<string, Icon>): Body;
|
|
97
111
|
/**
|
|
98
112
|
* The icon of that name, or an error listing the set.
|
|
99
113
|
*
|
|
@@ -101,7 +115,20 @@ export declare function bodyFor(attrs: Record<string, string>, appearance: Recor
|
|
|
101
115
|
* `DIAGRAM_KEYS` follows and it is here for the same reason: a misspelt
|
|
102
116
|
* `icon: laptp` that quietly draws nothing is indistinguishable from the tool
|
|
103
117
|
* being broken, and an author will stare at the file looking for the mistake in
|
|
104
|
-
* the wrong place.
|
|
105
|
-
* the
|
|
118
|
+
* the wrong place. The vocabulary is short enough for the error to list all of
|
|
119
|
+
* it, the file's own icons included.
|
|
120
|
+
*
|
|
121
|
+
* An icon the file declares wins over a built-in of the same name, so an author
|
|
122
|
+
* who dislikes the drawing of `disk` can replace it everywhere at once.
|
|
123
|
+
*/
|
|
124
|
+
export declare function iconNamed(named: string, line: number, declared?: ReadonlyMap<string, Icon>): Icon;
|
|
125
|
+
/**
|
|
126
|
+
* Read an author's SVG into an icon. `name` makes the drawing's own ids and
|
|
127
|
+
* class names unique, so two pasted icons that both call a gradient `a` do not
|
|
128
|
+
* paint with each other's.
|
|
129
|
+
*
|
|
130
|
+
* Refused, each by name: text with no `<svg>` in it, a drawing with no way to
|
|
131
|
+
* tell its size, and anything that runs script, which a diagram has no use for
|
|
132
|
+
* and a browser showing the diagram might execute.
|
|
106
133
|
*/
|
|
107
|
-
export declare function
|
|
134
|
+
export declare function pastedIcon(source: string, name: string, line: number): PastedIcon;
|
package/dist/icons.js
CHANGED
|
@@ -149,7 +149,7 @@ const PLAIN = { kind: 'shape', outline: 'rectangle' };
|
|
|
149
149
|
* has overridden is unused rather than wrong. Both keys from one level is a
|
|
150
150
|
* genuine contradiction and is refused by name.
|
|
151
151
|
*/
|
|
152
|
-
export function bodyFor(attrs, appearance, line) {
|
|
152
|
+
export function bodyFor(attrs, appearance, line, declared = new Map()) {
|
|
153
153
|
const wroteShape = attrs['shape'] !== undefined;
|
|
154
154
|
const wroteIcon = attrs['icon'] !== undefined;
|
|
155
155
|
let shape = wroteIcon && !wroteShape ? undefined : appearance['shape'];
|
|
@@ -159,7 +159,7 @@ export function bodyFor(attrs, appearance, line) {
|
|
|
159
159
|
'body — `shape:` is the outline it is drawn with, `icon:` is the picture it is drawn as', line);
|
|
160
160
|
}
|
|
161
161
|
if (icon !== undefined)
|
|
162
|
-
return { kind: 'icon', icon: iconNamed(icon, line) };
|
|
162
|
+
return { kind: 'icon', icon: iconNamed(icon, line, declared) };
|
|
163
163
|
if (shape === undefined)
|
|
164
164
|
return PLAIN;
|
|
165
165
|
if (shape === 'none')
|
|
@@ -194,14 +194,101 @@ function refuse(message, line) {
|
|
|
194
194
|
* `DIAGRAM_KEYS` follows and it is here for the same reason: a misspelt
|
|
195
195
|
* `icon: laptp` that quietly draws nothing is indistinguishable from the tool
|
|
196
196
|
* being broken, and an author will stare at the file looking for the mistake in
|
|
197
|
-
* the wrong place.
|
|
198
|
-
* the
|
|
197
|
+
* the wrong place. The vocabulary is short enough for the error to list all of
|
|
198
|
+
* it, the file's own icons included.
|
|
199
|
+
*
|
|
200
|
+
* An icon the file declares wins over a built-in of the same name, so an author
|
|
201
|
+
* who dislikes the drawing of `disk` can replace it everywhere at once.
|
|
199
202
|
*/
|
|
200
|
-
export function iconNamed(named, line) {
|
|
201
|
-
const icon = ICONS[named];
|
|
203
|
+
export function iconNamed(named, line, declared = new Map()) {
|
|
204
|
+
const icon = declared.get(named) ?? ICONS[named];
|
|
202
205
|
if (icon === undefined) {
|
|
203
206
|
const was = named === 'instance' ? ' — the cube was called `instance` until 0.3.0' : '';
|
|
204
|
-
|
|
207
|
+
const all = [...new Set([...ICON_NAMES, ...declared.keys()])];
|
|
208
|
+
throw new SourceError(`there is no icon called "${named}". The icons are ${all.join(', ')}${was}`, line);
|
|
205
209
|
}
|
|
206
210
|
return icon;
|
|
207
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Read an author's SVG into an icon. `name` makes the drawing's own ids and
|
|
214
|
+
* class names unique, so two pasted icons that both call a gradient `a` do not
|
|
215
|
+
* paint with each other's.
|
|
216
|
+
*
|
|
217
|
+
* Refused, each by name: text with no `<svg>` in it, a drawing with no way to
|
|
218
|
+
* tell its size, and anything that runs script, which a diagram has no use for
|
|
219
|
+
* and a browser showing the diagram might execute.
|
|
220
|
+
*/
|
|
221
|
+
export function pastedIcon(source, name, line) {
|
|
222
|
+
const start = source.search(/<svg[\s>/]/);
|
|
223
|
+
if (start === -1) {
|
|
224
|
+
throw new SourceError(`icon "${name}" is not an SVG — it should contain an <svg> element`, line);
|
|
225
|
+
}
|
|
226
|
+
if (/<script[\s>]/i.test(source) || /\son[a-z]+\s*=/i.test(source)) {
|
|
227
|
+
throw new SourceError(`icon "${name}" contains script, which a diagram has no use for and a browser showing it ` +
|
|
228
|
+
'might run — remove the <script> element or the on… attribute', line);
|
|
229
|
+
}
|
|
230
|
+
const open = /^<svg((?:\s+[^\s=>/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'))?)*)\s*(\/?)>/.exec(source.slice(start));
|
|
231
|
+
if (open === null) {
|
|
232
|
+
throw new SourceError(`icon "${name}" has an <svg> tag that does not close — it needs its >`, line);
|
|
233
|
+
}
|
|
234
|
+
const bodyStart = start + open[0].length;
|
|
235
|
+
const end = source.lastIndexOf('</svg>');
|
|
236
|
+
if (open[2] !== '/' && end < bodyStart) {
|
|
237
|
+
throw new SourceError(`icon "${name}" has no </svg> to end it`, line);
|
|
238
|
+
}
|
|
239
|
+
let inner = open[2] === '/' ? '' : source.slice(bodyStart, end);
|
|
240
|
+
const attrs = new Map();
|
|
241
|
+
for (const [, key, value] of open[1].matchAll(/([^\s=]+)(?:\s*=\s*("[^"]*"|'[^']*'))?/g)) {
|
|
242
|
+
attrs.set(key, value ?? '""');
|
|
243
|
+
}
|
|
244
|
+
// The placement sets where the drawing goes and how big it is, and a nested
|
|
245
|
+
// <svg> needs no namespace of its own.
|
|
246
|
+
const width = attrs.get('width');
|
|
247
|
+
const height = attrs.get('height');
|
|
248
|
+
for (const key of [...attrs.keys()]) {
|
|
249
|
+
if (['x', 'y', 'width', 'height', 'version'].includes(key) || key === 'xmlns' || key.startsWith('xmlns:')) {
|
|
250
|
+
attrs.delete(key);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!attrs.has('viewBox')) {
|
|
254
|
+
const number = (value) => Number(value?.slice(1, -1).replace(/px$/, ''));
|
|
255
|
+
const w = number(width);
|
|
256
|
+
const h = number(height);
|
|
257
|
+
if (!(w > 0 && h > 0)) {
|
|
258
|
+
throw new SourceError(`icon "${name}" has no viewBox and no width and height to take one from, so there is no ` +
|
|
259
|
+
'telling how big its drawing is — add viewBox="0 0 <width> <height>"', line);
|
|
260
|
+
}
|
|
261
|
+
attrs.set('viewBox', `"0 0 ${w} ${h}"`);
|
|
262
|
+
}
|
|
263
|
+
// The output declares no xlink namespace, and SVG 2 reads a plain `href`.
|
|
264
|
+
inner = inner.replace(/\sxlink:href=/g, ' href=');
|
|
265
|
+
const prefix = `rd-${name.replace(/[^A-Za-z0-9_-]/g, '_')}-`;
|
|
266
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
267
|
+
// An id is renamed where it is defined and where it is referred to, which is
|
|
268
|
+
// `url(#…)` or `href="#…"` — never a bare `#…`, which may be a color.
|
|
269
|
+
for (const [, , double, single] of inner.matchAll(/\sid\s*=\s*("([^"]*)"|'([^']*)')/g)) {
|
|
270
|
+
const id = double ?? single;
|
|
271
|
+
const pattern = escape(id);
|
|
272
|
+
inner = inner
|
|
273
|
+
.replace(new RegExp(`(\\sid\\s*=\\s*["'])${pattern}(["'])`, 'g'), `$1${prefix}${id}$2`)
|
|
274
|
+
.replace(new RegExp(`(url\\(\\s*['"]?#)${pattern}(?=['"]?\\s*\\))`, 'g'), `$1${prefix}${id}`)
|
|
275
|
+
.replace(new RegExp(`(\\shref\\s*=\\s*["']#)${pattern}(?=["'])`, 'g'), `$1${prefix}${id}`);
|
|
276
|
+
}
|
|
277
|
+
// A class is renamed on the elements that wear it and in the drawing's own
|
|
278
|
+
// <style> blocks, since a stylesheet in an SVG applies to the whole document.
|
|
279
|
+
const classes = new Set();
|
|
280
|
+
for (const [, , double, single] of inner.matchAll(/\sclass\s*=\s*("([^"]*)"|'([^']*)')/g)) {
|
|
281
|
+
for (const each of (double ?? single).split(/\s+/))
|
|
282
|
+
if (each !== '')
|
|
283
|
+
classes.add(each);
|
|
284
|
+
}
|
|
285
|
+
inner = inner.replace(/(\sclass\s*=\s*["'])([^"']*)/g, (_, key, value) => key + value.replace(/[^\s]+/g, (each) => prefix + each));
|
|
286
|
+
inner = inner.replace(/(<style[^>]*>)([\s\S]*?)(<\/style>)/g, (_, open, css, close) => {
|
|
287
|
+
for (const each of classes) {
|
|
288
|
+
css = css.replace(new RegExp(`\\.${escape(each)}(?![\\w-])`, 'g'), `.${prefix}${each}`);
|
|
289
|
+
}
|
|
290
|
+
return open + css + close;
|
|
291
|
+
});
|
|
292
|
+
const written = [...attrs].map(([key, value]) => `${key}=${value}`).join(' ');
|
|
293
|
+
return { attrs: written, inner: inner.trim() };
|
|
294
|
+
}
|
package/dist/lexer.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import { SourceError } from './errors.js';
|
|
1
2
|
export interface Token {
|
|
2
3
|
text: string;
|
|
3
4
|
/** True when the token came from a quoted string, so `foo:` inside it is literal. */
|
|
4
5
|
quoted: boolean;
|
|
6
|
+
/** True when the string was written between `"""` marks. */
|
|
7
|
+
triple?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A `"""` string still open at the end of the text given. `parse` catches this
|
|
11
|
+
* one, adds the next line and tries again, which is how a string runs over
|
|
12
|
+
* several lines without the lexer having to know about lines.
|
|
13
|
+
*/
|
|
14
|
+
export declare class OpenString extends SourceError {
|
|
5
15
|
}
|
|
6
16
|
/**
|
|
7
17
|
* Split one line into tokens. Whitespace separates; double quotes group, with
|
|
@@ -21,6 +31,9 @@ export interface Token {
|
|
|
21
31
|
* `depth` is how many brackets are still open from the lines before, when a
|
|
22
32
|
* statement continues onto this one.
|
|
23
33
|
*
|
|
34
|
+
* `line` may hold line breaks, but only inside a `"""` string: `parse` joins
|
|
35
|
+
* the lines such a string spans before handing them over.
|
|
36
|
+
*
|
|
24
37
|
* Returns an empty array for a blank or comment-only line.
|
|
25
38
|
*/
|
|
26
39
|
export declare function tokenizeLine(line: string, lineNumber: number, depth?: number): Token[];
|
package/dist/lexer.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { SourceError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* A `"""` string still open at the end of the text given. `parse` catches this
|
|
4
|
+
* one, adds the next line and tries again, which is how a string runs over
|
|
5
|
+
* several lines without the lexer having to know about lines.
|
|
6
|
+
*/
|
|
7
|
+
export class OpenString extends SourceError {
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* Split one line into tokens. Whitespace separates; double quotes group, with
|
|
4
11
|
* `\"` and `\\` as the only escapes. A `//` outside quotes starts a comment and
|
|
@@ -17,6 +24,9 @@ import { SourceError } from './errors.js';
|
|
|
17
24
|
* `depth` is how many brackets are still open from the lines before, when a
|
|
18
25
|
* statement continues onto this one.
|
|
19
26
|
*
|
|
27
|
+
* `line` may hold line breaks, but only inside a `"""` string: `parse` joins
|
|
28
|
+
* the lines such a string spans before handing them over.
|
|
29
|
+
*
|
|
20
30
|
* Returns an empty array for a blank or comment-only line.
|
|
21
31
|
*/
|
|
22
32
|
export function tokenizeLine(line, lineNumber, depth = 0) {
|
|
@@ -42,6 +52,17 @@ export function tokenizeLine(line, lineNumber, depth = 0) {
|
|
|
42
52
|
i += 1;
|
|
43
53
|
continue;
|
|
44
54
|
}
|
|
55
|
+
// Between `"""` marks everything is taken as written, quotes and line breaks
|
|
56
|
+
// included, with no escapes. That is what makes an SVG pasteable: it is full
|
|
57
|
+
// of double quotes and never has three in a row.
|
|
58
|
+
if (line.startsWith('"""', i)) {
|
|
59
|
+
const end = line.indexOf('"""', i + 3);
|
|
60
|
+
if (end === -1)
|
|
61
|
+
throw new OpenString('this """ string is never closed — end it with """', lineNumber);
|
|
62
|
+
tokens.push({ text: line.slice(i + 3, end), quoted: true, triple: true });
|
|
63
|
+
i = end + 3;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
45
66
|
if (ch === '"') {
|
|
46
67
|
let text = '';
|
|
47
68
|
i += 1;
|
package/dist/parser.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { SourceError } from './errors.js';
|
|
|
10
10
|
* it. An indented line with nothing open above it is refused rather than read
|
|
11
11
|
* as a statement of its own: it is a stray indent or a deleted first line, and
|
|
12
12
|
* both are worth hearing about.
|
|
13
|
+
*
|
|
14
|
+
* A `"""` string is the one thing that crosses these rules: the lines it spans
|
|
15
|
+
* are its own, blank or not, and a line opening with one continues the
|
|
16
|
+
* statement above without being indented.
|
|
13
17
|
*/
|
|
14
18
|
export declare function parse(source: string): Document;
|
|
15
19
|
/**
|
package/dist/parser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COLOR_KEYS, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, PASSAGE_AXES, TEXT_KEYS, LINE_KEYS, LINE_VALUES, CONTENTS_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, MARKS, MARK_GLYPHS, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
3
|
import { STATEMENT_KEYWORDS } from './grammar.js';
|
|
4
|
-
import { isAttrKey, tokenizeLine } from './lexer.js';
|
|
4
|
+
import { isAttrKey, OpenString, tokenizeLine } from './lexer.js';
|
|
5
5
|
import { THEME_COLORS, THEME_NAMES, THEMES } from './themes.js';
|
|
6
6
|
/**
|
|
7
7
|
* Parse a whole source file. A statement starts on an unindented line and runs
|
|
@@ -13,6 +13,10 @@ import { THEME_COLORS, THEME_NAMES, THEMES } from './themes.js';
|
|
|
13
13
|
* it. An indented line with nothing open above it is refused rather than read
|
|
14
14
|
* as a statement of its own: it is a stray indent or a deleted first line, and
|
|
15
15
|
* both are worth hearing about.
|
|
16
|
+
*
|
|
17
|
+
* A `"""` string is the one thing that crosses these rules: the lines it spans
|
|
18
|
+
* are its own, blank or not, and a line opening with one continues the
|
|
19
|
+
* statement above without being indented.
|
|
16
20
|
*/
|
|
17
21
|
export function parse(source) {
|
|
18
22
|
const statements = [];
|
|
@@ -27,26 +31,53 @@ export function parse(source) {
|
|
|
27
31
|
open = null;
|
|
28
32
|
depth = 0;
|
|
29
33
|
};
|
|
30
|
-
source.split(/\r?\n/)
|
|
34
|
+
const lines = source.split(/\r?\n/);
|
|
35
|
+
for (let index = 0; index < lines.length; index++) {
|
|
31
36
|
const lineNumber = index + 1;
|
|
37
|
+
let line = lines[index];
|
|
32
38
|
if (line.trim() === '') {
|
|
33
39
|
close();
|
|
34
|
-
|
|
40
|
+
continue;
|
|
35
41
|
}
|
|
42
|
+
// A line opening with `"""` continues the statement above without being
|
|
43
|
+
// indented, because nothing else could start with one: no statement begins
|
|
44
|
+
// with a string, so `icon rack` on one line and the SVG from the next is
|
|
45
|
+
// unambiguous. The exception is that narrow on purpose — a statement that
|
|
46
|
+
// could carry on through any line would turn a forgotten word into an error
|
|
47
|
+
// about some other line.
|
|
36
48
|
const indented = line[0] === ' ' || line[0] === '\t';
|
|
37
|
-
|
|
49
|
+
const continues = indented || line.startsWith('"""');
|
|
50
|
+
if (!continues)
|
|
38
51
|
close();
|
|
39
|
-
|
|
52
|
+
// A `"""` string may run on over the lines after it, blank ones included,
|
|
53
|
+
// and every line it covers belongs to it rather than to the statement
|
|
54
|
+
// structure above.
|
|
55
|
+
let tokens;
|
|
56
|
+
for (;;) {
|
|
57
|
+
try {
|
|
58
|
+
tokens = tokenizeLine(line, lineNumber, depth);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!(error instanceof OpenString) || index + 1 >= lines.length)
|
|
63
|
+
throw error;
|
|
64
|
+
index += 1;
|
|
65
|
+
line += '\n' + lines[index];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
40
68
|
if (tokens.length === 0)
|
|
41
|
-
|
|
42
|
-
if (!
|
|
69
|
+
continue;
|
|
70
|
+
if (!continues) {
|
|
43
71
|
open = { tokens, line: lineNumber };
|
|
44
72
|
}
|
|
45
73
|
else {
|
|
46
74
|
const first = tokens[0];
|
|
47
75
|
if (!open) {
|
|
48
|
-
throw new SourceError(
|
|
49
|
-
'
|
|
76
|
+
throw new SourceError(indented
|
|
77
|
+
? 'this line is indented, which continues the statement above, but there is none to ' +
|
|
78
|
+
'continue — remove the indentation to start a new statement'
|
|
79
|
+
: 'a """ string at the start of a line belongs to the statement above, but there is ' +
|
|
80
|
+
'none — write it after the words it goes with, such as `icon rack`', lineNumber);
|
|
50
81
|
}
|
|
51
82
|
if (!first.quoted && STATEMENT_KEYWORDS.includes(first.text)) {
|
|
52
83
|
throw new SourceError(`\`${first.text}\` cannot continue the statement above — an indented line continues ` +
|
|
@@ -62,7 +93,7 @@ export function parse(source) {
|
|
|
62
93
|
else if (token.text === ')')
|
|
63
94
|
depth -= 1;
|
|
64
95
|
}
|
|
65
|
-
}
|
|
96
|
+
}
|
|
66
97
|
close();
|
|
67
98
|
return { statements };
|
|
68
99
|
}
|
|
@@ -108,7 +139,16 @@ function parseStatement(tokens, line) {
|
|
|
108
139
|
if (!keyword || keyword.quoted) {
|
|
109
140
|
throw new SourceError('a statement must begin with a keyword', line);
|
|
110
141
|
}
|
|
142
|
+
// What a line break inside a node's or an edge's text should mean has not
|
|
143
|
+
// been decided, so until it is a `"""` string is refused anywhere but the one
|
|
144
|
+
// place it was made for, rather than drawn in some way that might change.
|
|
145
|
+
if (keyword.text !== 'icon' && tokens.some((token) => token.triple)) {
|
|
146
|
+
throw new SourceError('a """ string holds the SVG of an `icon` declaration and is not accepted anywhere else yet — ' +
|
|
147
|
+
'use ordinary quotes here', line);
|
|
148
|
+
}
|
|
111
149
|
switch (keyword.text) {
|
|
150
|
+
case 'icon':
|
|
151
|
+
return parseIcon(tokens, line);
|
|
112
152
|
case 'node':
|
|
113
153
|
return parseNode(tokens, line);
|
|
114
154
|
case 'edge':
|
|
@@ -712,6 +752,22 @@ function parseStyle(head, line) {
|
|
|
712
752
|
}
|
|
713
753
|
return { kind: 'style', name, attrs, line };
|
|
714
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* `icon <name> "<svg>…</svg>"`, or `icon <name> ./file.svg`. What the picture
|
|
757
|
+
* is — pasted markup or a file to read — is worked out when the diagram is
|
|
758
|
+
* resolved, since only the caller knows whether files can be read at all.
|
|
759
|
+
*/
|
|
760
|
+
function parseIcon(head, line) {
|
|
761
|
+
const name = requireName(head[1], 'icon', line);
|
|
762
|
+
const picture = head[2];
|
|
763
|
+
if (picture === undefined || isAttrKey(picture)) {
|
|
764
|
+
throw new SourceError(`icon "${name}" needs its picture: the SVG itself between """ marks, or the name of an .svg file`, line);
|
|
765
|
+
}
|
|
766
|
+
if (head.length > 3) {
|
|
767
|
+
throw new SourceError(`icon "${name}" takes one picture and nothing else — "${head[3].text}" is left over`, line);
|
|
768
|
+
}
|
|
769
|
+
return { kind: 'icon', name, source: picture.text, pasted: picture.triple === true || picture.text.trim().startsWith('<'), line };
|
|
770
|
+
}
|
|
715
771
|
/**
|
|
716
772
|
* `diagram <attributes>` — no name, because a file holds one diagram. Unknown
|
|
717
773
|
* keys are refused rather than ignored: a misspelt diagram-wide setting that
|
package/dist/render.js
CHANGED
|
@@ -335,6 +335,17 @@ function outlineDetail(shape, x, y, w, h) {
|
|
|
335
335
|
}
|
|
336
336
|
/** One icon, scaled from its own grid onto a square of `side` at `x, y`. */
|
|
337
337
|
function drawIcon(icon, x, y, side, theme) {
|
|
338
|
+
if ('inner' in icon) {
|
|
339
|
+
// Its own viewBox fits it to the square, centered and in proportion.
|
|
340
|
+
// `color` is set only when the drawing does not set it, so `currentColor`
|
|
341
|
+
// means the theme's icon line unless the author said otherwise.
|
|
342
|
+
const color = /(^|\s)color=/.test(icon.attrs) ? '' : ` color="${theme.iconInk}"`;
|
|
343
|
+
return [
|
|
344
|
+
` <svg x="${round(x)}" y="${round(y)}" width="${round(side)}" height="${round(side)}"${color} ${icon.attrs}>`,
|
|
345
|
+
icon.inner,
|
|
346
|
+
' </svg>',
|
|
347
|
+
].join('\n');
|
|
348
|
+
}
|
|
338
349
|
const scale = side / icon.grid;
|
|
339
350
|
const color = (tone) => tone === 'ink' ? theme.iconInk : tone === 'shade' ? theme.iconShade : theme.background;
|
|
340
351
|
const paths = icon.paths.map((path) => {
|
|
@@ -1877,7 +1888,10 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1877
1888
|
const normal = heading.map((one) => ({ x: -one.y, y: one.x }));
|
|
1878
1889
|
const along = (from, to, axis) => (to.x - from.x) * axis.x + (to.y - from.y) * axis.y;
|
|
1879
1890
|
const shift = along(base[0], start, normal[0]);
|
|
1880
|
-
|
|
1891
|
+
// Any shift will do, however small: where more lines leave a side than it
|
|
1892
|
+
// has room for, their ends sit under half a pixel apart, and refusing those
|
|
1893
|
+
// sent fifty-six copies to the search and took seconds.
|
|
1894
|
+
if (Math.abs(shift) < 1e-6 || Math.abs(along(base[0], start, heading[0])) > 0.5)
|
|
1881
1895
|
return undefined;
|
|
1882
1896
|
if (Math.abs(along(base[count], end, normal[count - 1]) - shift) > 0.5 || Math.abs(along(base[count], end, heading[count - 1])) > 0.5) {
|
|
1883
1897
|
return undefined;
|
package/dist/resolve.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export interface ResolveOptions {
|
|
|
6
6
|
fontSize?: number;
|
|
7
7
|
/** Blank space kept around the whole diagram. */
|
|
8
8
|
margin?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Read the file an `icon` declaration names, as text. The command-line tool
|
|
11
|
+
* supplies it, reading relative to the diagram's own file; without it, as in
|
|
12
|
+
* a browser, an icon has to be pasted in rather than named by file.
|
|
13
|
+
*/
|
|
14
|
+
readIconFile?: (path: string) => string;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
17
|
* Turn a parsed document into solved geometry.
|
package/dist/resolve.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ALL_ATTR_KEYS, ATTR_KEYS, MARKS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, C
|
|
|
2
2
|
import { BARE_EDGE_RUN, CHILD_GAP, DECK_STEP, DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, HEADER_GAP, ICON_GAP, ICON_LINES, TEXT_CLEARANCE, PAD, SEPARATION_GAP, THICKNESS, arrowLength, fontSizeFor, leafTop, textExtent, textStyleFor, widestLine, } from './constants.js';
|
|
3
3
|
import { fix, reachability, tightest } from './constrain.js';
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
5
|
-
import { bodyFor } from './icons.js';
|
|
5
|
+
import { bodyFor, pastedIcon } from './icons.js';
|
|
6
6
|
import { monospaceMeasurer } from './measure.js';
|
|
7
7
|
import { markupStyles, parseMarkup, plain, splitRuns, wrapLine } from './text.js';
|
|
8
8
|
/**
|
|
@@ -24,7 +24,8 @@ export function resolve(doc, options = {}) {
|
|
|
24
24
|
const styles = collectStyles(doc.statements);
|
|
25
25
|
checkStyleKeys(doc.statements);
|
|
26
26
|
const defaults = collectDefaults(doc.statements, styles);
|
|
27
|
-
const
|
|
27
|
+
const icons = collectIcons(doc.statements, options.readIconFile);
|
|
28
|
+
const { nodes, byName, roots } = buildTree(doc.statements, styles, defaults, icons);
|
|
28
29
|
// Edges are resolved to nodes before anything is sized, because a labeled
|
|
29
30
|
// edge claims room in the gap it crosses and so has to be in hand while the
|
|
30
31
|
// gaps are being worked out. Nothing here reads geometry.
|
|
@@ -59,6 +60,33 @@ function collectStyles(statements) {
|
|
|
59
60
|
}
|
|
60
61
|
return styles;
|
|
61
62
|
}
|
|
63
|
+
/** The file's own icons, by name, each pasted in or read from its file. */
|
|
64
|
+
function collectIcons(statements, readIconFile) {
|
|
65
|
+
const icons = new Map();
|
|
66
|
+
for (const stmt of statements) {
|
|
67
|
+
if (stmt.kind !== 'icon')
|
|
68
|
+
continue;
|
|
69
|
+
if (icons.has(stmt.name)) {
|
|
70
|
+
throw new SourceError(`icon "${stmt.name}" is declared twice`, stmt.line);
|
|
71
|
+
}
|
|
72
|
+
let svg = stmt.source.trim();
|
|
73
|
+
if (!stmt.pasted) {
|
|
74
|
+
if (readIconFile === undefined) {
|
|
75
|
+
throw new SourceError(`icon "${stmt.name}" names a file, "${svg}", and files can be read only by the ` +
|
|
76
|
+
'command-line tool — paste the SVG itself between """ marks instead', stmt.line);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
svg = readIconFile(svg);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
83
|
+
throw new SourceError(`icon "${stmt.name}" could not read "${svg}": ${why}`, stmt.line);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
icons.set(stmt.name, pastedIcon(svg, stmt.name, stmt.line));
|
|
87
|
+
}
|
|
88
|
+
return icons;
|
|
89
|
+
}
|
|
62
90
|
/**
|
|
63
91
|
* Each kind's default, with any style it names already folded in beneath its
|
|
64
92
|
* own words, so a default arrives at a node as one flat bundle. The same kind
|
|
@@ -105,7 +133,7 @@ function collectDiagram(statements) {
|
|
|
105
133
|
}
|
|
106
134
|
return found ?? {};
|
|
107
135
|
}
|
|
108
|
-
function buildTree(statements, styles, defaults) {
|
|
136
|
+
function buildTree(statements, styles, defaults, icons) {
|
|
109
137
|
const nodes = [];
|
|
110
138
|
const byName = new Map();
|
|
111
139
|
const roots = [];
|
|
@@ -140,7 +168,7 @@ function buildTree(statements, styles, defaults) {
|
|
|
140
168
|
// Weakest first: every node's default, then the leaf's or the container's,
|
|
141
169
|
// then the node's own styles and words over both.
|
|
142
170
|
const appearance = over(over(defaults.get('node') ?? {}, defaults.get(parents.has(stmt.name) ? 'container' : 'leaf') ?? {}), appearanceOf(stmt.attrs, styles, stmt.line));
|
|
143
|
-
const body = bodyFor(stmt.attrs, appearance, stmt.line);
|
|
171
|
+
const body = bodyFor(stmt.attrs, appearance, stmt.line, icons);
|
|
144
172
|
const kind = KIND_OF_BODY[body.kind];
|
|
145
173
|
// A node with no text of its own is labelled with its name, because the
|
|
146
174
|
// first lines anybody types are `node a` and `node b right of a` and they
|
|
@@ -208,7 +236,7 @@ function buildTree(statements, styles, defaults) {
|
|
|
208
236
|
// the other kinds refuse the word in `checkAttrs`, and a style's is unused.
|
|
209
237
|
const named = appearance['badge'];
|
|
210
238
|
if (named !== undefined && body.kind === 'shape') {
|
|
211
|
-
const badge = badgeChild(node, named);
|
|
239
|
+
const badge = badgeChild(node, named, icons);
|
|
212
240
|
badges.set(badge.name, node.name);
|
|
213
241
|
node.children.push(badge);
|
|
214
242
|
nodes.push(badge);
|
|
@@ -218,13 +246,13 @@ function buildTree(statements, styles, defaults) {
|
|
|
218
246
|
return { nodes, byName, roots };
|
|
219
247
|
}
|
|
220
248
|
/** The child `badge:` writes out, as `buildTree` describes. */
|
|
221
|
-
function badgeChild(parent, named) {
|
|
249
|
+
function badgeChild(parent, named, icons) {
|
|
222
250
|
const icon = { icon: named };
|
|
223
251
|
const size = parent.textAttrs['size'];
|
|
224
252
|
return {
|
|
225
253
|
name: `${parent.name}.badge`,
|
|
226
254
|
kind: 'icon',
|
|
227
|
-
body: bodyFor(icon, icon, parent.line),
|
|
255
|
+
body: bodyFor(icon, icon, parent.line, icons),
|
|
228
256
|
text: '',
|
|
229
257
|
lines: linesFor('', {}, `"${parent.name}.badge"`, parent.line),
|
|
230
258
|
children: [],
|