reladraw 0.16.0 → 0.17.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 +11 -7
- package/SYNTAX.md +810 -680
- package/dist/ast.d.ts +342 -18
- package/dist/ast.js +138 -27
- package/dist/checks.d.ts +58 -0
- package/dist/checks.js +891 -0
- package/dist/cli.js +88 -12
- package/dist/constants.d.ts +15 -55
- package/dist/constants.js +30 -78
- package/dist/grammar.d.ts +2 -2
- package/dist/grammar.js +70 -10
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -1
- package/dist/lines.d.ts +96 -0
- package/dist/lines.js +1968 -0
- package/dist/linetexts.d.ts +44 -0
- package/dist/linetexts.js +412 -0
- package/dist/model.d.ts +187 -36
- package/dist/model.js +23 -1
- package/dist/parser.js +1777 -107
- package/dist/place.d.ts +501 -0
- package/dist/place.js +2226 -0
- package/dist/render.d.ts +3 -1
- package/dist/render.js +691 -2493
- package/dist/resolve.d.ts +1 -1
- package/dist/resolve.js +1195 -1789
- package/dist/size.d.ts +64 -0
- package/dist/size.js +222 -0
- package/dist/themes.d.ts +17 -0
- package/dist/themes.js +28 -3
- package/package.json +2 -2
- package/dist/constrain.d.ts +0 -56
- package/dist/constrain.js +0 -95
package/dist/cli.js
CHANGED
|
@@ -1,31 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, resolve as resolvePath } from 'node:path';
|
|
5
|
+
import { nearMatch, readChecks } from './checks.js';
|
|
5
6
|
import { SourceError } from './errors.js';
|
|
6
|
-
import {
|
|
7
|
+
import { draw } from './index.js';
|
|
7
8
|
import { THEME_NAMES, THEMES } from './themes.js';
|
|
8
|
-
const USAGE = `reladraw —
|
|
9
|
+
const USAGE = `reladraw — draw a diagram from stated placement
|
|
9
10
|
|
|
10
|
-
reladraw <input.reladraw> [-o <output.svg>] [
|
|
11
|
+
reladraw render <input.reladraw> [-o <output.svg>] [options]
|
|
12
|
+
reladraw check <input.reladraw> [options]
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
render draw the diagram to an SVG, and print any findings: places
|
|
15
|
+
where the picture, drawn as written, may not read as meant.
|
|
16
|
+
check lay the diagram out and print only the findings. No SVG is
|
|
17
|
+
written, so a good one is left alone while you iterate.
|
|
18
|
+
|
|
19
|
+
-o, --out where render writes the SVG. Defaults to the input path with
|
|
13
20
|
its extension replaced by .svg. Use - for standard output.
|
|
14
21
|
--theme render in this theme, whatever the file's \`diagram theme:\`
|
|
15
22
|
says. One of ${THEME_NAMES.join(', ')}.
|
|
23
|
+
--guides draw the guides: everything written with \`guide\` in front,
|
|
24
|
+
dashed, to see how the picture is built.
|
|
25
|
+
--checks turn kinds of finding on or off, over what the file says:
|
|
26
|
+
--checks "edge-crossings: on, node-overlap: off"
|
|
27
|
+
--no-checks print no findings. Errors still print.
|
|
16
28
|
-h, --help print this.
|
|
17
29
|
`;
|
|
30
|
+
const COMMANDS = ['render', 'check'];
|
|
18
31
|
async function main(argv) {
|
|
19
32
|
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
|
|
20
33
|
process.stdout.write(USAGE);
|
|
21
34
|
return argv.length === 0 ? 1 : 0;
|
|
22
35
|
}
|
|
36
|
+
const verb = argv[0];
|
|
37
|
+
if (!COMMANDS.includes(verb)) {
|
|
38
|
+
// A file where the command goes is the old form, and is told the new one;
|
|
39
|
+
// anything else is a command that does not exist, with the near one.
|
|
40
|
+
if (/\.reladraw$/.test(verb) || existsSync(verb)) {
|
|
41
|
+
process.stderr.write(`reladraw: a command comes first — reladraw render ${argv.join(' ')}\n`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const near = nearMatch(verb, COMMANDS);
|
|
45
|
+
process.stderr.write(`reladraw: there is no command "${verb}"` +
|
|
46
|
+
(near ? ` — did you mean ${near}?\n` : ` — the commands are ${COMMANDS.join(', ')}\n`));
|
|
47
|
+
}
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const command = verb;
|
|
23
51
|
let input;
|
|
24
52
|
let out;
|
|
25
53
|
let theme;
|
|
26
|
-
|
|
54
|
+
let checks;
|
|
55
|
+
let noChecks = false;
|
|
56
|
+
let guides = false;
|
|
57
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
27
58
|
const arg = argv[i];
|
|
28
|
-
if (arg === '-o' || arg === '--out') {
|
|
59
|
+
if ((arg === '-o' || arg === '--out') && command === 'render') {
|
|
29
60
|
out = argv[i + 1];
|
|
30
61
|
if (out === undefined) {
|
|
31
62
|
process.stderr.write('reladraw: -o needs a path\n');
|
|
@@ -33,6 +64,10 @@ async function main(argv) {
|
|
|
33
64
|
}
|
|
34
65
|
i += 1;
|
|
35
66
|
}
|
|
67
|
+
else if (arg === '-o' || arg === '--out') {
|
|
68
|
+
process.stderr.write('reladraw: check writes no SVG, so it takes no -o; reladraw render writes one\n');
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
36
71
|
else if (arg === '--theme') {
|
|
37
72
|
theme = argv[i + 1];
|
|
38
73
|
if (theme === undefined) {
|
|
@@ -45,6 +80,20 @@ async function main(argv) {
|
|
|
45
80
|
}
|
|
46
81
|
i += 1;
|
|
47
82
|
}
|
|
83
|
+
else if (arg === '--checks') {
|
|
84
|
+
checks = argv[i + 1];
|
|
85
|
+
if (checks === undefined) {
|
|
86
|
+
process.stderr.write('reladraw: --checks needs a list, such as --checks "edge-crossings: on"\n');
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
i += 1;
|
|
90
|
+
}
|
|
91
|
+
else if (arg === '--no-checks') {
|
|
92
|
+
noChecks = true;
|
|
93
|
+
}
|
|
94
|
+
else if (arg === '--guides') {
|
|
95
|
+
guides = true;
|
|
96
|
+
}
|
|
48
97
|
else if (arg.startsWith('-') && arg !== '-') {
|
|
49
98
|
process.stderr.write(`reladraw: unknown option ${arg}\n`);
|
|
50
99
|
return 1;
|
|
@@ -58,17 +107,40 @@ async function main(argv) {
|
|
|
58
107
|
}
|
|
59
108
|
}
|
|
60
109
|
if (input === undefined) {
|
|
61
|
-
process.stderr.write(
|
|
110
|
+
process.stderr.write(`reladraw: no input file — reladraw ${command} <input.reladraw>\n`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
if (noChecks && checks !== undefined) {
|
|
114
|
+
process.stderr.write('reladraw: --no-checks and --checks say opposite things; give one\n');
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
if (noChecks && command === 'check') {
|
|
118
|
+
process.stderr.write('reladraw: check with --no-checks would print nothing; reladraw render draws\n');
|
|
62
119
|
return 1;
|
|
63
120
|
}
|
|
121
|
+
// Read once here, so a mistake in --checks is reported before the file is drawn.
|
|
122
|
+
let overChecks;
|
|
123
|
+
if (checks !== undefined) {
|
|
124
|
+
const written = checks;
|
|
125
|
+
try {
|
|
126
|
+
readChecks(written, {});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
process.stderr.write(`reladraw: --checks: ${error.message}\n`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
overChecks = (fromFile) => readChecks(written, fromFile);
|
|
133
|
+
}
|
|
64
134
|
const source = await readFile(input, 'utf8');
|
|
65
|
-
let
|
|
135
|
+
let drawn;
|
|
66
136
|
try {
|
|
67
|
-
|
|
137
|
+
drawn = draw(source, {
|
|
68
138
|
...(theme === undefined ? {} : { theme: THEMES[theme] }),
|
|
139
|
+
...(guides ? { guides: true } : {}),
|
|
69
140
|
// An icon's file is found from the diagram, not from wherever the tool
|
|
70
141
|
// happens to be run.
|
|
71
142
|
readIconFile: (path) => readFileSync(resolvePath(dirname(input), path), 'utf8'),
|
|
143
|
+
...(noChecks ? { checks: false } : overChecks ? { checks: overChecks } : {}),
|
|
72
144
|
});
|
|
73
145
|
}
|
|
74
146
|
catch (error) {
|
|
@@ -78,13 +150,17 @@ async function main(argv) {
|
|
|
78
150
|
}
|
|
79
151
|
throw error;
|
|
80
152
|
}
|
|
153
|
+
for (const finding of drawn.findings)
|
|
154
|
+
process.stderr.write(`${finding.format(input)}\n`);
|
|
155
|
+
if (command === 'check')
|
|
156
|
+
return 0;
|
|
81
157
|
if (out === '-') {
|
|
82
|
-
process.stdout.write(svg);
|
|
158
|
+
process.stdout.write(drawn.svg);
|
|
83
159
|
return 0;
|
|
84
160
|
}
|
|
85
161
|
const target = resolvePath(out ?? input.replace(/\.[^.]+$/, '') + '.svg');
|
|
86
162
|
await mkdir(dirname(target), { recursive: true });
|
|
87
|
-
await writeFile(target, svg, 'utf8');
|
|
163
|
+
await writeFile(target, drawn.svg, 'utf8');
|
|
88
164
|
process.stderr.write(`${target}\n`);
|
|
89
165
|
return 0;
|
|
90
166
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -4,21 +4,24 @@ import type { Measurer } from './measure.js';
|
|
|
4
4
|
import { type Line } from './text.js';
|
|
5
5
|
/** Inside a box, between its border and its contents. */
|
|
6
6
|
export declare const PAD = 14;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The file's unit, in pixels: about a quarter of an ordinary one-line box, so
|
|
9
|
+
* the gap words land near whole numbers. Findings measure in it. Provisional
|
|
10
|
+
* until the gap words are measured against it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const UNIT = 12;
|
|
9
13
|
/** Between a container's own text and its first child. */
|
|
10
14
|
export declare const HEADER_GAP = 10;
|
|
11
15
|
/**
|
|
12
|
-
* How far each deck
|
|
13
|
-
* whole line of text plus the padding above it, or a
|
|
14
|
-
* then immediately covered by the
|
|
16
|
+
* How far each card of a deck sits up and to the left of the one in front. It
|
|
17
|
+
* has to clear a whole line of text plus the padding above it, or a card's
|
|
18
|
+
* text is drawn and then immediately covered by the card in front of it.
|
|
15
19
|
*/
|
|
16
20
|
export declare const DECK_STEP = 34;
|
|
17
21
|
/**
|
|
18
|
-
* The named gaps a placement may ask for, each
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* to leave room for something else.
|
|
22
|
+
* The named gaps a placement may ask for, each an exact distance in whole
|
|
23
|
+
* units. Chosen 2026-10-14 against today's spacing: 2, 4 and 8 units keep the
|
|
24
|
+
* old look, where 1, 2 and 3 left a line barely longer than its arrowhead.
|
|
22
25
|
*/
|
|
23
26
|
export declare const GAPS: Record<string, number>;
|
|
24
27
|
/**
|
|
@@ -59,43 +62,6 @@ export declare const THICKNESS: Record<string, number>;
|
|
|
59
62
|
* thickness is a size, like a text's, and a size is allowed to take room.
|
|
60
63
|
*/
|
|
61
64
|
export declare function arrowLength(thickness: number): number;
|
|
62
|
-
/**
|
|
63
|
-
* Line left showing behind the head of an edge with no text, so a gap an arrow
|
|
64
|
-
* crosses is at least this plus its heads. Stacked children sit `CHILD_GAP`
|
|
65
|
-
* apart, which is less than one head, and an arrow between them drew as a head
|
|
66
|
-
* with no line. This is what the tightest named gap already leaves behind a
|
|
67
|
-
* normal head, so any gap an author could have asked for holds it unchanged.
|
|
68
|
-
*/
|
|
69
|
-
export declare const BARE_EDGE_RUN: number;
|
|
70
|
-
/**
|
|
71
|
-
* Line left showing between an edge's text and the box at that end of the
|
|
72
|
-
* corridor it crosses.
|
|
73
|
-
*
|
|
74
|
-
* Unlike `SEPARATION_GAP` and `ATTACH_MARGIN` this is not "small on purpose".
|
|
75
|
-
* Those two keep two things from touching, and the least distance that reads as
|
|
76
|
-
* "not touching" is the right one. This one has something to show: a text sits
|
|
77
|
-
* in a knockout that erases the line behind it, so whatever is left either side
|
|
78
|
-
* is the entire evidence that the text belongs to an edge at all. At ten pixels
|
|
79
|
-
* it did not read as a line — the seed diagram in the playground drew as a word
|
|
80
|
-
* with a dash beside it — so it is the length of a run of line, not a margin.
|
|
81
|
-
*
|
|
82
|
-
* Say `gap:` if you want the corridor wider than its contents.
|
|
83
|
-
*/
|
|
84
|
-
export declare const TEXT_CLEARANCE = 20;
|
|
85
|
-
/**
|
|
86
|
-
* How much room an edge's text takes along one axis.
|
|
87
|
-
*
|
|
88
|
-
* The knockout rectangle drawn behind a text is the text plus five either side,
|
|
89
|
-
* so that rectangle, not the glyphs, is what must not overlap anything.
|
|
90
|
-
*
|
|
91
|
-
* Shared by the resolver, which widens a corridor to hold a text, and the
|
|
92
|
-
* renderer, which spaces the lanes of a channel by it, so the two cannot
|
|
93
|
-
* disagree about how much room a text needs. The two ask different questions of
|
|
94
|
-
* it and both are right: the resolver measures *along* the run, so an edge
|
|
95
|
-
* traveling horizontally needs the text's width; the renderer measures *across*
|
|
96
|
-
* the channel, so an edge traveling horizontally down one needs its height.
|
|
97
|
-
*/
|
|
98
|
-
export declare function textExtent(lines: Line[], textAttrs: Attrs, axis: 'x' | 'y', measurer: Measurer, fontSize: number, line: number): number;
|
|
99
65
|
/**
|
|
100
66
|
* The widest of these lines, which is how wide the block of them is. Shared by
|
|
101
67
|
* the resolver, which sizes a node to hold its text, and the renderer, which
|
|
@@ -116,9 +82,9 @@ export declare const ICON_GAP = 10;
|
|
|
116
82
|
export declare const DEFAULT_FONT_SIZE = 14;
|
|
117
83
|
/**
|
|
118
84
|
* The named text sizes, each a multiple of the document's own size. A plain
|
|
119
|
-
* number
|
|
120
|
-
* diagram moving can make
|
|
121
|
-
*
|
|
85
|
+
* number is a multiple too, and `16px` a size in pixels: nothing else in the
|
|
86
|
+
* diagram moving can make either wrong. The names stay preferred because they
|
|
87
|
+
* say why one text is bigger than another.
|
|
122
88
|
*
|
|
123
89
|
* `small` is sampled rather than chosen. In
|
|
124
90
|
* `examples/reference/arch.png` the box and container texts run 25
|
|
@@ -169,9 +135,3 @@ export interface TextStyle {
|
|
|
169
135
|
* cannot disagree about which end the band is at.
|
|
170
136
|
*/
|
|
171
137
|
export declare function textStyleFor(attrs: Attrs, line: number, fallbackAlign?: 'start' | 'middle', fallbackAt?: Position): TextStyle;
|
|
172
|
-
/**
|
|
173
|
-
* Where a leaf's text or badge starts vertically, given which end it sits at.
|
|
174
|
-
* Shared, because the resolver works out the text's box and the renderer draws
|
|
175
|
-
* it, and the two must not be able to disagree.
|
|
176
|
-
*/
|
|
177
|
-
export declare function leafTop(end: 'top' | 'center' | 'bottom', y: number, height: number, own: number): number;
|
package/dist/constants.js
CHANGED
|
@@ -3,29 +3,32 @@ import { SourceError } from './errors.js';
|
|
|
3
3
|
import { plain } from './text.js';
|
|
4
4
|
/** Inside a box, between its border and its contents. */
|
|
5
5
|
export const PAD = 14;
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The file's unit, in pixels: about a quarter of an ordinary one-line box, so
|
|
8
|
+
* the gap words land near whole numbers. Findings measure in it. Provisional
|
|
9
|
+
* until the gap words are measured against it.
|
|
10
|
+
*/
|
|
11
|
+
export const UNIT = 12;
|
|
8
12
|
/** Between a container's own text and its first child. */
|
|
9
13
|
export const HEADER_GAP = 10;
|
|
10
14
|
/**
|
|
11
|
-
* How far each deck
|
|
12
|
-
* whole line of text plus the padding above it, or a
|
|
13
|
-
* then immediately covered by the
|
|
15
|
+
* How far each card of a deck sits up and to the left of the one in front. It
|
|
16
|
+
* has to clear a whole line of text plus the padding above it, or a card's
|
|
17
|
+
* text is drawn and then immediately covered by the card in front of it.
|
|
14
18
|
*/
|
|
15
19
|
export const DECK_STEP = 34;
|
|
16
20
|
/**
|
|
17
|
-
* The named gaps a placement may ask for, each
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* to leave room for something else.
|
|
21
|
+
* The named gaps a placement may ask for, each an exact distance in whole
|
|
22
|
+
* units. Chosen 2026-10-14 against today's spacing: 2, 4 and 8 units keep the
|
|
23
|
+
* old look, where 1, 2 and 3 left a line barely longer than its arrowhead.
|
|
21
24
|
*/
|
|
22
25
|
export const GAPS = {
|
|
23
26
|
// A zero gap turns an offset into edge-to-edge contact, so "my top edge
|
|
24
27
|
// against Docker's bottom edge" needs no vocabulary of its own.
|
|
25
28
|
none: 0,
|
|
26
|
-
tight:
|
|
27
|
-
normal:
|
|
28
|
-
wide:
|
|
29
|
+
tight: 2 * UNIT,
|
|
30
|
+
normal: 4 * UNIT,
|
|
31
|
+
wide: 8 * UNIT,
|
|
29
32
|
};
|
|
30
33
|
/**
|
|
31
34
|
* How far apart two boxes are pushed when they would otherwise overlap. Small
|
|
@@ -71,47 +74,6 @@ export const THICKNESS = {
|
|
|
71
74
|
export function arrowLength(thickness) {
|
|
72
75
|
return ARROW_MARKER_WIDTH * thickness;
|
|
73
76
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Line left showing behind the head of an edge with no text, so a gap an arrow
|
|
76
|
-
* crosses is at least this plus its heads. Stacked children sit `CHILD_GAP`
|
|
77
|
-
* apart, which is less than one head, and an arrow between them drew as a head
|
|
78
|
-
* with no line. This is what the tightest named gap already leaves behind a
|
|
79
|
-
* normal head, so any gap an author could have asked for holds it unchanged.
|
|
80
|
-
*/
|
|
81
|
-
export const BARE_EDGE_RUN = GAPS['tight'] - ARROW_LENGTH;
|
|
82
|
-
/**
|
|
83
|
-
* Line left showing between an edge's text and the box at that end of the
|
|
84
|
-
* corridor it crosses.
|
|
85
|
-
*
|
|
86
|
-
* Unlike `SEPARATION_GAP` and `ATTACH_MARGIN` this is not "small on purpose".
|
|
87
|
-
* Those two keep two things from touching, and the least distance that reads as
|
|
88
|
-
* "not touching" is the right one. This one has something to show: a text sits
|
|
89
|
-
* in a knockout that erases the line behind it, so whatever is left either side
|
|
90
|
-
* is the entire evidence that the text belongs to an edge at all. At ten pixels
|
|
91
|
-
* it did not read as a line — the seed diagram in the playground drew as a word
|
|
92
|
-
* with a dash beside it — so it is the length of a run of line, not a margin.
|
|
93
|
-
*
|
|
94
|
-
* Say `gap:` if you want the corridor wider than its contents.
|
|
95
|
-
*/
|
|
96
|
-
export const TEXT_CLEARANCE = 20;
|
|
97
|
-
/**
|
|
98
|
-
* How much room an edge's text takes along one axis.
|
|
99
|
-
*
|
|
100
|
-
* The knockout rectangle drawn behind a text is the text plus five either side,
|
|
101
|
-
* so that rectangle, not the glyphs, is what must not overlap anything.
|
|
102
|
-
*
|
|
103
|
-
* Shared by the resolver, which widens a corridor to hold a text, and the
|
|
104
|
-
* renderer, which spaces the lanes of a channel by it, so the two cannot
|
|
105
|
-
* disagree about how much room a text needs. The two ask different questions of
|
|
106
|
-
* it and both are right: the resolver measures *along* the run, so an edge
|
|
107
|
-
* traveling horizontally needs the text's width; the renderer measures *across*
|
|
108
|
-
* the channel, so an edge traveling horizontally down one needs its height.
|
|
109
|
-
*/
|
|
110
|
-
export function textExtent(lines, textAttrs, axis, measurer, fontSize, line) {
|
|
111
|
-
const size = fontSizeFor('edge', textAttrs, fontSize, line);
|
|
112
|
-
const width = lines.reduce((widest, drawn) => Math.max(widest, measurer.measure(plain(drawn), size).width), 0);
|
|
113
|
-
return axis === 'x' ? width + 10 : lines.length * measurer.lineHeight(size);
|
|
114
|
-
}
|
|
115
77
|
/**
|
|
116
78
|
* The widest of these lines, which is how wide the block of them is. Shared by
|
|
117
79
|
* the resolver, which sizes a node to hold its text, and the renderer, which
|
|
@@ -134,9 +96,9 @@ export const ICON_GAP = 10;
|
|
|
134
96
|
export const DEFAULT_FONT_SIZE = 14;
|
|
135
97
|
/**
|
|
136
98
|
* The named text sizes, each a multiple of the document's own size. A plain
|
|
137
|
-
* number
|
|
138
|
-
* diagram moving can make
|
|
139
|
-
*
|
|
99
|
+
* number is a multiple too, and `16px` a size in pixels: nothing else in the
|
|
100
|
+
* diagram moving can make either wrong. The names stay preferred because they
|
|
101
|
+
* say why one text is bigger than another.
|
|
140
102
|
*
|
|
141
103
|
* `small` is sampled rather than chosen. In
|
|
142
104
|
* `examples/reference/arch.png` the box and container texts run 25
|
|
@@ -168,15 +130,13 @@ export function fontSizeFor(kind, textAttrs, fontSize, line) {
|
|
|
168
130
|
const scale = TEXT_SIZES[named];
|
|
169
131
|
if (scale !== undefined)
|
|
170
132
|
return Math.round(fontSize * scale);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: '';
|
|
179
|
-
throw new SourceError(`size takes ${Object.keys(TEXT_SIZES).join(', ')} or a number of pixels, not "${named}"${hint}`, line);
|
|
133
|
+
// A bare number is a multiple of normal; `px` fixes it in pixels (design.org 3.8).
|
|
134
|
+
const number = /^(\d*\.?\d+)(px)?$/.exec(named);
|
|
135
|
+
if (number && Number(number[1]) > 0) {
|
|
136
|
+
return number[2] ? Number(number[1]) : Math.round(fontSize * Number(number[1]));
|
|
137
|
+
}
|
|
138
|
+
const hint = named.startsWith('-') || /^0*\.?0+(px)?$/.test(named) ? '; a text size has to be more than zero' : '';
|
|
139
|
+
throw new SourceError(`size takes ${Object.keys(TEXT_SIZES).join(', ')}, a multiple of normal such as 1.2, or pixels such as 16px, not "${named}"${hint}`, line);
|
|
180
140
|
}
|
|
181
141
|
export const DEFAULT_MARGIN = 40;
|
|
182
142
|
/**
|
|
@@ -206,7 +166,11 @@ export function textStyleFor(attrs, line, fallbackAlign = 'start', fallbackAt =
|
|
|
206
166
|
: `a text's at takes one of ${POSITIONS.join(', ')}, not "${written}"`, line);
|
|
207
167
|
}
|
|
208
168
|
const at = written ?? fallbackAt;
|
|
209
|
-
|
|
169
|
+
// On a left or right side, align: says where along it (top, center or
|
|
170
|
+
// bottom), which the resolver has checked; the lines keep their default.
|
|
171
|
+
const onLeftOrRight = attrs['place']?.endsWith(' left') === true || attrs['place']?.endsWith(' right') === true;
|
|
172
|
+
const along = onLeftOrRight && (attrs['align'] === 'top' || attrs['align'] === 'bottom');
|
|
173
|
+
const align = along ? undefined : attrs['align'];
|
|
210
174
|
if (align !== undefined && TEXT_ALIGNMENTS[align] === undefined) {
|
|
211
175
|
throw new SourceError(`a text's align takes left, center or right, not "${align}"`, line);
|
|
212
176
|
}
|
|
@@ -218,15 +182,3 @@ export function textStyleFor(attrs, line, fallbackAlign = 'start', fallbackAt =
|
|
|
218
182
|
align: align === undefined ? fallbackAlign : TEXT_ALIGNMENTS[align],
|
|
219
183
|
};
|
|
220
184
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Where a leaf's text or badge starts vertically, given which end it sits at.
|
|
223
|
-
* Shared, because the resolver works out the text's box and the renderer draws
|
|
224
|
-
* it, and the two must not be able to disagree.
|
|
225
|
-
*/
|
|
226
|
-
export function leafTop(end, y, height, own) {
|
|
227
|
-
if (end === 'top')
|
|
228
|
-
return y + PAD;
|
|
229
|
-
if (end === 'bottom')
|
|
230
|
-
return y + height - PAD - own;
|
|
231
|
-
return y + (height - own) / 2;
|
|
232
|
-
}
|
package/dist/grammar.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type TokenKind =
|
|
|
40
40
|
| 'name'
|
|
41
41
|
/** `->`, `<-`, `<->`, and `--` or `-` for a plain line. */
|
|
42
42
|
| 'arrow'
|
|
43
|
-
/** Placement and
|
|
43
|
+
/** Placement and line vocabulary: `right`, `of`, `even`, `with`, `and`, `between`, `via`'s stops, … */
|
|
44
44
|
| 'relation'
|
|
45
45
|
/** A `key:` opening an attribute or a bracketed modifier. */
|
|
46
46
|
| 'attribute'
|
|
@@ -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", "icon"];
|
|
67
|
+
export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "style", "diagram", "default", "icon", "point", "region", "bundle", "guide"];
|
|
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
|
package/dist/grammar.js
CHANGED
|
@@ -26,16 +26,23 @@
|
|
|
26
26
|
* editor formats need and the reason a `.reladraw` grammar is small in all of
|
|
27
27
|
* them.
|
|
28
28
|
*/
|
|
29
|
-
import { DIRECTIONS, MARKS, POSITIONS, SIDES
|
|
29
|
+
import { DEFAULT_TARGETS, DIRECTIONS, MARKS, PARTS, POINT_TEXT_SIDES, POSITIONS, SIDES } from './ast.js';
|
|
30
30
|
/**
|
|
31
31
|
* The words a statement may open with. `parseStatement` in `parser.ts` is the
|
|
32
32
|
* authority — its switch is what actually accepts them — and this list mirrors
|
|
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', 'icon'];
|
|
36
|
+
export const STATEMENT_KEYWORDS = ['node', 'edge', 'style', 'diagram', 'default', 'icon', 'point', 'region', 'bundle', 'guide'];
|
|
37
37
|
/** Statements whose second word declares a name. `diagram` has none. */
|
|
38
|
-
const DECLARES_NAME = ['node', 'style', 'icon'];
|
|
38
|
+
const DECLARES_NAME = ['node', 'style', 'icon', 'point', 'region', 'bundle'];
|
|
39
|
+
/**
|
|
40
|
+
* Keywords that take another after them: `guide region channel`, `default
|
|
41
|
+
* leaf`, `default guide edge`. The words that may follow are the kinds a
|
|
42
|
+
* default is written for, so `leaf` and `container` color as keywords there.
|
|
43
|
+
*/
|
|
44
|
+
const LEADS = ['guide', 'default'];
|
|
45
|
+
const FOLLOWERS = [...new Set(DEFAULT_TARGETS.flatMap((target) => target.split(' ')))];
|
|
39
46
|
/**
|
|
40
47
|
* Every word that says something about where a thing goes. Assembled from the
|
|
41
48
|
* lists the parser itself reads, so a direction or a passage axis added there
|
|
@@ -45,19 +52,45 @@ export const RELATION_WORDS = [
|
|
|
45
52
|
...DIRECTIONS,
|
|
46
53
|
...SIDES,
|
|
47
54
|
...POSITIONS,
|
|
48
|
-
|
|
55
|
+
// `tall text`, `server top-left`: the part of a node a placement names.
|
|
56
|
+
...PARTS,
|
|
57
|
+
...POINT_TEXT_SIDES,
|
|
49
58
|
// The connecting words. `of` is optional after a direction, `and` joins
|
|
50
|
-
// targets, `between`
|
|
51
|
-
// `on ... at ...` is the overlay. Note `of` and `at` are what
|
|
52
|
-
// position vocabularies apart: a direction is always followed
|
|
53
|
-
// named position always preceded by `at`.
|
|
59
|
+
// targets, `between` names the point between two things, `even with` is the
|
|
60
|
+
// alignment, and `on ... at ...` is the overlay. Note `of` and `at` are what
|
|
61
|
+
// tell the two position vocabularies apart: a direction is always followed
|
|
62
|
+
// by `of`, a named position always preceded by `at`.
|
|
54
63
|
'of',
|
|
55
64
|
'at',
|
|
56
65
|
'on',
|
|
57
66
|
'and',
|
|
58
|
-
'
|
|
67
|
+
'even',
|
|
59
68
|
'with',
|
|
60
69
|
'between',
|
|
70
|
+
// Points (design.org 4): a fraction of the way, a point from two things,
|
|
71
|
+
// a region's corners.
|
|
72
|
+
'down',
|
|
73
|
+
'up',
|
|
74
|
+
'across',
|
|
75
|
+
'from',
|
|
76
|
+
'to',
|
|
77
|
+
// `(inside top)`, `outside router right`, `via: inside p top`.
|
|
78
|
+
'inside',
|
|
79
|
+
'outside',
|
|
80
|
+
// Lines (design.org 6): `straight down from z3`, `each child of pool`,
|
|
81
|
+
// `leg climb`, `via: through slot horizontal`.
|
|
82
|
+
'straight',
|
|
83
|
+
'each',
|
|
84
|
+
'child',
|
|
85
|
+
'leg',
|
|
86
|
+
'through',
|
|
87
|
+
'horizontal',
|
|
88
|
+
'vertical',
|
|
89
|
+
// A line's text (design.org 7.3): `(left of edge)`, `(on: longest level)`.
|
|
90
|
+
// `edge` here is the line the text belongs to; at the start of a line it is
|
|
91
|
+
// still the keyword, since the keyword is matched first.
|
|
92
|
+
'edge',
|
|
93
|
+
'level',
|
|
61
94
|
];
|
|
62
95
|
/** Longest first, so `above-left` is not read as `above` followed by `-left`. */
|
|
63
96
|
function alternation(words) {
|
|
@@ -137,6 +170,11 @@ const COMMENT = sticky(PATTERNS.comment);
|
|
|
137
170
|
const KEYWORD = sticky(PATTERNS.keyword);
|
|
138
171
|
const WORD = sticky(PATTERNS.word);
|
|
139
172
|
const WORD_IN_GROUP = sticky(PATTERNS.wordInGroup);
|
|
173
|
+
const ARROW = new RegExp(`^${PATTERNS.arrow}$`);
|
|
174
|
+
/** Whether a whole word is an arrow, as the parser's `looksLikeArrow` asks. */
|
|
175
|
+
function isArrow(word) {
|
|
176
|
+
return ARROW.test(word);
|
|
177
|
+
}
|
|
140
178
|
function sticky(source) {
|
|
141
179
|
return new RegExp(source, 'y');
|
|
142
180
|
}
|
|
@@ -178,9 +216,21 @@ export function highlightLine(line) {
|
|
|
178
216
|
}
|
|
179
217
|
if (opening) {
|
|
180
218
|
opening = false;
|
|
181
|
-
|
|
219
|
+
let keyword = match(KEYWORD, line, at);
|
|
182
220
|
if (keyword !== null) {
|
|
183
221
|
push('keyword', at + keyword.length);
|
|
222
|
+
// `default guide edge` leads twice, so this loops.
|
|
223
|
+
while (LEADS.includes(keyword)) {
|
|
224
|
+
const space = match(SPACE, line, at);
|
|
225
|
+
if (space === null)
|
|
226
|
+
break;
|
|
227
|
+
const next = match(WORD, line, at + space.length);
|
|
228
|
+
if (next === null || !FOLLOWERS.includes(next))
|
|
229
|
+
break;
|
|
230
|
+
push('plain', at + space.length);
|
|
231
|
+
push('keyword', at + next.length);
|
|
232
|
+
keyword = next;
|
|
233
|
+
}
|
|
184
234
|
if (DECLARES_NAME.includes(keyword)) {
|
|
185
235
|
const space = match(SPACE, line, at);
|
|
186
236
|
if (space !== null) {
|
|
@@ -193,6 +243,16 @@ export function highlightLine(line) {
|
|
|
193
243
|
push('name', at + name.length);
|
|
194
244
|
}
|
|
195
245
|
}
|
|
246
|
+
else if (keyword === 'edge') {
|
|
247
|
+
// `edge req client -> api`: a word before the ends names the line,
|
|
248
|
+
// told apart by the arrow coming third rather than second, which is
|
|
249
|
+
// the parser's own test.
|
|
250
|
+
const head = /^([ \t]+)([^\s"]+)[ \t]+([^\s"]+)[ \t]+(\S+)/.exec(line.slice(at));
|
|
251
|
+
if (head && !isArrow(head[3]) && isArrow(head[4]) && !RELATION_WORDS.includes(head[2])) {
|
|
252
|
+
push('plain', at + head[1].length);
|
|
253
|
+
push('name', at + head[2].length);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
196
256
|
continue;
|
|
197
257
|
}
|
|
198
258
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './ast.js';
|
|
2
|
+
export * from './checks.js';
|
|
2
3
|
export * from './constants.js';
|
|
3
4
|
export * from './errors.js';
|
|
4
5
|
export * from './grammar.js';
|
|
@@ -8,6 +9,7 @@ export { parse } from './parser.js';
|
|
|
8
9
|
export { resolve, type ResolveOptions } from './resolve.js';
|
|
9
10
|
export { render, type RenderOptions } from './render.js';
|
|
10
11
|
export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES, type Theme } from './themes.js';
|
|
12
|
+
import { type Checks, type Finding } from './checks.js';
|
|
11
13
|
import { type RenderOptions } from './render.js';
|
|
12
14
|
import { type ResolveOptions } from './resolve.js';
|
|
13
15
|
/**
|
|
@@ -16,3 +18,21 @@ import { type ResolveOptions } from './resolve.js';
|
|
|
16
18
|
* the statement starts on; see `blame`.
|
|
17
19
|
*/
|
|
18
20
|
export declare function compile(source: string, options?: ResolveOptions & RenderOptions): string;
|
|
21
|
+
export interface DrawOptions extends ResolveOptions, RenderOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Which kinds of finding to look for. Absent: what the file's
|
|
24
|
+
* `diagram checks: (…)` says over the defaults. A function: given that, it
|
|
25
|
+
* returns the checks to run, which is how `--checks` lays itself over the
|
|
26
|
+
* file. `false`: none, as `--no-checks`.
|
|
27
|
+
*/
|
|
28
|
+
checks?: false | ((fromFile: Checks) => Checks);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Source text in; the SVG and the findings about it out. A finding never stops
|
|
32
|
+
* the drawing: it says where the picture, drawn as written, may not read as
|
|
33
|
+
* meant.
|
|
34
|
+
*/
|
|
35
|
+
export declare function draw(source: string, options?: DrawOptions): {
|
|
36
|
+
svg: string;
|
|
37
|
+
findings: Finding[];
|
|
38
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './ast.js';
|
|
2
|
+
export * from './checks.js';
|
|
2
3
|
export * from './constants.js';
|
|
3
4
|
export * from './errors.js';
|
|
4
5
|
export * from './grammar.js';
|
|
@@ -8,6 +9,7 @@ export { parse } from './parser.js';
|
|
|
8
9
|
export { resolve } from './resolve.js';
|
|
9
10
|
export { render } from './render.js';
|
|
10
11
|
export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES } from './themes.js';
|
|
12
|
+
import { fileChecks, findings } from './checks.js';
|
|
11
13
|
import { SourceError } from './errors.js';
|
|
12
14
|
import { blame, parse } from './parser.js';
|
|
13
15
|
import { render } from './render.js';
|
|
@@ -18,7 +20,22 @@ import { resolve } from './resolve.js';
|
|
|
18
20
|
* the statement starts on; see `blame`.
|
|
19
21
|
*/
|
|
20
22
|
export function compile(source, options = {}) {
|
|
21
|
-
|
|
23
|
+
return draw(source, { ...options, checks: false }).svg;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Source text in; the SVG and the findings about it out. A finding never stops
|
|
27
|
+
* the drawing: it says where the picture, drawn as written, may not read as
|
|
28
|
+
* meant.
|
|
29
|
+
*/
|
|
30
|
+
export function draw(source, options = {}) {
|
|
31
|
+
const run = (text) => {
|
|
32
|
+
const layout = resolve(parse(text), options);
|
|
33
|
+
const svg = render(layout, options);
|
|
34
|
+
if (options.checks === false)
|
|
35
|
+
return { svg, findings: [] };
|
|
36
|
+
const fromFile = fileChecks(layout);
|
|
37
|
+
return { svg, findings: findings(layout, options.checks ? options.checks(fromFile) : fromFile) };
|
|
38
|
+
};
|
|
22
39
|
try {
|
|
23
40
|
return run(source);
|
|
24
41
|
}
|