reladraw 0.1.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/icons.js 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.
@@ -26,7 +26,7 @@ export const ICON_STROKE = 1.1;
26
26
  function circle(cx, cy, r) {
27
27
  return `M${cx - r} ${cy} a${r} ${r} 0 1 0 ${r * 2} 0 a${r} ${r} 0 1 0 ${-r * 2} 0 Z`;
28
28
  }
29
- /** The three visible faces of an isometric cube, top face centred on `cx, cy`. */
29
+ /** The three visible faces of an isometric cube, top face centered on `cx, cy`. */
30
30
  function cube(cx, cy, s) {
31
31
  const half = s / 2;
32
32
  return [
@@ -83,14 +83,23 @@ export const ICONS = {
83
83
  * than any particular number: this is the symbol for "several", and at two
84
84
  * line-heights square a literal count turns to mush. Where the count carries
85
85
  * meaning — where one of them is the end of an arrow — they are nodes, and
86
- * `shape: instance` is how you draw them.
86
+ * `icon: cube` is how you draw them.
87
87
  */
88
88
  cubes: {
89
89
  grid: GRID,
90
90
  paths: [...cube(6.6, 2.6, 4.4), ...cube(17.4, 2.6, 4.4), ...cube(12, 11.6, 4.4)],
91
91
  },
92
- /** One unit of the kind `cubes` shows several of. */
93
- instance: {
92
+ /**
93
+ * One unit of the kind `cubes` shows several of.
94
+ *
95
+ * Called `instance` until 0.3.0, under the rule that a name says what the
96
+ * thing is rather than what the picture looks like. That rule works where a
97
+ * picture has one conventional meaning — a disk, a laptop, a folded corner —
98
+ * and it misfires here, because a cube has none: it stands for a container in
99
+ * one diagram, a VM in another, a service in a third, and the diagram assigns
100
+ * the meaning. `instance` picked one of those and hid the picture.
101
+ */
102
+ cube: {
94
103
  grid: GRID,
95
104
  paths: cube(12, 2.6, 8.4),
96
105
  },
@@ -107,60 +116,89 @@ export const ICONS = {
107
116
  };
108
117
  export const ICON_NAMES = Object.keys(ICONS);
109
118
  /**
110
- * The icon a node asks for, or nothing. Called by the resolver, which reserves
111
- * the room, and by the renderer, which fills it, so the two cannot disagree
112
- * about whether there is an icon at all.
113
- *
114
- * An unknown name is refused rather than dropped. That is the same rule
115
- * `DIAGRAM_KEYS` follows and it is here for the same reason: a misspelt
116
- * `icon: laptp` that quietly draws nothing is indistinguishable from the tool
117
- * being broken, and an author will stare at the file looking for the mistake in
118
- * the wrong place. Since the vocabulary is closed and short, the error can list
119
- * the whole of it.
120
- */
121
- /**
122
- * The outlines a box can take. `box` is the plain rectangle and needs no word.
119
+ * The outlines a node's body can take. `rectangle` is the plain one and needs no
120
+ * word, since it is what a node is when it says nothing.
123
121
  *
124
122
  * Named for what a node *is*, never for the geometry, which is the same rule the
125
123
  * icon names follow: `document` and not `folded-corner`. A shape carrying a
126
- * conventional meaning is a second channel alongside colour, and a stronger one
124
+ * conventional meaning is a second channel alongside color, and a stronger one
127
125
  * — a fill is whatever the author assigned and has to be learnt from the
128
126
  * diagram, while a folded corner has meant "an artifact, not a process" in
129
127
  * flowcharts for decades and reads with no legend at all.
128
+ *
129
+ * `circle` and `diamond` join these when a diagram asks. The set being short is
130
+ * a fact about what has been drawn, not about the key.
130
131
  */
131
- export const BOX_SHAPES = ['document'];
132
- const PLAIN = { outline: 'box' };
132
+ export const OUTLINES = ['rectangle', 'document'];
133
+ /** Every value `shape:` accepts, `none` included. */
134
+ export const SHAPE_WORDS = [...OUTLINES, 'none'];
135
+ const PLAIN = { kind: 'shape', outline: 'rectangle' };
133
136
  /**
134
- * What a node is drawn as. Shared by the resolver, which sizes it, and the
135
- * renderer, which draws it.
137
+ * Which body a node has, from what it wrote and what its style carried.
136
138
  *
137
- * The value is either a box outline or the name of a glyph. Those are the two
138
- * things "what is this drawn as" can answer, and the author has no reason to
139
- * care which category their answer fell into. The test that separates them is
140
- * whether the node still sizes itself from its label: a `document` does, a
141
- * glyph does not.
139
+ * Called once per node while the tree is built, and the answer is kept on the
140
+ * node — so the resolver, which sizes the body, and the renderer, which draws
141
+ * it, cannot disagree about what it is.
142
+ *
143
+ * A key the node wrote itself overrules the other key coming from a style. That
144
+ * is the same permissiveness `checkAttrs` already grants a style: a style is a
145
+ * bundle meant to be shared across kinds, and a key it carries that this node
146
+ * has overridden is unused rather than wrong. Both keys from one level is a
147
+ * genuine contradiction and is refused by name.
142
148
  */
143
- export function shapeFor(appearance, line) {
144
- const named = appearance['shape'];
145
- if (named === undefined)
146
- return PLAIN;
147
- if (named === 'box')
149
+ export function bodyFor(attrs, appearance, line) {
150
+ const wroteShape = attrs['shape'] !== undefined;
151
+ const wroteIcon = attrs['icon'] !== undefined;
152
+ let shape = wroteIcon && !wroteShape ? undefined : appearance['shape'];
153
+ let icon = wroteShape && !wroteIcon ? undefined : appearance['icon'];
154
+ if (shape !== undefined && icon !== undefined) {
155
+ throw new SourceError(`shape: ${shape} and icon: ${icon} both say what this node is drawn as, and it has one ` +
156
+ 'body — `shape:` is the outline it is drawn with, `icon:` is the picture it is drawn as', line);
157
+ }
158
+ if (icon !== undefined)
159
+ return { kind: 'icon', icon: iconNamed(icon, line) };
160
+ if (shape === undefined)
148
161
  return PLAIN;
149
- if (BOX_SHAPES.includes(named))
150
- return { outline: named };
151
- const glyph = ICONS[named];
152
- if (glyph !== undefined)
153
- return { outline: 'box', body: glyph };
154
- throw new SourceError(`there is no shape called "${named}". The shapes are box, ${BOX_SHAPES.join(', ')}, ` +
155
- `and any icon drawn as the node itself: ${ICON_NAMES.join(', ')}`, line);
162
+ if (shape === 'none')
163
+ return { kind: 'none' };
164
+ if (OUTLINES.includes(shape)) {
165
+ return { kind: 'shape', outline: shape };
166
+ }
167
+ // Until 0.3.0 `shape:` also took an icon name and drew the node as that
168
+ // picture. The two jobs are two parts and now have two keys, so an older file
169
+ // is told which one it wanted rather than being drawn as a plain rectangle.
170
+ if (ICONS[shape] !== undefined) {
171
+ const now = shape === 'instance' ? 'cube' : shape;
172
+ return refuse(`\`shape: ${shape}\` drew the node as a picture, and the picture is now \`icon:\` — ` +
173
+ `try \`icon: ${now}\``, line);
174
+ }
175
+ if (shape === 'instance') {
176
+ return refuse('`shape: instance` is now `icon: cube` — the icon was renamed with it', line);
177
+ }
178
+ if (shape === 'box') {
179
+ return refuse('`shape: box` is now `shape: rectangle`, since nothing else in the vocabulary is abbreviated', line);
180
+ }
181
+ return refuse(`there is no shape called "${shape}". The shapes are ${SHAPE_WORDS.join(', ')}, and a ` +
182
+ `picture is \`icon:\` rather than \`shape:\`: ${ICON_NAMES.join(', ')}`, line);
156
183
  }
157
- export function iconFor(appearance, line) {
158
- const named = appearance['icon'];
159
- if (named === undefined)
160
- return undefined;
184
+ function refuse(message, line) {
185
+ throw new SourceError(message, line);
186
+ }
187
+ /**
188
+ * The icon of that name, or an error listing the set.
189
+ *
190
+ * An unknown name is refused rather than dropped. That is the same rule
191
+ * `DIAGRAM_KEYS` follows and it is here for the same reason: a misspelt
192
+ * `icon: laptp` that quietly draws nothing is indistinguishable from the tool
193
+ * being broken, and an author will stare at the file looking for the mistake in
194
+ * the wrong place. Since the vocabulary is closed and short, the error can list
195
+ * the whole of it.
196
+ */
197
+ export function iconNamed(named, line) {
161
198
  const icon = ICONS[named];
162
199
  if (icon === undefined) {
163
- throw new SourceError(`there is no icon called "${named}". The icons are ${ICON_NAMES.join(', ')}`, line);
200
+ const was = named === 'instance' ? ' — the cube was called `instance` until 0.3.0' : '';
201
+ throw new SourceError(`there is no icon called "${named}". The icons are ${ICON_NAMES.join(', ')}${was}`, line);
164
202
  }
165
203
  return icon;
166
204
  }
package/dist/lexer.d.ts CHANGED
@@ -9,7 +9,7 @@ export interface Token {
9
9
  * runs to the end of the line, so a comment may trail a statement.
10
10
  *
11
11
  * A lone `/` is an ordinary character, which keeps a path or a ratio writable
12
- * unquoted. `#` is ordinary too: it opens a hex colour, which is why comments
12
+ * unquoted. `#` is ordinary too: it opens a hex color, which is why comments
13
13
  * are spelled `//` rather than the `#` an earlier version used.
14
14
  *
15
15
  * Parentheses group the modifiers on a placement — `left of hub (gap: wide)` —
package/dist/lexer.js CHANGED
@@ -5,7 +5,7 @@ import { SourceError } from './errors.js';
5
5
  * runs to the end of the line, so a comment may trail a statement.
6
6
  *
7
7
  * A lone `/` is an ordinary character, which keeps a path or a ratio writable
8
- * unquoted. `#` is ordinary too: it opens a hex colour, which is why comments
8
+ * unquoted. `#` is ordinary too: it opens a hex color, which is why comments
9
9
  * are spelled `//` rather than the `#` an earlier version used.
10
10
  *
11
11
  * Parentheses group the modifiers on a placement — `left of hub (gap: wide)` —
@@ -48,11 +48,12 @@ export function tokenizeLine(line, lineNumber) {
48
48
  const c = line[i];
49
49
  if (c === '\\' && i + 1 < line.length) {
50
50
  const next = line[i + 1];
51
- // `\/` is the one escape that must survive tokenizing. The line break
52
- // it escapes is not resolved until `splitLines`, long after this, so
53
- // collapsing it to a bare `/` here would lose the fact that the
54
- // author asked for a literal. Every other escape resolves now.
55
- text += next === '/' ? '\\/' : next;
51
+ // `\/` and `\[` are the two escapes that must survive tokenizing.
52
+ // Neither of the things they escape is resolved here — the line break
53
+ // waits for `splitRuns` and the markup tag for `parseMarkup` — so
54
+ // collapsing either to a bare character now would lose the fact that
55
+ // the author asked for a literal. Every other escape resolves here.
56
+ text += next === '/' || next === '[' ? `\\${next}` : next;
56
57
  i += 2;
57
58
  continue;
58
59
  }
package/dist/measure.d.ts CHANGED
@@ -20,16 +20,16 @@ export interface Measurer {
20
20
  lineHeight(fontSize: number): number;
21
21
  }
22
22
  /**
23
- * A slash with whitespace on both sides marks a line break, so a label is
23
+ * A slash with whitespace on both sides marks a line break, so a text is
24
24
  * really a short stack of lines. Each line is trimmed; empty ones are dropped.
25
25
  *
26
26
  * The whitespace is what makes the marker safe. Splitting on a bare `/` meant
27
- * no label could contain one, so `TCP/IP` came out as two lines, and so did
27
+ * no text could contain one, so `TCP/IP` came out as two lines, and so did
28
28
  * `16/9`, `I/O` and every path or URL. Requiring the spaces keeps the marker
29
29
  * legible where it is meant — `"Computer 1 / Ubuntu"` — while a
30
30
  * slash inside a word stays an ordinary character.
31
31
  *
32
- * That leaves the label that wants a spaced slash and no break — `Before / After`
32
+ * That leaves the text that wants a spaced slash and no break — `Before / After`
33
33
  * — which writes it `\/`. The lexer preserves that escape rather than resolving
34
34
  * it, so the backslash is still here to suppress the split, and is dropped once
35
35
  * the splitting is done.
package/dist/measure.js CHANGED
@@ -9,16 +9,16 @@
9
9
  * measures exactly.
10
10
  */
11
11
  /**
12
- * A slash with whitespace on both sides marks a line break, so a label is
12
+ * A slash with whitespace on both sides marks a line break, so a text is
13
13
  * really a short stack of lines. Each line is trimmed; empty ones are dropped.
14
14
  *
15
15
  * The whitespace is what makes the marker safe. Splitting on a bare `/` meant
16
- * no label could contain one, so `TCP/IP` came out as two lines, and so did
16
+ * no text could contain one, so `TCP/IP` came out as two lines, and so did
17
17
  * `16/9`, `I/O` and every path or URL. Requiring the spaces keeps the marker
18
18
  * legible where it is meant — `"Computer 1 / Ubuntu"` — while a
19
19
  * slash inside a word stays an ordinary character.
20
20
  *
21
- * That leaves the label that wants a spaced slash and no break — `Before / After`
21
+ * That leaves the text that wants a spaced slash and no break — `Before / After`
22
22
  * — which writes it `\/`. The lexer preserves that escape rather than resolving
23
23
  * it, so the backslash is still here to suppress the split, and is dropped once
24
24
  * the splitting is done.
package/dist/model.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Attrs, Axis, Placement } from './ast.js';
1
+ import type { Attrs, Axis, Kind, Placement } from './ast.js';
2
+ import type { Line } from './text.js';
3
+ import type { Body } from './icons.js';
2
4
  /** A `between` clause with its targets resolved. Mirrors `Passage` in `ast.ts`. */
3
5
  export interface LayoutPassage {
4
6
  nodes: [LayoutNode, LayoutNode];
@@ -6,13 +8,26 @@ export interface LayoutPassage {
6
8
  axis?: Axis;
7
9
  }
8
10
  /** A node with its geometry solved. Coordinates are absolute, origin top-left. */
11
+ /** A distance past each side of a box. */
12
+ export interface Reach {
13
+ left: number;
14
+ top: number;
15
+ right: number;
16
+ bottom: number;
17
+ }
9
18
  export interface LayoutNode {
10
19
  name: string;
11
- kind: 'box' | 'note';
12
- /** The label as written, before line splitting. */
20
+ /** Which set of attributes this node answers to, decided by its body. */
21
+ kind: Exclude<Kind, 'edge'>;
22
+ /**
23
+ * What the node is drawn as, resolved once while the tree is built — so the
24
+ * resolver, which sizes it, and the renderer, which draws it, cannot disagree.
25
+ */
26
+ body: Body;
27
+ /** The text as written, before line splitting. */
13
28
  text: string;
14
- /** The label split into the lines that will be drawn. */
15
- lines: string[];
29
+ /** The text split into the lines that will be drawn, each as its runs. */
30
+ lines: Line[];
16
31
  parent?: LayoutNode;
17
32
  children: LayoutNode[];
18
33
  x: number;
@@ -24,15 +39,60 @@ export interface LayoutNode {
24
39
  * except a deck, where the offset copies sit in that margin.
25
40
  */
26
41
  inset: number;
27
- /** One label per copy behind this node, back to front. Empty for most nodes. */
28
- deckLabels: string[];
42
+ /** One text per copy behind this node, back to front. Empty for most nodes. */
43
+ deckTexts: string[];
29
44
  /**
30
- * Vertical space this node's own label and icon occupy, at whichever end of
31
- * the box `label.at` puts them. Zero for leaves.
45
+ * Vertical space this node's own text and icon occupy, at whichever end of
46
+ * the box `text.at` puts them. Zero for leaves.
32
47
  */
33
48
  headerHeight: number;
34
- /** The label's bracketed modifiers, as written. Usually empty. */
35
- label: Attrs;
49
+ /**
50
+ * How far this node's children stick out past its own box on each side —
51
+ * a child placed `outside` it, or one further down that does. Everything
52
+ * kept clear of this node keeps clear of those too, since they are part of
53
+ * it; alignments still read the node's own box. Zero on every side for a
54
+ * node whose children all sit inside it.
55
+ */
56
+ reach: Reach;
57
+ /**
58
+ * Whether things stack beside this node's text — below it, or above it when
59
+ * the text is at the bottom. That is what a title band *is*, so it is what
60
+ * decides a container's look and its text's defaults, rather than whether
61
+ * the node has children: a node whose only child sits beside its text or in
62
+ * a corner has nothing stacked below the text, and draws as a leaf.
63
+ */
64
+ banded: boolean;
65
+ /**
66
+ * The rectangle this node's own text occupies, as an offset from the node's
67
+ * outer top-left. The *ink* box, not the room it ranges in: for a container
68
+ * that is the title itself, not the width of the band.
69
+ *
70
+ * Worked out by the resolver rather than the renderer because `hub text` is
71
+ * a placement target, so it has to be a number before anything is solved —
72
+ * and because the resolver reserving the room and the renderer filling it
73
+ * must not be able to disagree about where it ended up. The renderer draws
74
+ * from this rather than recomputing it.
75
+ *
76
+ * All zeroes where the node has no text.
77
+ */
78
+ textBox: {
79
+ x: number;
80
+ y: number;
81
+ width: number;
82
+ height: number;
83
+ };
84
+ /**
85
+ * Which way the text ranges in the room it was given, kept because a node
86
+ * can be widened *after* it was sized — `contents: (widths: match)` and
87
+ * `(widths: fill)` both do — and a centered or right-ranged text has to move
88
+ * with the new width. Without it the box grows and the words stay put.
89
+ */
90
+ textSide: 'left' | 'center' | 'right';
91
+ /**
92
+ * What the text's brackets said, with anything a style's `text: (…)`
93
+ * contributed underneath it. Usually empty.
94
+ */
95
+ textAttrs: Attrs;
36
96
  attrs: Attrs;
37
97
  /** Style attributes merged in from a named style, then overridden by the node's own. */
38
98
  appearance: Attrs;
@@ -40,15 +100,19 @@ export interface LayoutNode {
40
100
  placements: Placement[];
41
101
  line: number;
42
102
  }
43
- export interface LayoutLink {
103
+ export interface LayoutEdge {
44
104
  from: LayoutNode;
45
105
  to: LayoutNode;
46
106
  both: boolean;
47
- label?: string;
107
+ text?: string;
108
+ /** The text split into the lines that will be drawn, each as its runs. */
109
+ lines?: Line[];
110
+ /** The text's bracketed modifiers, with a style's `text: (…)` underneath. */
111
+ textAttrs: Attrs;
48
112
  /**
49
113
  * The gap a `between` clause named, with its two nodes resolved. Nothing in
50
114
  * the resolver uses this — a corridor is measured off the solved layout
51
- * rather than solved for, so links stay out of the constraint system entirely.
115
+ * rather than solved for, so edges stay out of the constraint system entirely.
52
116
  */
53
117
  between?: LayoutPassage;
54
118
  attrs: Attrs;
@@ -60,7 +124,13 @@ export interface Layout {
60
124
  nodes: LayoutNode[];
61
125
  /** Top-level nodes only, in declaration order. */
62
126
  roots: LayoutNode[];
63
- links: LayoutLink[];
127
+ edges: LayoutEdge[];
128
+ /**
129
+ * Every style the file's markup names, resolved to the color it lends. The
130
+ * renderer needs it because a marked run borrows its color from a style
131
+ * rather than stating one, and styles are otherwise merged away by here.
132
+ */
133
+ markup: Record<string, string>;
64
134
  /**
65
135
  * What the `diagram` statement said, as written. Nothing here affects
66
136
  * geometry; it rides along so the renderer sees the whole compiled document
@@ -71,7 +141,7 @@ export interface Layout {
71
141
  height: number;
72
142
  /**
73
143
  * The clear band left around the drawing. Kept so the renderer can hold the
74
- * same band open around a link that leaves the boxes' bounds — a curve out of
144
+ * same band open around an edge that leaves the boxes' bounds — a curve out of
75
145
  * a `top` side does exactly that, and the canvas has to grow to hold it.
76
146
  */
77
147
  margin: number;