reladraw 0.2.0 → 0.3.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/dist/constants.js CHANGED
@@ -1,13 +1,15 @@
1
+ import { POSITIONS, isPosition } from './ast.js';
1
2
  import { SourceError } from './errors.js';
3
+ import { plain } from './text.js';
2
4
  /** Inside a box, between its border and its contents. */
3
5
  export const PAD = 14;
4
6
  /** Between stacked children of one container. */
5
7
  export const CHILD_GAP = 10;
6
- /** Between a container's own label and its first child. */
8
+ /** Between a container's own text and its first child. */
7
9
  export const HEADER_GAP = 10;
8
10
  /**
9
11
  * How far each deck copy is offset behind the front face. It has to clear a
10
- * whole line of text plus the padding above it, or a copy's label is drawn and
12
+ * whole line of text plus the padding above it, or a copy's text is drawn and
11
13
  * then immediately covered by the copy in front of it.
12
14
  */
13
15
  export const DECK_STEP = 34;
@@ -33,7 +35,7 @@ export const GAPS = {
33
35
  */
34
36
  export const SEPARATION_GAP = GAPS['tight'];
35
37
  /**
36
- * Between two links meeting the same side of the same box. An author names a
38
+ * Between two edges meeting the same side of the same box. An author names a
37
39
  * side, never a point on it, so this is the tool keeping two attachments apart
38
40
  * rather than a distance anyone asked for — small, like `SEPARATION_GAP`, and
39
41
  * squeezed further if the side is too short to hold the whole group.
@@ -41,7 +43,7 @@ export const SEPARATION_GAP = GAPS['tight'];
41
43
  export const ATTACH_STEP = 16;
42
44
  /** Kept clear at each end of a side, so an attachment never sits on a corner. */
43
45
  export const ATTACH_MARGIN = 10;
44
- /** How wide a link's line is drawn. */
46
+ /** How wide an edge's line is drawn. */
45
47
  export const LINE_WIDTH = 1.6;
46
48
  /**
47
49
  * The arrowhead's length, in the `markerUnits="strokeWidth"` the marker is
@@ -55,48 +57,56 @@ export const ARROW_MARKER_WIDTH = 7;
55
57
  */
56
58
  export const ARROW_LENGTH = ARROW_MARKER_WIDTH * LINE_WIDTH;
57
59
  /**
58
- * Line left showing between a link's label and the box at that end of the
60
+ * Line left showing between an edge's text and the box at that end of the
59
61
  * corridor it crosses.
60
62
  *
61
63
  * Unlike `SEPARATION_GAP` and `ATTACH_MARGIN` this is not "small on purpose".
62
64
  * Those two keep two things from touching, and the least distance that reads as
63
- * "not touching" is the right one. This one has something to show: a label sits
65
+ * "not touching" is the right one. This one has something to show: a text sits
64
66
  * in a knockout that erases the line behind it, so whatever is left either side
65
- * is the entire evidence that the label belongs to a link at all. At ten pixels
67
+ * is the entire evidence that the text belongs to an edge at all. At ten pixels
66
68
  * it did not read as a line — the seed diagram in the playground drew as a word
67
69
  * with a dash beside it — so it is the length of a run of line, not a margin.
68
70
  *
69
71
  * Say `gap:` if you want the corridor wider than its contents.
70
72
  */
71
- export const LABEL_CLEARANCE = 20;
73
+ export const TEXT_CLEARANCE = 20;
72
74
  /**
73
- * How much room a link's label takes along one axis.
75
+ * How much room an edge's text takes along one axis.
74
76
  *
75
- * The knockout rectangle drawn behind a label is the text plus five either side,
77
+ * The knockout rectangle drawn behind a text is the text plus five either side,
76
78
  * so that rectangle, not the glyphs, is what must not overlap anything.
77
79
  *
78
- * Shared by the resolver, which widens a corridor to hold a label, and the
80
+ * Shared by the resolver, which widens a corridor to hold a text, and the
79
81
  * renderer, which spaces the lanes of a channel by it, so the two cannot
80
- * disagree about how much room a label needs. The two ask different questions of
81
- * it and both are right: the resolver measures *along* the run, so a link
82
- * traveling horizontally needs the label's width; the renderer measures *across*
83
- * the channel, so a link traveling horizontally down one needs its height.
84
- */
85
- export function labelExtent(label, appearance, axis, measurer, fontSize, line) {
86
- const size = fontSizeFor('link', appearance, fontSize, line);
87
- const { width, lines } = measurer.measure(label, size);
82
+ * disagree about how much room a text needs. The two ask different questions of
83
+ * it and both are right: the resolver measures *along* the run, so an edge
84
+ * traveling horizontally needs the text's width; the renderer measures *across*
85
+ * the channel, so an edge traveling horizontally down one needs its height.
86
+ */
87
+ export function textExtent(lines, textAttrs, axis, measurer, fontSize, line) {
88
+ const size = fontSizeFor('edge', textAttrs, fontSize, line);
89
+ const width = lines.reduce((widest, drawn) => Math.max(widest, measurer.measure(plain(drawn), size).width), 0);
88
90
  return axis === 'x' ? width + 10 : lines.length * measurer.lineHeight(size);
89
91
  }
90
92
  /**
91
- * An icon is two lines of the label tall, and that ratio is what makes it a
92
- * *label-sized* ornament rather than a picture with a size of its own. It is
93
+ * The widest of these lines, which is how wide the block of them is. Shared by
94
+ * the resolver, which sizes a node to hold its text, and the renderer, which
95
+ * places that block in the room the node gave it.
96
+ */
97
+ export function widestLine(lines, measurer, fontSize) {
98
+ return lines.reduce((widest, line) => Math.max(widest, measurer.measure(plain(line), fontSize).width), 0);
99
+ }
100
+ /**
101
+ * An icon is two lines of the text tall, and that ratio is what makes it a
102
+ * *text-sized* ornament rather than a picture with a size of its own. It is
93
103
  * read off the reference, where the title lines run 25 pixels baseline to
94
104
  * baseline and the drive and machine glyphs are close to 50 tall. Deriving it
95
105
  * from the text also means an icon on a `size: small` node shrinks with it,
96
106
  * which is what anyone would expect and what a fixed pixel count would not do.
97
107
  */
98
108
  export const ICON_LINES = 2;
99
- /** Between the label column and the icon column beside it. */
109
+ /** Between the text column and the icon column beside it. */
100
110
  export const ICON_GAP = 10;
101
111
  export const DEFAULT_FONT_SIZE = 14;
102
112
  /**
@@ -106,7 +116,7 @@ export const DEFAULT_FONT_SIZE = 14;
106
116
  * it says nothing about why one piece of text is smaller than another.
107
117
  *
108
118
  * `small` is sampled rather than chosen. In
109
- * `examples/reference/arch.png` the box and container labels run 25
119
+ * `examples/reference/arch.png` the box and container texts run 25
110
120
  * pixels baseline to baseline and every annotation runs 21, which is this
111
121
  * ratio; `./dev.sh textrows` is how that was read off. `large` is the same step
112
122
  * taken the other way, so the scale is symmetric about the document size.
@@ -117,20 +127,21 @@ export const TEXT_SIZES = {
117
127
  large: 25 / 21,
118
128
  };
119
129
  /**
120
- * What each kind of text is set at when the file says nothing. A note annotates
121
- * the diagram rather than being part of it, and at the size of a box label an
122
- * aside reads as a statement — so `note` starts small and says so by being a
123
- * note. This is a default and not a ceiling: `size:` overrides it, the same way
124
- * `fill:` overrides the theme's color.
130
+ * What each kind of text is set at when the file says nothing. A node with no
131
+ * body annotates the diagram rather than being part of it, and at the size of a
132
+ * box text an aside reads as a statement — so `shape: none` starts small and
133
+ * says so by having no body. This is a default and not a ceiling: `(size: …)`
134
+ * in the text's brackets overrides it, the same way `fill:` overrides the
135
+ * theme's color.
125
136
  */
126
- const DEFAULT_TEXT_SIZE = { note: 'small' };
137
+ const DEFAULT_TEXT_SIZE = { none: 'small' };
127
138
  /**
128
139
  * The size a piece of text is set at. Shared by the resolver, which reserves
129
140
  * the room, and the renderer, which fills it, so the two cannot disagree about
130
141
  * how much room there is.
131
142
  */
132
- export function fontSizeFor(kind, appearance, fontSize, line) {
133
- const named = appearance['size'] ?? DEFAULT_TEXT_SIZE[kind] ?? 'normal';
143
+ export function fontSizeFor(kind, textAttrs, fontSize, line) {
144
+ const named = textAttrs['size'] ?? DEFAULT_TEXT_SIZE[kind] ?? 'normal';
134
145
  const scale = TEXT_SIZES[named];
135
146
  if (scale === undefined) {
136
147
  throw new SourceError(`size takes ${Object.keys(TEXT_SIZES).join(', ')}, not "${named}"`, line);
@@ -138,52 +149,54 @@ export function fontSizeFor(kind, appearance, fontSize, line) {
138
149
  return Math.round(fontSize * scale);
139
150
  }
140
151
  export const DEFAULT_MARGIN = 40;
141
- /**
142
- * Where a container's own label sits. Every container reserves a band for its
143
- * label and its icon; `at` says which end of the box that band is, and the
144
- * contents take what is left. `align` says how the text sits across it.
145
- *
146
- * The two are independent and neither implies the other. A label at the bottom
147
- * is an ordinary label that happens to be at the bottom — there is no kind of
148
- * label being named here and no second thing quietly coming along with the
149
- * first. An earlier version bundled them as `label: heading | caption`, which
150
- * read a position as though it were a meaning; a folded corner means "artifact
151
- * rather than process" and a reader decodes it, while "lower down" means only
152
- * lower down.
153
- *
154
- * A leaf has no band, so `at` says nothing about one and is refused there. But
155
- * `align` is not about the band: a label of more than one line has lines of
156
- * unequal length whatever kind of box it is in, and how those sit across each
157
- * other is a real question anywhere. A leaf's default is centered rather than
158
- * ranged left, which is why the fallback is a parameter.
159
- */
160
- export const LABEL_ENDS = ['top', 'bottom'];
161
152
  /**
162
153
  * Author's word to the SVG's. One spelling of each, per the rule that refuses
163
- * synonyms for `box` and `link`: an alias is a variant a reader has to learn,
154
+ * synonyms for `node` and `edge`: an alias is a variant a reader has to learn,
164
155
  * and every document and example has to pick one of them anyway.
165
156
  */
166
- const LABEL_ALIGNMENTS = {
157
+ const TEXT_ALIGNMENTS = {
167
158
  left: 'start',
168
159
  center: 'middle',
169
160
  right: 'end',
170
161
  };
171
162
  /**
172
- * Read a label's bracketed modifiers. Shared by the resolver, which offsets the
163
+ * Read a text's bracketed modifiers. Shared by the resolver, which offsets the
173
164
  * contents away from the band, and the renderer, which draws into it, so the two
174
165
  * cannot disagree about which end the band is at.
175
166
  */
176
- export function labelStyleFor(label, line, fallbackAlign = 'start') {
177
- const at = label['at'];
178
- if (at !== undefined && !LABEL_ENDS.includes(at)) {
179
- throw new SourceError(`a label's at takes ${LABEL_ENDS.join(' or ')}, not "${at}"`, line);
167
+ export function textStyleFor(attrs, line, fallbackAlign = 'start', fallbackAt = 'top-left') {
168
+ const written = attrs['at'];
169
+ if (written !== undefined && !isPosition(written)) {
170
+ // A bare side word is the one mistake worth naming rather than only
171
+ // refusing: it was the whole vocabulary until 0.3.0, and the point at the
172
+ // middle of that side is exactly one word away.
173
+ const midpoint = `${written}-center`;
174
+ throw new SourceError(isPosition(midpoint)
175
+ ? `a text sits at a *point* of the box, and "${written}" names a side — write "${midpoint}" for the point at the middle of it`
176
+ : `a text's at takes one of ${POSITIONS.join(', ')}, not "${written}"`, line);
180
177
  }
181
- const align = label['align'];
182
- if (align !== undefined && LABEL_ALIGNMENTS[align] === undefined) {
183
- throw new SourceError(`a label's align takes left, center or right, not "${align}"`, line);
178
+ const at = written ?? fallbackAt;
179
+ const align = attrs['align'];
180
+ if (align !== undefined && TEXT_ALIGNMENTS[align] === undefined) {
181
+ throw new SourceError(`a text's align takes left, center or right, not "${align}"`, line);
184
182
  }
183
+ const parts = at.split('-');
185
184
  return {
186
- at: at ?? 'top',
187
- align: align === undefined ? fallbackAlign : LABEL_ALIGNMENTS[align],
185
+ at,
186
+ end: parts.includes('top') ? 'top' : parts.includes('bottom') ? 'bottom' : 'center',
187
+ side: parts.includes('left') ? 'left' : parts.includes('right') ? 'right' : 'center',
188
+ align: align === undefined ? fallbackAlign : TEXT_ALIGNMENTS[align],
188
189
  };
189
190
  }
191
+ /**
192
+ * Where a leaf's text or badge starts vertically, given which end it sits at.
193
+ * Shared, because the resolver works out the text's box and the renderer draws
194
+ * it, and the two must not be able to disagree.
195
+ */
196
+ export function leafTop(end, y, height, own) {
197
+ if (end === 'top')
198
+ return y + PAD;
199
+ if (end === 'bottom')
200
+ return y + height - PAD - own;
201
+ return y + (height - own) / 2;
202
+ }
package/dist/grammar.d.ts CHANGED
@@ -29,13 +29,15 @@ export type TokenKind =
29
29
  'comment'
30
30
  /** A quoted string, quotes included. Unterminated ones count, so typing is quiet. */
31
31
  | 'string'
32
- /** The word a statement opens with: `box`, `link`, … */
32
+ /** A markup tag inside a text: `[dim]` and `[/dim]`. */
33
+ | 'markup'
34
+ /** The word a statement opens with: `node`, `edge`, … */
33
35
  | 'keyword'
34
36
  /** The name a statement declares, right after its keyword. */
35
37
  | 'name'
36
38
  /** `->` and `<->`. */
37
39
  | 'arrow'
38
- /** Placement and link vocabulary: `right`, `of`, `level`, `with`, `and`, `between`, … */
40
+ /** Placement and edge vocabulary: `right`, `of`, `level`, `with`, `and`, `between`, … */
39
41
  | 'relation'
40
42
  /** A `key:` opening an attribute or a bracketed modifier. */
41
43
  | 'attribute'
@@ -43,7 +45,7 @@ export type TokenKind =
43
45
  | 'value'
44
46
  /** `#14532d`, wherever it appears. */
45
47
  | 'color'
46
- /** `(` and `)` around a placement's or a label's modifiers. */
48
+ /** `(` and `)` around a placement's or a text's modifiers. */
47
49
  | 'bracket'
48
50
  /** Everything else: node names being referred to, and whitespace. */
49
51
  | 'plain';
@@ -59,7 +61,7 @@ export interface Span {
59
61
  * it. A word missing here is a word that draws in the plain color, which is a
60
62
  * dull page rather than a wrong one.
61
63
  */
62
- export declare const STATEMENT_KEYWORDS: readonly ["box", "note", "link", "deck", "style", "diagram"];
64
+ export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "deck", "style", "diagram"];
63
65
  /**
64
66
  * Every word that says something about where a thing goes. Assembled from the
65
67
  * lists the parser itself reads, so a direction or a passage axis added there
@@ -83,6 +85,17 @@ export declare const PATTERNS: {
83
85
  readonly color: "#[0-9A-Fa-f]{3,8}\\b";
84
86
  readonly relation: `(?:${string})\\b`;
85
87
  readonly bracket: "[()]";
88
+ /**
89
+ * A markup tag, which appears only inside a string. An editor grammar that
90
+ * scopes patterns should apply this one inside the string scope; the scanner
91
+ * below does the same thing by splitting a matched string span.
92
+ *
93
+ * The escape is `\[`, and this pattern deliberately does not exclude it —
94
+ * doing so needs a lookbehind, which several grammar formats lack, and a
95
+ * wrongly-colored escaped bracket is a much smaller wrong than a highlighter
96
+ * that cannot be ported.
97
+ */
98
+ readonly markup: "\\[\\/?[A-Za-z][A-Za-z0-9_-]*\\]";
86
99
  readonly word: "(?:[^\\s()\"\\/]|\\/(?!\\/))(?:[^\\s\"\\/]|\\/(?!\\/))*";
87
100
  /**
88
101
  * The same, inside an open bracket, where `)` closes the group instead of
package/dist/grammar.js CHANGED
@@ -23,16 +23,16 @@
23
23
  * closes on one, and there are no blocks. That is the property those editor
24
24
  * formats need and the reason a `.reladraw` grammar is small in all of them.
25
25
  */
26
- import { DIRECTIONS, EDGES, PASSAGE_AXES } from './ast.js';
26
+ import { DIRECTIONS, POSITIONS, SIDES, PASSAGE_AXES } from './ast.js';
27
27
  /**
28
28
  * The words a statement may open with. `parseStatement` in `parser.ts` is the
29
29
  * authority — its switch is what actually accepts them — and this list mirrors
30
30
  * it. A word missing here is a word that draws in the plain color, which is a
31
31
  * dull page rather than a wrong one.
32
32
  */
33
- export const STATEMENT_KEYWORDS = ['box', 'note', 'link', 'deck', 'style', 'diagram'];
33
+ export const STATEMENT_KEYWORDS = ['node', 'edge', 'deck', 'style', 'diagram'];
34
34
  /** Statements whose second word declares a name. `diagram` has none. */
35
- const DECLARES_NAME = ['box', 'note', 'deck', 'style'];
35
+ const DECLARES_NAME = ['node', 'deck', 'style'];
36
36
  /**
37
37
  * Every word that says something about where a thing goes. Assembled from the
38
38
  * lists the parser itself reads, so a direction or a passage axis added there
@@ -40,11 +40,17 @@ const DECLARES_NAME = ['box', 'note', 'deck', 'style'];
40
40
  */
41
41
  export const RELATION_WORDS = [
42
42
  ...DIRECTIONS,
43
- ...EDGES,
43
+ ...SIDES,
44
+ ...POSITIONS,
44
45
  ...Object.keys(PASSAGE_AXES),
45
46
  // The connecting words. `of` is optional after a direction, `and` joins
46
- // targets, `between` opens a passage, `level with` is the alignment.
47
+ // targets, `between` opens a passage, `level with` is the alignment, and
48
+ // `on ... at ...` is the overlay. Note `of` and `at` are what tell the two
49
+ // position vocabularies apart: a direction is always followed by `of`, a
50
+ // named position always preceded by `at`.
47
51
  'of',
52
+ 'at',
53
+ 'on',
48
54
  'and',
49
55
  'level',
50
56
  'with',
@@ -75,6 +81,17 @@ export const PATTERNS = {
75
81
  color: '#[0-9A-Fa-f]{3,8}\\b',
76
82
  relation: `(?:${alternation(RELATION_WORDS)})\\b`,
77
83
  bracket: '[()]',
84
+ /**
85
+ * A markup tag, which appears only inside a string. An editor grammar that
86
+ * scopes patterns should apply this one inside the string scope; the scanner
87
+ * below does the same thing by splitting a matched string span.
88
+ *
89
+ * The escape is `\[`, and this pattern deliberately does not exclude it —
90
+ * doing so needs a lookbehind, which several grammar formats lack, and a
91
+ * wrongly-colored escaped bracket is a much smaller wrong than a highlighter
92
+ * that cannot be ported.
93
+ */
94
+ markup: '\\[\\/?[A-Za-z][A-Za-z0-9_-]*\\]',
78
95
  // Matches what `tokenizeLine` treats as one bare token, and the awkwardness is
79
96
  // load-bearing rather than accidental. A `(` counts as punctuation only where
80
97
  // a token starts, so `rgb(20,20,20)` is one word — hence the first character
@@ -159,7 +176,7 @@ export function highlightLine(line) {
159
176
  if (space !== null) {
160
177
  push('plain', at + space.length);
161
178
  const name = match(WORD, line, at);
162
- // `style backup border: …` declares a name; `box fill: red` is a
179
+ // `style backup border: …` declares a name; `node fill: red` is a
163
180
  // half-typed line whose second word is already an attribute, and
164
181
  // coloring that as a name would be a lie about what it is.
165
182
  if (name !== null && !name.endsWith(':'))
@@ -198,7 +215,16 @@ export function highlightLine(line) {
198
215
  ? 'value'
199
216
  : rule.kind;
200
217
  expectingValue = rule.kind === 'attribute';
201
- push(kind, at + text.length);
218
+ // A string is the one span with something inside it: markup naming a
219
+ // style. The spans still cover the line exactly, which is what a
220
+ // highlighter drawn behind a textarea needs.
221
+ if (rule.kind === 'string') {
222
+ for (const part of splitMarkup(text, at))
223
+ push(part.kind, part.end);
224
+ }
225
+ else {
226
+ push(kind, at + text.length);
227
+ }
202
228
  matched = true;
203
229
  break;
204
230
  }
@@ -209,6 +235,26 @@ export function highlightLine(line) {
209
235
  }
210
236
  return spans;
211
237
  }
238
+ /**
239
+ * One string span cut into the plain stretches and the markup tags inside it.
240
+ * An escaped bracket is left to be colored as a tag when it looks like one,
241
+ * which is the wrong the pattern's doc comment accepts in exchange for being
242
+ * portable.
243
+ */
244
+ function splitMarkup(text, from) {
245
+ const parts = [];
246
+ const tag = new RegExp(PATTERNS.markup, 'g');
247
+ let at = 0;
248
+ for (let found = tag.exec(text); found !== null; found = tag.exec(text)) {
249
+ if (found.index > at)
250
+ parts.push({ kind: 'string', end: from + found.index });
251
+ parts.push({ kind: 'markup', end: from + found.index + found[0].length });
252
+ at = found.index + found[0].length;
253
+ }
254
+ if (at < text.length || parts.length === 0)
255
+ parts.push({ kind: 'string', end: from + text.length });
256
+ return parts;
257
+ }
212
258
  /** Every line of a source file, classified. Line endings are not included. */
213
259
  export function highlight(source) {
214
260
  return source.split(/\r?\n/).map(highlightLine);
package/dist/icons.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * the SVG. Inline paths cost a few hundred bytes each and always arrive.
9
9
  *
10
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:
11
+ * same discipline as `muted` over a hex value:
12
12
  * the word is the whole interface, so it has to carry meaning rather than
13
13
  * geometry, and naming the meaning is what lets the drawing be improved later
14
14
  * without every diagram that uses it changing sense.
@@ -21,7 +21,7 @@
21
21
  /**
22
22
  * Icons carry three tones rather than colors. `ink` is the drawn line, `shade`
23
23
  * the body it encloses, and `void` is the page showing through — the same
24
- * knockout a link label already uses. Naming tones instead of colors is what
24
+ * knockout an edge text already uses. Naming tones instead of colors is what
25
25
  * lets one glyph sit correctly on a dark theme and a light one.
26
26
  */
27
27
  export type IconTone = 'ink' | 'shade' | 'void';
@@ -40,19 +40,8 @@ export declare const ICON_STROKE = 1.1;
40
40
  export declare const ICONS: Record<string, Icon>;
41
41
  export declare const ICON_NAMES: string[];
42
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.
43
+ * The outlines a node's body can take. `rectangle` is the plain one and needs no
44
+ * word, since it is what a node is when it says nothing.
56
45
  *
57
46
  * Named for what a node *is*, never for the geometry, which is the same rule the
58
47
  * icon names follow: `document` and not `folded-corner`. A shape carrying a
@@ -60,27 +49,56 @@ export declare const ICON_NAMES: string[];
60
49
  * — a fill is whatever the author assigned and has to be learnt from the
61
50
  * diagram, while a folded corner has meant "an artifact, not a process" in
62
51
  * flowcharts for decades and reads with no legend at all.
52
+ *
53
+ * `circle` and `diamond` join these when a diagram asks. The set being short is
54
+ * a fact about what has been drawn, not about the key.
63
55
  */
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
- }
56
+ export declare const OUTLINES: readonly ["rectangle", "document"];
57
+ export type Outline = (typeof OUTLINES)[number];
58
+ /** Every value `shape:` accepts, `none` included. */
59
+ export declare const SHAPE_WORDS: readonly ["rectangle", "document", "none"];
75
60
  /**
76
- * What a node is drawn as. Shared by the resolver, which sizes it, and the
77
- * renderer, which draws it.
61
+ * What a node is drawn as: an outline, a picture, or nothing at all.
62
+ *
63
+ * Two keys name it and each names one part. `shape:` is the outline the node is
64
+ * drawn with, `none` included; `icon:` is the picture the node is drawn *as*.
65
+ * Writing both is an error, because a node has one body.
78
66
  *
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.
67
+ * The test that separates them is whether the node still sizes itself from its
68
+ * text: a `document` does, a picture does not — its size is the picture's and
69
+ * its text goes underneath.
70
+ */
71
+ export type Body = {
72
+ kind: 'shape';
73
+ outline: Outline;
74
+ } | {
75
+ kind: 'icon';
76
+ icon: Icon;
77
+ } | {
78
+ kind: 'none';
79
+ };
80
+ /**
81
+ * Which body a node has, from what it wrote and what its style carried.
82
+ *
83
+ * Called once per node while the tree is built, and the answer is kept on the
84
+ * node — so the resolver, which sizes the body, and the renderer, which draws
85
+ * it, cannot disagree about what it is.
86
+ *
87
+ * A key the node wrote itself overrules the other key coming from a style. That
88
+ * is the same permissiveness `checkAttrs` already grants a style: a style is a
89
+ * bundle meant to be shared across kinds, and a key it carries that this node
90
+ * has overridden is unused rather than wrong. Both keys from one level is a
91
+ * genuine contradiction and is refused by name.
92
+ */
93
+ export declare function bodyFor(attrs: Record<string, string>, appearance: Record<string, string>, line: number): Body;
94
+ /**
95
+ * The icon of that name, or an error listing the set.
96
+ *
97
+ * An unknown name is refused rather than dropped. That is the same rule
98
+ * `DIAGRAM_KEYS` follows and it is here for the same reason: a misspelt
99
+ * `icon: laptp` that quietly draws nothing is indistinguishable from the tool
100
+ * being broken, and an author will stare at the file looking for the mistake in
101
+ * the wrong place. Since the vocabulary is closed and short, the error can list
102
+ * the whole of it.
84
103
  */
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;
104
+ export declare function iconNamed(named: string, line: number): Icon;