reladraw 0.0.1 → 0.1.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.
@@ -0,0 +1,56 @@
1
+ import type { Placement } from './ast.js';
2
+ /**
3
+ * The tightest arrangement satisfying a set of relative distances.
4
+ *
5
+ * Every placement in the language turns into the same shape of fact: one thing sits
6
+ * at least so far along an axis from another. Written as `pos[to] >= pos[from]
7
+ * + weight`, a whole diagram is a system of those, and the arrangement the
8
+ * author meant is the one where nothing is further apart than it has to be.
9
+ *
10
+ * That arrangement is unique and is found by longest paths, so no search
11
+ * happens and no alternative is ever weighed. Distances come out of the
12
+ * arithmetic; which side of what a thing sits on came from the author.
13
+ */
14
+ export interface Constraint {
15
+ from: number;
16
+ to: number;
17
+ weight: number;
18
+ /** The placement this came from, so a failure can be reported in the author's words. */
19
+ placement?: Placement;
20
+ }
21
+ /** An exact distance, which is two inequalities pointing opposite ways. */
22
+ export declare function fix(from: number, to: number, distance: number, placement?: Placement): Constraint[];
23
+ export interface Contradiction {
24
+ /** The placements that cannot all hold. Empty if the loop is entirely implicit. */
25
+ placements: Placement[];
26
+ }
27
+ /**
28
+ * Solve, or report the placements that fight.
29
+ *
30
+ * Longest path from a virtual source joined to everything at zero, which is
31
+ * Bellman-Ford with the comparison flipped. A distance that keeps growing after
32
+ * one pass per node means the constraints run in a circle that demands ever
33
+ * more room, and that circle is exactly the set of placements to quote back.
34
+ */
35
+ export declare function tightest(count: number, constraints: Constraint[]): {
36
+ positions: number[];
37
+ } | {
38
+ contradiction: Contradiction;
39
+ };
40
+ /**
41
+ * Which members can be driven apart from which, along one axis.
42
+ *
43
+ * A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
44
+ * below and leaves it free to move further away. Follow those edges and the
45
+ * question "may this one end up beyond that one?" becomes plain reachability:
46
+ * if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
47
+ * lets the distance from `i` to `j` grow without limit and never lets it be
48
+ * closed from the other side. Pushing `j` past `i` is then the one separation
49
+ * the file allows, and no choice was made.
50
+ *
51
+ * Reachable both ways means the two are pinned at a fixed distance, so they
52
+ * cannot be separated along this axis at all. Reachable neither way means the
53
+ * file said nothing that orders them, which is the case the caller must refuse
54
+ * rather than guess at.
55
+ */
56
+ export declare function reachability(count: number, constraints: Constraint[]): boolean[][];
@@ -0,0 +1,95 @@
1
+ /** An exact distance, which is two inequalities pointing opposite ways. */
2
+ export function fix(from, to, distance, placement) {
3
+ return [
4
+ { from, to, weight: distance, ...(placement ? { placement } : {}) },
5
+ { from: to, to: from, weight: -distance, ...(placement ? { placement } : {}) },
6
+ ];
7
+ }
8
+ /**
9
+ * Solve, or report the placements that fight.
10
+ *
11
+ * Longest path from a virtual source joined to everything at zero, which is
12
+ * Bellman-Ford with the comparison flipped. A distance that keeps growing after
13
+ * one pass per node means the constraints run in a circle that demands ever
14
+ * more room, and that circle is exactly the set of placements to quote back.
15
+ */
16
+ export function tightest(count, constraints) {
17
+ const positions = new Array(count).fill(0);
18
+ const cameFrom = new Array(count).fill(undefined);
19
+ for (let pass = 0; pass < count; pass += 1) {
20
+ let moved = false;
21
+ for (const constraint of constraints) {
22
+ const candidate = positions[constraint.from] + constraint.weight;
23
+ if (candidate > positions[constraint.to] + 1e-9) {
24
+ positions[constraint.to] = candidate;
25
+ cameFrom[constraint.to] = constraint;
26
+ moved = true;
27
+ }
28
+ }
29
+ if (!moved)
30
+ return { positions };
31
+ }
32
+ // Still moving after a full pass per node, so some loop demands more room
33
+ // every time round it. Walk backwards to find it.
34
+ for (const constraint of constraints) {
35
+ if (positions[constraint.from] + constraint.weight > positions[constraint.to] + 1e-9) {
36
+ return { contradiction: { placements: loopFrom(constraint.to, cameFrom, count) } };
37
+ }
38
+ }
39
+ return { contradiction: { placements: [] } };
40
+ }
41
+ /**
42
+ * Which members can be driven apart from which, along one axis.
43
+ *
44
+ * A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
45
+ * below and leaves it free to move further away. Follow those edges and the
46
+ * question "may this one end up beyond that one?" becomes plain reachability:
47
+ * if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
48
+ * lets the distance from `i` to `j` grow without limit and never lets it be
49
+ * closed from the other side. Pushing `j` past `i` is then the one separation
50
+ * the file allows, and no choice was made.
51
+ *
52
+ * Reachable both ways means the two are pinned at a fixed distance, so they
53
+ * cannot be separated along this axis at all. Reachable neither way means the
54
+ * file said nothing that orders them, which is the case the caller must refuse
55
+ * rather than guess at.
56
+ */
57
+ export function reachability(count, constraints) {
58
+ const reach = Array.from({ length: count }, () => new Array(count).fill(false));
59
+ for (const constraint of constraints)
60
+ reach[constraint.from][constraint.to] = true;
61
+ for (let via = 0; via < count; via += 1) {
62
+ for (let from = 0; from < count; from += 1) {
63
+ if (!reach[from][via])
64
+ continue;
65
+ for (let to = 0; to < count; to += 1) {
66
+ if (reach[via][to])
67
+ reach[from][to] = true;
68
+ }
69
+ }
70
+ }
71
+ return reach;
72
+ }
73
+ /** The placements on the loop reached by following each position back to what set it. */
74
+ function loopFrom(start, cameFrom, count) {
75
+ let at = start;
76
+ for (let step = 0; step < count; step += 1) {
77
+ const previous = cameFrom[at];
78
+ if (previous === undefined)
79
+ break;
80
+ at = previous.from;
81
+ }
82
+ const placements = [];
83
+ const seen = new Set();
84
+ let cursor = at;
85
+ while (!seen.has(cursor)) {
86
+ seen.add(cursor);
87
+ const previous = cameFrom[cursor];
88
+ if (previous === undefined)
89
+ break;
90
+ if (previous.placement)
91
+ placements.push(previous.placement);
92
+ cursor = previous.from;
93
+ }
94
+ return placements.reverse();
95
+ }
@@ -0,0 +1,7 @@
1
+ /** A problem in the source, reported in the vocabulary of the source. */
2
+ export declare class SourceError extends Error {
3
+ readonly line: number;
4
+ constructor(message: string, line: number);
5
+ /** `12: two placements for "server"` — the form the command-line tool prints. */
6
+ format(file?: string): string;
7
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,14 @@
1
+ /** A problem in the source, reported in the vocabulary of the source. */
2
+ export class SourceError extends Error {
3
+ line;
4
+ constructor(message, line) {
5
+ super(message);
6
+ this.line = line;
7
+ this.name = 'SourceError';
8
+ }
9
+ /** `12: two placements for "server"` — the form the command-line tool prints. */
10
+ format(file) {
11
+ const where = file ? `${file}:${this.line}` : `line ${this.line}`;
12
+ return `${where}: ${this.message}`;
13
+ }
14
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The language's *lexical* vocabulary, and a scanner that classifies one line of
3
+ * source into coloured spans.
4
+ *
5
+ * This is deliberately separate from `parser.ts`, and it is not a second parser.
6
+ * The parser answers "what does this file mean" and refuses anything it cannot
7
+ * answer for; a highlighter has to colour a half-typed line without complaint,
8
+ * so it answers only "what kind of word is this" and never fails. Every rule
9
+ * below is a regex applied to a single line, in priority order, with one bit of
10
+ * carried state (whether anything has been seen on the line yet, and whether the
11
+ * previous token was an attribute key).
12
+ *
13
+ * **That restriction is the point, and it is what makes this reusable.** An
14
+ * Emacs `font-lock-keywords` list, a Vim `syntax match` file and a TextMate
15
+ * grammar are all exactly this: an ordered list of single-line regexes with a
16
+ * face attached. So the vocabulary here — which is the half that goes stale, and
17
+ * which is imported from `ast.ts`, `constants.ts` and `icons.ts` rather than
18
+ * retyped — can be emitted into any of those without this file being ported.
19
+ * The scanner is the JavaScript consumer of the same spec, and the playground is
20
+ * its only caller.
21
+ *
22
+ * Nothing in the language spans lines: `//` runs to the end of one, a string
23
+ * closes on one, and there are no blocks. That is the property those editor
24
+ * formats need and the reason a `.reladraw` grammar is small in all of them.
25
+ */
26
+ /** What a span of source is, for colouring. */
27
+ export type TokenKind =
28
+ /** `// to the end of the line` */
29
+ 'comment'
30
+ /** A quoted string, quotes included. Unterminated ones count, so typing is quiet. */
31
+ | 'string'
32
+ /** The word a statement opens with: `box`, `link`, … */
33
+ | 'keyword'
34
+ /** The name a statement declares, right after its keyword. */
35
+ | 'name'
36
+ /** `->` and `<->`. */
37
+ | 'arrow'
38
+ /** Placement and link vocabulary: `right`, `of`, `level`, `with`, `and`, `between`, … */
39
+ | 'relation'
40
+ /** A `key:` opening an attribute or a bracketed modifier. */
41
+ | 'attribute'
42
+ /** The single word an attribute takes. */
43
+ | 'value'
44
+ /** `#14532d`, wherever it appears. */
45
+ | 'colour'
46
+ /** `(` and `)` around a placement's or a label's modifiers. */
47
+ | 'bracket'
48
+ /** Everything else: node names being referred to, and whitespace. */
49
+ | 'plain';
50
+ export interface Span {
51
+ kind: TokenKind;
52
+ /** Half-open, in characters, into the line it came from. */
53
+ start: number;
54
+ end: number;
55
+ }
56
+ /**
57
+ * The words a statement may open with. `parseStatement` in `parser.ts` is the
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 colour, which is a
60
+ * dull page rather than a wrong one.
61
+ */
62
+ export declare const STATEMENT_KEYWORDS: readonly ["box", "note", "link", "deck", "style", "diagram"];
63
+ /**
64
+ * Every word that says something about where a thing goes. Assembled from the
65
+ * lists the parser itself reads, so a direction or a passage axis added there
66
+ * colours here without anybody remembering to come back.
67
+ */
68
+ export declare const RELATION_WORDS: string[];
69
+ /**
70
+ * The patterns, as source strings, so a generator can emit them into another
71
+ * editor's grammar without reaching into a compiled RegExp. Each is written to
72
+ * match at the point the scanner has reached; the scanner adds the sticky flag.
73
+ *
74
+ * A name may contain dots (containment) and hyphens, which is why the word
75
+ * pattern is what it is rather than `\w+`.
76
+ */
77
+ export declare const PATTERNS: {
78
+ readonly comment: "\\/\\/.*";
79
+ readonly string: "\"(?:\\\\.|[^\"\\\\])*\"?";
80
+ readonly keyword: `(?:${string})\\b`;
81
+ readonly arrow: "<->|->|<-";
82
+ readonly attribute: "[A-Za-z][A-Za-z0-9_-]*:";
83
+ readonly colour: "#[0-9A-Fa-f]{3,8}\\b";
84
+ readonly relation: `(?:${string})\\b`;
85
+ readonly bracket: "[()]";
86
+ readonly word: "(?:[^\\s()\"\\/]|\\/(?!\\/))(?:[^\\s\"\\/]|\\/(?!\\/))*";
87
+ /**
88
+ * The same, inside an open bracket, where `)` closes the group instead of
89
+ * being an ordinary character. An editor grammar that cannot count brackets
90
+ * should use this one throughout: mistaking `rgb(20,20,20)` for three tokens
91
+ * is a smaller wrong than swallowing the `)` that ends `(gap: tight)`.
92
+ */
93
+ readonly wordInGroup: "(?:[^\\s()\"\\/]|\\/(?!\\/))(?:[^\\s)\"\\/]|\\/(?!\\/))*";
94
+ };
95
+ /**
96
+ * Classify one line. Always returns spans covering it end to end, in order, so a
97
+ * caller can rebuild the text by concatenation and can trust that nothing was
98
+ * dropped — which is what a highlighter drawn *behind* a textarea needs, since
99
+ * a lost character would slide every following one out of register.
100
+ */
101
+ export declare function highlightLine(line: string): Span[];
102
+ /** Every line of a source file, classified. Line endings are not included. */
103
+ export declare function highlight(source: string): Span[][];
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The language's *lexical* vocabulary, and a scanner that classifies one line of
3
+ * source into coloured spans.
4
+ *
5
+ * This is deliberately separate from `parser.ts`, and it is not a second parser.
6
+ * The parser answers "what does this file mean" and refuses anything it cannot
7
+ * answer for; a highlighter has to colour a half-typed line without complaint,
8
+ * so it answers only "what kind of word is this" and never fails. Every rule
9
+ * below is a regex applied to a single line, in priority order, with one bit of
10
+ * carried state (whether anything has been seen on the line yet, and whether the
11
+ * previous token was an attribute key).
12
+ *
13
+ * **That restriction is the point, and it is what makes this reusable.** An
14
+ * Emacs `font-lock-keywords` list, a Vim `syntax match` file and a TextMate
15
+ * grammar are all exactly this: an ordered list of single-line regexes with a
16
+ * face attached. So the vocabulary here — which is the half that goes stale, and
17
+ * which is imported from `ast.ts`, `constants.ts` and `icons.ts` rather than
18
+ * retyped — can be emitted into any of those without this file being ported.
19
+ * The scanner is the JavaScript consumer of the same spec, and the playground is
20
+ * its only caller.
21
+ *
22
+ * Nothing in the language spans lines: `//` runs to the end of one, a string
23
+ * closes on one, and there are no blocks. That is the property those editor
24
+ * formats need and the reason a `.reladraw` grammar is small in all of them.
25
+ */
26
+ import { DIRECTIONS, EDGES, PASSAGE_AXES } from './ast.js';
27
+ /**
28
+ * The words a statement may open with. `parseStatement` in `parser.ts` is the
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 colour, which is a
31
+ * dull page rather than a wrong one.
32
+ */
33
+ export const STATEMENT_KEYWORDS = ['box', 'note', 'link', 'deck', 'style', 'diagram'];
34
+ /** Statements whose second word declares a name. `diagram` has none. */
35
+ const DECLARES_NAME = ['box', 'note', 'deck', 'style'];
36
+ /**
37
+ * Every word that says something about where a thing goes. Assembled from the
38
+ * lists the parser itself reads, so a direction or a passage axis added there
39
+ * colours here without anybody remembering to come back.
40
+ */
41
+ export const RELATION_WORDS = [
42
+ ...DIRECTIONS,
43
+ ...EDGES,
44
+ ...Object.keys(PASSAGE_AXES),
45
+ // The connecting words. `of` is optional after a direction, `and` joins
46
+ // targets, `between` opens a passage, `level with` is the alignment.
47
+ 'of',
48
+ 'and',
49
+ 'level',
50
+ 'with',
51
+ 'between',
52
+ ];
53
+ /** Longest first, so `above-left` is not read as `above` followed by `-left`. */
54
+ function alternation(words) {
55
+ return [...new Set(words)].sort((a, b) => b.length - a.length).join('|');
56
+ }
57
+ /**
58
+ * The patterns, as source strings, so a generator can emit them into another
59
+ * editor's grammar without reaching into a compiled RegExp. Each is written to
60
+ * match at the point the scanner has reached; the scanner adds the sticky flag.
61
+ *
62
+ * A name may contain dots (containment) and hyphens, which is why the word
63
+ * pattern is what it is rather than `\w+`.
64
+ */
65
+ export const PATTERNS = {
66
+ comment: '\\/\\/.*',
67
+ // The closing quote is optional: every string is unterminated for as long as
68
+ // it is being typed, and a highlighter that waits for the quote repaints the
69
+ // rest of the file on every keystroke.
70
+ string: '"(?:\\\\.|[^"\\\\])*"?',
71
+ keyword: `(?:${alternation(STATEMENT_KEYWORDS)})\\b`,
72
+ // `<->` first, or `<-` would match its opening half and leave a stray `>`.
73
+ arrow: '<->|->|<-',
74
+ attribute: '[A-Za-z][A-Za-z0-9_-]*:',
75
+ colour: '#[0-9A-Fa-f]{3,8}\\b',
76
+ relation: `(?:${alternation(RELATION_WORDS)})\\b`,
77
+ bracket: '[()]',
78
+ // Matches what `tokenizeLine` treats as one bare token, and the awkwardness is
79
+ // load-bearing rather than accidental. A `(` counts as punctuation only where
80
+ // a token starts, so `rgb(20,20,20)` is one word — hence the first character
81
+ // being spelled differently from the rest. A lone `/` is ordinary and only a
82
+ // doubled one opens a comment, which is what the lookahead is for.
83
+ word: '(?:[^\\s()"\\/]|\\/(?!\\/))(?:[^\\s"\\/]|\\/(?!\\/))*',
84
+ /**
85
+ * The same, inside an open bracket, where `)` closes the group instead of
86
+ * being an ordinary character. An editor grammar that cannot count brackets
87
+ * should use this one throughout: mistaking `rgb(20,20,20)` for three tokens
88
+ * is a smaller wrong than swallowing the `)` that ends `(gap: tight)`.
89
+ */
90
+ wordInGroup: '(?:[^\\s()"\\/]|\\/(?!\\/))(?:[^\\s)"\\/]|\\/(?!\\/))*',
91
+ };
92
+ /**
93
+ * The rules that are pure regex, in priority order. Three things are missing and
94
+ * each for a reason: a comment wins over all of them and is tried first, a
95
+ * bracket is punctuation only at certain depths so the scanner counts them
96
+ * itself, and the catch-all word rule varies with that same depth and is
97
+ * appended per call.
98
+ */
99
+ const RULES = [
100
+ { kind: 'string', re: sticky(PATTERNS.string) },
101
+ { kind: 'arrow', re: sticky(PATTERNS.arrow) },
102
+ // Before `relation`, because the trailing colon is what tells `left: …` from
103
+ // the `left` of a placement, and after `arrow` so `->` is never a word.
104
+ { kind: 'attribute', re: sticky(PATTERNS.attribute) },
105
+ { kind: 'colour', re: sticky(PATTERNS.colour) },
106
+ { kind: 'relation', re: sticky(PATTERNS.relation) },
107
+ ];
108
+ const SPACE = sticky('[ \\t]+');
109
+ const COMMENT = sticky(PATTERNS.comment);
110
+ const KEYWORD = sticky(PATTERNS.keyword);
111
+ const WORD = sticky(PATTERNS.word);
112
+ const WORD_IN_GROUP = sticky(PATTERNS.wordInGroup);
113
+ function sticky(source) {
114
+ return new RegExp(source, 'y');
115
+ }
116
+ function match(re, line, at) {
117
+ re.lastIndex = at;
118
+ const found = re.exec(line);
119
+ return found ? found[0] : null;
120
+ }
121
+ /**
122
+ * Classify one line. Always returns spans covering it end to end, in order, so a
123
+ * caller can rebuild the text by concatenation and can trust that nothing was
124
+ * dropped — which is what a highlighter drawn *behind* a textarea needs, since
125
+ * a lost character would slide every following one out of register.
126
+ */
127
+ export function highlightLine(line) {
128
+ const spans = [];
129
+ let at = 0;
130
+ /** Nothing but whitespace seen yet, so the next word is the statement keyword. */
131
+ let opening = true;
132
+ /** The last thing emitted was a `key:`, so the next word is its value. */
133
+ let expectingValue = false;
134
+ /** How many brackets are open, which is what makes a `)` punctuation. */
135
+ let depth = 0;
136
+ const push = (kind, end) => {
137
+ spans.push({ kind, start: at, end });
138
+ at = end;
139
+ };
140
+ while (at < line.length) {
141
+ const gap = match(SPACE, line, at);
142
+ if (gap !== null) {
143
+ push('plain', at + gap.length);
144
+ continue;
145
+ }
146
+ // A comment wins everywhere, including in the middle of a statement.
147
+ const comment = match(COMMENT, line, at);
148
+ if (comment !== null) {
149
+ push('comment', at + comment.length);
150
+ continue;
151
+ }
152
+ if (opening) {
153
+ opening = false;
154
+ const keyword = match(KEYWORD, line, at);
155
+ if (keyword !== null) {
156
+ push('keyword', at + keyword.length);
157
+ if (DECLARES_NAME.includes(keyword)) {
158
+ const space = match(SPACE, line, at);
159
+ if (space !== null) {
160
+ push('plain', at + space.length);
161
+ const name = match(WORD, line, at);
162
+ // `style backup stroke: …` declares a name; `box fill: red` is a
163
+ // half-typed line whose second word is already an attribute, and
164
+ // colouring that as a name would be a lie about what it is.
165
+ if (name !== null && !name.endsWith(':'))
166
+ push('name', at + name.length);
167
+ }
168
+ }
169
+ continue;
170
+ }
171
+ }
172
+ // Brackets before the rest, and only where they are really punctuation: an
173
+ // opening one wherever a token starts, a closing one only while a group is
174
+ // open. That is `tokenizeLine`'s rule, and it is what keeps an unquoted
175
+ // `rgb(20,20,20)` a single word rather than three.
176
+ const ch = line[at];
177
+ if (ch === '(') {
178
+ depth += 1;
179
+ expectingValue = false;
180
+ push('bracket', at + 1);
181
+ continue;
182
+ }
183
+ if (ch === ')' && depth > 0) {
184
+ depth -= 1;
185
+ expectingValue = false;
186
+ push('bracket', at + 1);
187
+ continue;
188
+ }
189
+ let matched = false;
190
+ for (const rule of [...RULES, { kind: 'plain', re: depth > 0 ? WORD_IN_GROUP : WORD }]) {
191
+ const text = match(rule.re, line, at);
192
+ if (text === null)
193
+ continue;
194
+ // An attribute's value is whatever single token follows it, whatever it
195
+ // would otherwise have been called: `to: right` is a value, not a
196
+ // direction, and `style: wide` is a style name, not a gap.
197
+ const kind = expectingValue && rule.kind !== 'string' && rule.kind !== 'colour' && rule.kind !== 'bracket'
198
+ ? 'value'
199
+ : rule.kind;
200
+ expectingValue = rule.kind === 'attribute';
201
+ push(kind, at + text.length);
202
+ matched = true;
203
+ break;
204
+ }
205
+ // Nothing in `RULES` can fail on a non-space character, but a scanner that
206
+ // could loop forever is not worth the saved line.
207
+ if (!matched)
208
+ push('plain', at + 1);
209
+ }
210
+ return spans;
211
+ }
212
+ /** Every line of a source file, classified. Line endings are not included. */
213
+ export function highlight(source) {
214
+ return source.split(/\r?\n/).map(highlightLine);
215
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The built-in icon set.
3
+ *
4
+ * Every glyph is path data written into the SVG, and that is the whole reason
5
+ * this file exists rather than a dependency. The output contract is a standalone
6
+ * document: an icon font would render as blank boxes on any machine that does
7
+ * not have the font, and an `<image href>` would need the file to travel beside
8
+ * the SVG. Inline paths cost a few hundred bytes each and always arrive.
9
+ *
10
+ * A name here says what the thing *is*, never what the picture looks like. The
11
+ * same discipline as `gap: wide` over `gap: 110` and `muted` over a hex value:
12
+ * the word is the whole interface, so it has to carry meaning rather than
13
+ * geometry, and naming the meaning is what lets the drawing be improved later
14
+ * without every diagram that uses it changing sense.
15
+ *
16
+ * The set is deliberately small. In a drawing tool you pick a shape out of a
17
+ * visual palette and hundreds are browsable; here you type the word from memory,
18
+ * which caps the useful vocabulary at something that fits in a head. Add a name
19
+ * when a diagram asks for a distinction it cannot otherwise make.
20
+ */
21
+ /**
22
+ * Icons carry three tones rather than colours. `ink` is the drawn line, `shade`
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 colours is what
25
+ * lets one glyph sit correctly on a dark theme and a light one.
26
+ */
27
+ export type IconTone = 'ink' | 'shade' | 'void';
28
+ export interface IconPath {
29
+ d: string;
30
+ fill?: IconTone;
31
+ stroke?: IconTone;
32
+ }
33
+ export interface Icon {
34
+ /** Side of the square the paths are drawn on. Scaled to the drawn size. */
35
+ readonly grid: number;
36
+ readonly paths: readonly IconPath[];
37
+ }
38
+ /** Line width on the 24-unit grid, scaled with everything else. */
39
+ export declare const ICON_STROKE = 1.1;
40
+ export declare const ICONS: Record<string, Icon>;
41
+ export declare const ICON_NAMES: string[];
42
+ /**
43
+ * The icon a node asks for, or nothing. Called by the resolver, which reserves
44
+ * the room, and by the renderer, which fills it, so the two cannot disagree
45
+ * about whether there is an icon at all.
46
+ *
47
+ * An unknown name is refused rather than dropped. That is the same rule
48
+ * `DIAGRAM_KEYS` follows and it is here for the same reason: a misspelt
49
+ * `icon: laptp` that quietly draws nothing is indistinguishable from the tool
50
+ * being broken, and an author will stare at the file looking for the mistake in
51
+ * the wrong place. Since the vocabulary is closed and short, the error can list
52
+ * the whole of it.
53
+ */
54
+ /**
55
+ * The outlines a box can take. `box` is the plain rectangle and needs no word.
56
+ *
57
+ * Named for what a node *is*, never for the geometry, which is the same rule the
58
+ * icon names follow: `document` and not `folded-corner`. A shape carrying a
59
+ * conventional meaning is a second channel alongside colour, and a stronger one
60
+ * — a fill is whatever the author assigned and has to be learnt from the
61
+ * diagram, while a folded corner has meant "an artifact, not a process" in
62
+ * flowcharts for decades and reads with no legend at all.
63
+ */
64
+ export declare const BOX_SHAPES: readonly ["document"];
65
+ export type BoxShape = 'box' | (typeof BOX_SHAPES)[number];
66
+ export interface NodeShape {
67
+ outline: BoxShape;
68
+ /**
69
+ * Set when the node is drawn as a glyph rather than as a box. There is then
70
+ * no outline, no fill and no padding: the node *is* the picture, and its size
71
+ * is the picture's. `icon:` decorates a box, this replaces it.
72
+ */
73
+ body?: Icon;
74
+ }
75
+ /**
76
+ * What a node is drawn as. Shared by the resolver, which sizes it, and the
77
+ * renderer, which draws it.
78
+ *
79
+ * The value is either a box outline or the name of a glyph. Those are the two
80
+ * things "what is this drawn as" can answer, and the author has no reason to
81
+ * care which category their answer fell into. The test that separates them is
82
+ * whether the node still sizes itself from its label: a `document` does, a
83
+ * glyph does not.
84
+ */
85
+ export declare function shapeFor(appearance: Record<string, string>, line: number): NodeShape;
86
+ export declare function iconFor(appearance: Record<string, string>, line: number): Icon | undefined;