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.
package/dist/ast.d.ts ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The shape a source file parses into. Nothing here knows about geometry —
3
+ * these types are a faithful record of what the author wrote, and no more.
4
+ */
5
+ export declare const DIRECTIONS: readonly ["above", "below", "left", "right", "above-left", "above-right", "below-left", "below-right"];
6
+ export type Direction = (typeof DIRECTIONS)[number];
7
+ export declare function isDirection(word: string): word is Direction;
8
+ export type Axis = 'x' | 'y';
9
+ /**
10
+ * Which edge of the target a `level with` shares. `centre` is the plain form;
11
+ * the rest are written in front of it, as in `top level with media`.
12
+ */
13
+ export declare const EDGES: readonly ["centre", "top", "bottom", "left", "right"];
14
+ export type Edge = (typeof EDGES)[number];
15
+ /** An edge belongs to one axis, so an alignment never has to say which. */
16
+ export declare const EDGE_AXIS: Record<Edge, Axis>;
17
+ /**
18
+ * Naming more than one target places the node against the box that just bounds
19
+ * them all — `right of borg and bare` clears both. It is a single target that
20
+ * nobody had to declare, which is why it is a list on one placement rather than
21
+ * several placements: two separate `level with` statements are two demands that
22
+ * fight, while one naming two targets is a single demand about one region.
23
+ */
24
+ export type Targets = string[];
25
+ /**
26
+ * `right of docker` — the node sits a gap beyond one of the target's edges.
27
+ * A direction rules out part of an axis rather than fixing a point, which is
28
+ * what lets two of them bracket a node between two targets.
29
+ */
30
+ export interface OffsetPlacement {
31
+ kind: 'offset';
32
+ direction: Direction;
33
+ targets: Targets;
34
+ /**
35
+ * The gap this one placement asks for, from `(gap: wide)` written after the
36
+ * targets. A gap describes a relationship rather than a box, so this is its
37
+ * proper home; `gap:` on the node remains the default for every placement
38
+ * that does not say. Absent means take the node's.
39
+ */
40
+ gap?: string;
41
+ line: number;
42
+ }
43
+ /** `level with docker` — share an edge or a centre line, with no gap in between. */
44
+ export interface AlignPlacement {
45
+ kind: 'align';
46
+ axis: Axis;
47
+ edge: Edge;
48
+ targets: Targets;
49
+ line: number;
50
+ }
51
+ /**
52
+ * One thing the author said about where a node goes. A node carries as many as
53
+ * it needs; the resolver intersects them.
54
+ */
55
+ export type Placement = OffsetPlacement | AlignPlacement;
56
+ /**
57
+ * The modifiers a placement understands, in brackets after its targets. Refused
58
+ * by name when unrecognised, for the reason `DIAGRAM_KEYS` are: a modifier that
59
+ * silently does nothing looks like a bug in the tool rather than a typo.
60
+ */
61
+ export declare const PLACEMENT_KEYS: readonly ["gap"];
62
+ /** How a placement reads back in the author's own words, for error messages. */
63
+ export declare function describePlacement(placement: Placement): string;
64
+ /** "borg", "borg and bare", "borg, bare and media" — as the author would write them. */
65
+ export declare function listTargets(targets: Targets): string;
66
+ /**
67
+ * `between computer1 and computer2` on a link — the gap it passes through.
68
+ *
69
+ * This is not a claim about the whole line. It binds only the stretch where the
70
+ * line is actually passing the pair, and says nothing about where it goes
71
+ * before or after.
72
+ */
73
+ export interface Passage {
74
+ /** Exactly two, because a gap has two sides. */
75
+ targets: Targets;
76
+ /**
77
+ * Which of the two gaps was meant, for a pair that is apart on both axes.
78
+ * Absent whenever the pair leaves only one possibility, which is most of the
79
+ * time — the word is a tie-break, not part of the statement.
80
+ */
81
+ axis?: Axis;
82
+ }
83
+ /** How the axis of a passage is written, and what it means. */
84
+ export declare const PASSAGE_AXES: Record<string, Axis>;
85
+ /** `vertically` or `horizontally`, from the axis it binds. */
86
+ export declare function describeAxis(axis: Axis): string;
87
+ /** `key: value` pairs trailing a statement. Values are always strings here. */
88
+ export type Attrs = Record<string, string>;
89
+ /**
90
+ * What a label's brackets may say: `"Docker" (at: bottom, align: centre)`.
91
+ *
92
+ * They are bracketed onto the label rather than written among the node's
93
+ * attributes for the same reason a gap is bracketed onto its placement — they
94
+ * modify that one thing, and the brackets make the scope visible instead of
95
+ * positional. `at` and `align` are independent: neither implies the other, and a
96
+ * label at the bottom is an ordinary label that happens to be at the bottom.
97
+ */
98
+ export declare const LABEL_KEYS: readonly ["at", "align"];
99
+ export interface BoxStmt {
100
+ kind: 'box';
101
+ name: string;
102
+ text: string;
103
+ /** The label's bracketed modifiers, as written. Usually empty. */
104
+ label: Attrs;
105
+ /** Everything the author said about where this goes. Empty for the anchor. */
106
+ placements: Placement[];
107
+ attrs: Attrs;
108
+ line: number;
109
+ }
110
+ export interface LinkStmt {
111
+ kind: 'link';
112
+ from: string;
113
+ to: string;
114
+ /** `<->` rather than `->`. */
115
+ both: boolean;
116
+ label?: string;
117
+ /** `between computer1 and computer2` — the gap the line passes through. */
118
+ between?: Passage;
119
+ attrs: Attrs;
120
+ line: number;
121
+ }
122
+ export interface NoteStmt {
123
+ kind: 'note';
124
+ name: string;
125
+ text: string;
126
+ /** Everything the author said about where this goes. Empty for the anchor. */
127
+ placements: Placement[];
128
+ attrs: Attrs;
129
+ line: number;
130
+ }
131
+ export interface DeckStmt {
132
+ kind: 'deck';
133
+ /** The container to draw with offset copies behind it. */
134
+ name: string;
135
+ /** One label per copy, back to front as written. */
136
+ labels: string[];
137
+ line: number;
138
+ }
139
+ /**
140
+ * `diagram background: #111111` — settings that belong to the drawing as a
141
+ * whole rather than to anything in it. It has no name because there is only
142
+ * ever one diagram per file.
143
+ */
144
+ export interface DiagramStmt {
145
+ kind: 'diagram';
146
+ attrs: Attrs;
147
+ line: number;
148
+ }
149
+ /** The attributes a `diagram` statement understands. */
150
+ export declare const DIAGRAM_KEYS: readonly ["background"];
151
+ export interface StyleStmt {
152
+ kind: 'style';
153
+ name: string;
154
+ attrs: Attrs;
155
+ line: number;
156
+ }
157
+ export type Stmt = BoxStmt | LinkStmt | NoteStmt | DeckStmt | StyleStmt | DiagramStmt;
158
+ export interface Document {
159
+ statements: Stmt[];
160
+ }
package/dist/ast.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The shape a source file parses into. Nothing here knows about geometry —
3
+ * these types are a faithful record of what the author wrote, and no more.
4
+ */
5
+ export const DIRECTIONS = [
6
+ 'above',
7
+ 'below',
8
+ 'left',
9
+ 'right',
10
+ 'above-left',
11
+ 'above-right',
12
+ 'below-left',
13
+ 'below-right',
14
+ ];
15
+ export function isDirection(word) {
16
+ return DIRECTIONS.includes(word);
17
+ }
18
+ /**
19
+ * Which edge of the target a `level with` shares. `centre` is the plain form;
20
+ * the rest are written in front of it, as in `top level with media`.
21
+ */
22
+ export const EDGES = ['centre', 'top', 'bottom', 'left', 'right'];
23
+ /** An edge belongs to one axis, so an alignment never has to say which. */
24
+ export const EDGE_AXIS = {
25
+ centre: 'y',
26
+ top: 'y',
27
+ bottom: 'y',
28
+ left: 'x',
29
+ right: 'x',
30
+ };
31
+ /**
32
+ * The modifiers a placement understands, in brackets after its targets. Refused
33
+ * by name when unrecognised, for the reason `DIAGRAM_KEYS` are: a modifier that
34
+ * silently does nothing looks like a bug in the tool rather than a typo.
35
+ */
36
+ export const PLACEMENT_KEYS = ['gap'];
37
+ /** How a placement reads back in the author's own words, for error messages. */
38
+ export function describePlacement(placement) {
39
+ const targets = listTargets(placement.targets);
40
+ if (placement.kind === 'align') {
41
+ const edge = placement.edge === 'centre' ? '' : `${placement.edge} `;
42
+ return `${edge}level with ${targets}`;
43
+ }
44
+ // "left of X" and "above X" are both good English; "above of X" is not.
45
+ const joiner = placement.direction === 'above' || placement.direction === 'below' ? '' : 'of ';
46
+ const gap = placement.gap === undefined ? '' : ` (gap: ${placement.gap})`;
47
+ return `${placement.direction} ${joiner}${targets}${gap}`;
48
+ }
49
+ /** "borg", "borg and bare", "borg, bare and media" — as the author would write them. */
50
+ export function listTargets(targets) {
51
+ if (targets.length <= 1)
52
+ return targets[0] ?? '';
53
+ return `${targets.slice(0, -1).join(', ')} and ${targets[targets.length - 1]}`;
54
+ }
55
+ /** How the axis of a passage is written, and what it means. */
56
+ export const PASSAGE_AXES = {
57
+ // The gap you measure with a vertical ruler: one target above, one below. A
58
+ // line running along it therefore travels horizontally, which is the reading
59
+ // to watch out for — the word describes the gap, not the direction of travel.
60
+ vertically: 'y',
61
+ horizontally: 'x',
62
+ };
63
+ /** `vertically` or `horizontally`, from the axis it binds. */
64
+ export function describeAxis(axis) {
65
+ return axis === 'y' ? 'vertically' : 'horizontally';
66
+ }
67
+ /**
68
+ * What a label's brackets may say: `"Docker" (at: bottom, align: centre)`.
69
+ *
70
+ * They are bracketed onto the label rather than written among the node's
71
+ * attributes for the same reason a gap is bracketed onto its placement — they
72
+ * modify that one thing, and the brackets make the scope visible instead of
73
+ * positional. `at` and `align` are independent: neither implies the other, and a
74
+ * label at the bottom is an ordinary label that happens to be at the bottom.
75
+ */
76
+ export const LABEL_KEYS = ['at', 'align'];
77
+ /** The attributes a `diagram` statement understands. */
78
+ export const DIAGRAM_KEYS = ['background'];
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, resolve as resolvePath } from 'node:path';
4
+ import { SourceError } from './errors.js';
5
+ import { compile } from './index.js';
6
+ const USAGE = `reladraw — render a diagram from stated placement
7
+
8
+ reladraw <input.reladraw> [-o <output.svg>]
9
+
10
+ -o, --out where to write the SVG. Defaults to the input path with
11
+ its extension replaced by .svg. Use - for standard output.
12
+ -h, --help print this.
13
+ `;
14
+ async function main(argv) {
15
+ if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
16
+ process.stdout.write(USAGE);
17
+ return argv.length === 0 ? 1 : 0;
18
+ }
19
+ let input;
20
+ let out;
21
+ for (let i = 0; i < argv.length; i += 1) {
22
+ const arg = argv[i];
23
+ if (arg === '-o' || arg === '--out') {
24
+ out = argv[i + 1];
25
+ if (out === undefined) {
26
+ process.stderr.write('reladraw: -o needs a path\n');
27
+ return 1;
28
+ }
29
+ i += 1;
30
+ }
31
+ else if (arg.startsWith('-') && arg !== '-') {
32
+ process.stderr.write(`reladraw: unknown option ${arg}\n`);
33
+ return 1;
34
+ }
35
+ else if (input === undefined) {
36
+ input = arg;
37
+ }
38
+ else {
39
+ process.stderr.write(`reladraw: unexpected argument ${arg}\n`);
40
+ return 1;
41
+ }
42
+ }
43
+ if (input === undefined) {
44
+ process.stderr.write('reladraw: no input file\n');
45
+ return 1;
46
+ }
47
+ const source = await readFile(input, 'utf8');
48
+ let svg;
49
+ try {
50
+ svg = compile(source);
51
+ }
52
+ catch (error) {
53
+ if (error instanceof SourceError) {
54
+ process.stderr.write(`${error.format(input)}\n`);
55
+ return 1;
56
+ }
57
+ throw error;
58
+ }
59
+ if (out === '-') {
60
+ process.stdout.write(svg);
61
+ return 0;
62
+ }
63
+ const target = resolvePath(out ?? input.replace(/\.[^.]+$/, '') + '.svg');
64
+ await mkdir(dirname(target), { recursive: true });
65
+ await writeFile(target, svg, 'utf8');
66
+ process.stderr.write(`${target}\n`);
67
+ return 0;
68
+ }
69
+ main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
70
+ process.stderr.write(`reladraw: ${error instanceof Error ? error.message : String(error)}\n`);
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,142 @@
1
+ /** Spacing and text sizes shared by the resolver and the renderer, so the two cannot drift. */
2
+ import type { Attrs } from './ast.js';
3
+ import type { Measurer } from './measure.js';
4
+ /** Inside a box, between its border and its contents. */
5
+ export declare const PAD = 14;
6
+ /** Between stacked children of one container. */
7
+ export declare const CHILD_GAP = 10;
8
+ /** Between a container's own label and its first child. */
9
+ export declare const HEADER_GAP = 10;
10
+ /**
11
+ * How far each deck copy is offset behind the front face. It has to clear a
12
+ * whole line of text plus the padding above it, or a copy's label is drawn and
13
+ * then immediately covered by the copy in front of it.
14
+ */
15
+ export declare const DECK_STEP = 34;
16
+ /**
17
+ * The named gaps a placement may ask for, each a *minimum* distance rather than a
18
+ * fixed one. Anything the author puts between two things widens the space
19
+ * between them on its own, so no gap here ever has to be chosen large enough
20
+ * to leave room for something else.
21
+ */
22
+ export declare const GAPS: Record<string, number>;
23
+ /**
24
+ * How far apart two boxes are pushed when they would otherwise overlap. Small
25
+ * on purpose: this is the tool enforcing something the author did not write, so
26
+ * the space it leaves should read as "these are not the same box" and never as
27
+ * a relationship someone stated. Say `gap:` if you want breathing room.
28
+ */
29
+ export declare const SEPARATION_GAP: number;
30
+ /**
31
+ * Between two links meeting the same side of the same box. An author names a
32
+ * side, never a point on it, so this is the tool keeping two attachments apart
33
+ * rather than a distance anyone asked for — small, like `SEPARATION_GAP`, and
34
+ * squeezed further if the side is too short to hold the whole group.
35
+ */
36
+ export declare const ATTACH_STEP = 16;
37
+ /** Kept clear at each end of a side, so an attachment never sits on a corner. */
38
+ export declare const ATTACH_MARGIN = 10;
39
+ /** How wide a link's line is drawn. */
40
+ export declare const LINE_WIDTH = 1.6;
41
+ /**
42
+ * The arrowhead's length, in the `markerUnits="strokeWidth"` the marker is
43
+ * declared in, so its drawn length is this times `LINE_WIDTH`.
44
+ */
45
+ export declare const ARROW_MARKER_WIDTH = 7;
46
+ /**
47
+ * How much of the line an arrowhead covers. Derived rather than written down,
48
+ * because the resolver reserves it and the renderer draws it, and a number
49
+ * agreed by coincidence is a number that drifts.
50
+ */
51
+ export declare const ARROW_LENGTH: number;
52
+ /**
53
+ * Line left showing between a link's label and the box at that end of the
54
+ * corridor it crosses.
55
+ *
56
+ * Unlike `SEPARATION_GAP` and `ATTACH_MARGIN` this is not "small on purpose".
57
+ * Those two keep two things from touching, and the least distance that reads as
58
+ * "not touching" is the right one. This one has something to show: a label sits
59
+ * in a knockout that erases the line behind it, so whatever is left either side
60
+ * is the entire evidence that the label belongs to a link at all. At ten pixels
61
+ * it did not read as a line — the seed diagram in the playground drew as a word
62
+ * with a dash beside it — so it is the length of a run of line, not a margin.
63
+ *
64
+ * Say `gap:` if you want the corridor wider than its contents.
65
+ */
66
+ export declare const LABEL_CLEARANCE = 20;
67
+ /**
68
+ * How much room a link's label takes along one axis.
69
+ *
70
+ * The knockout rectangle drawn behind a label is the text plus five either side,
71
+ * so that rectangle, not the glyphs, is what must not overlap anything.
72
+ *
73
+ * Shared by the resolver, which widens a corridor to hold a label, and the
74
+ * renderer, which spaces the lanes of a channel by it, so the two cannot
75
+ * disagree about how much room a label needs. The two ask different questions of
76
+ * it and both are right: the resolver measures *along* the run, so a link
77
+ * travelling horizontally needs the label's width; the renderer measures *across*
78
+ * the channel, so a link travelling horizontally down one needs its height.
79
+ */
80
+ export declare function labelExtent(label: string, appearance: Attrs, axis: 'x' | 'y', measurer: Measurer, fontSize: number, line: number): number;
81
+ /**
82
+ * An icon is two lines of the label tall, and that ratio is what makes it a
83
+ * *label-sized* ornament rather than a picture with a size of its own. It is
84
+ * read off the reference, where the title lines run 25 pixels baseline to
85
+ * baseline and the drive and machine glyphs are close to 50 tall. Deriving it
86
+ * from the text also means an icon on a `size: small` node shrinks with it,
87
+ * which is what anyone would expect and what a fixed pixel count would not do.
88
+ */
89
+ export declare const ICON_LINES = 2;
90
+ /** Between the label column and the icon column beside it. */
91
+ export declare const ICON_GAP = 10;
92
+ export declare const DEFAULT_FONT_SIZE = 14;
93
+ /**
94
+ * The named text sizes, each a multiple of the document's own size. Named
95
+ * rather than numeric for the reason gaps are: a number here is typography by
96
+ * coordinate. It goes stale the moment the document is set at another size, and
97
+ * it says nothing about why one piece of text is smaller than another.
98
+ *
99
+ * `small` is sampled rather than chosen. In
100
+ * `examples/reference/arch.png` the box and container labels run 25
101
+ * pixels baseline to baseline and every annotation runs 21, which is this
102
+ * ratio; `./dev.sh textrows` is how that was read off. `large` is the same step
103
+ * taken the other way, so the scale is symmetric about the document size.
104
+ */
105
+ export declare const TEXT_SIZES: Record<string, number>;
106
+ /**
107
+ * The size a piece of text is set at. Shared by the resolver, which reserves
108
+ * the room, and the renderer, which fills it, so the two cannot disagree about
109
+ * how much room there is.
110
+ */
111
+ export declare function fontSizeFor(kind: string, appearance: Attrs, fontSize: number, line: number): number;
112
+ export declare const DEFAULT_MARGIN = 40;
113
+ /**
114
+ * Where a container's own label sits. Every container reserves a band for its
115
+ * label and its icon; `at` says which end of the box that band is, and the
116
+ * contents take what is left. `align` says how the text sits across it.
117
+ *
118
+ * The two are independent and neither implies the other. A label at the bottom
119
+ * is an ordinary label that happens to be at the bottom — there is no kind of
120
+ * label being named here and no second thing quietly coming along with the
121
+ * first. An earlier version bundled them as `label: heading | caption`, which
122
+ * read a position as though it were a meaning; a folded corner means "artifact
123
+ * rather than process" and a reader decodes it, while "lower down" means only
124
+ * lower down.
125
+ *
126
+ * A leaf has no band — its label is centred in the box — so neither says
127
+ * anything about one.
128
+ */
129
+ export declare const LABEL_ENDS: readonly ["top", "bottom"];
130
+ export type LabelEnd = (typeof LABEL_ENDS)[number];
131
+ export interface LabelStyle {
132
+ /** Which end of the box the band sits at. */
133
+ at: LabelEnd;
134
+ /** How the text sits in the band, in the renderer's own vocabulary. */
135
+ align: 'start' | 'middle' | 'end';
136
+ }
137
+ /**
138
+ * Read a label's bracketed modifiers. Shared by the resolver, which offsets the
139
+ * contents away from the band, and the renderer, which draws into it, so the two
140
+ * cannot disagree about which end the band is at.
141
+ */
142
+ export declare function labelStyleFor(label: Attrs, line: number): LabelStyle;
@@ -0,0 +1,187 @@
1
+ import { SourceError } from './errors.js';
2
+ /** Inside a box, between its border and its contents. */
3
+ export const PAD = 14;
4
+ /** Between stacked children of one container. */
5
+ export const CHILD_GAP = 10;
6
+ /** Between a container's own label and its first child. */
7
+ export const HEADER_GAP = 10;
8
+ /**
9
+ * 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
11
+ * then immediately covered by the copy in front of it.
12
+ */
13
+ export const DECK_STEP = 34;
14
+ /**
15
+ * The named gaps a placement may ask for, each a *minimum* distance rather than a
16
+ * fixed one. Anything the author puts between two things widens the space
17
+ * between them on its own, so no gap here ever has to be chosen large enough
18
+ * to leave room for something else.
19
+ */
20
+ export const GAPS = {
21
+ // A zero gap turns an offset into edge-to-edge contact, so "my top edge
22
+ // against Docker's bottom edge" needs no vocabulary of its own.
23
+ none: 0,
24
+ tight: 24,
25
+ normal: 56,
26
+ wide: 110,
27
+ };
28
+ /**
29
+ * How far apart two boxes are pushed when they would otherwise overlap. Small
30
+ * on purpose: this is the tool enforcing something the author did not write, so
31
+ * the space it leaves should read as "these are not the same box" and never as
32
+ * a relationship someone stated. Say `gap:` if you want breathing room.
33
+ */
34
+ export const SEPARATION_GAP = GAPS['tight'];
35
+ /**
36
+ * Between two links meeting the same side of the same box. An author names a
37
+ * side, never a point on it, so this is the tool keeping two attachments apart
38
+ * rather than a distance anyone asked for — small, like `SEPARATION_GAP`, and
39
+ * squeezed further if the side is too short to hold the whole group.
40
+ */
41
+ export const ATTACH_STEP = 16;
42
+ /** Kept clear at each end of a side, so an attachment never sits on a corner. */
43
+ export const ATTACH_MARGIN = 10;
44
+ /** How wide a link's line is drawn. */
45
+ export const LINE_WIDTH = 1.6;
46
+ /**
47
+ * The arrowhead's length, in the `markerUnits="strokeWidth"` the marker is
48
+ * declared in, so its drawn length is this times `LINE_WIDTH`.
49
+ */
50
+ export const ARROW_MARKER_WIDTH = 7;
51
+ /**
52
+ * How much of the line an arrowhead covers. Derived rather than written down,
53
+ * because the resolver reserves it and the renderer draws it, and a number
54
+ * agreed by coincidence is a number that drifts.
55
+ */
56
+ export const ARROW_LENGTH = ARROW_MARKER_WIDTH * LINE_WIDTH;
57
+ /**
58
+ * Line left showing between a link's label and the box at that end of the
59
+ * corridor it crosses.
60
+ *
61
+ * Unlike `SEPARATION_GAP` and `ATTACH_MARGIN` this is not "small on purpose".
62
+ * 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
64
+ * 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
66
+ * it did not read as a line — the seed diagram in the playground drew as a word
67
+ * with a dash beside it — so it is the length of a run of line, not a margin.
68
+ *
69
+ * Say `gap:` if you want the corridor wider than its contents.
70
+ */
71
+ export const LABEL_CLEARANCE = 20;
72
+ /**
73
+ * How much room a link's label takes along one axis.
74
+ *
75
+ * The knockout rectangle drawn behind a label is the text plus five either side,
76
+ * so that rectangle, not the glyphs, is what must not overlap anything.
77
+ *
78
+ * Shared by the resolver, which widens a corridor to hold a label, and the
79
+ * 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
+ * travelling horizontally needs the label's width; the renderer measures *across*
83
+ * the channel, so a link travelling 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);
88
+ return axis === 'x' ? width + 10 : lines.length * measurer.lineHeight(size);
89
+ }
90
+ /**
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
+ * read off the reference, where the title lines run 25 pixels baseline to
94
+ * baseline and the drive and machine glyphs are close to 50 tall. Deriving it
95
+ * from the text also means an icon on a `size: small` node shrinks with it,
96
+ * which is what anyone would expect and what a fixed pixel count would not do.
97
+ */
98
+ export const ICON_LINES = 2;
99
+ /** Between the label column and the icon column beside it. */
100
+ export const ICON_GAP = 10;
101
+ export const DEFAULT_FONT_SIZE = 14;
102
+ /**
103
+ * The named text sizes, each a multiple of the document's own size. Named
104
+ * rather than numeric for the reason gaps are: a number here is typography by
105
+ * coordinate. It goes stale the moment the document is set at another size, and
106
+ * it says nothing about why one piece of text is smaller than another.
107
+ *
108
+ * `small` is sampled rather than chosen. In
109
+ * `examples/reference/arch.png` the box and container labels run 25
110
+ * pixels baseline to baseline and every annotation runs 21, which is this
111
+ * ratio; `./dev.sh textrows` is how that was read off. `large` is the same step
112
+ * taken the other way, so the scale is symmetric about the document size.
113
+ */
114
+ export const TEXT_SIZES = {
115
+ small: 21 / 25,
116
+ normal: 1,
117
+ large: 25 / 21,
118
+ };
119
+ /**
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 colour.
125
+ */
126
+ const DEFAULT_TEXT_SIZE = { note: 'small' };
127
+ /**
128
+ * The size a piece of text is set at. Shared by the resolver, which reserves
129
+ * the room, and the renderer, which fills it, so the two cannot disagree about
130
+ * how much room there is.
131
+ */
132
+ export function fontSizeFor(kind, appearance, fontSize, line) {
133
+ const named = appearance['size'] ?? DEFAULT_TEXT_SIZE[kind] ?? 'normal';
134
+ const scale = TEXT_SIZES[named];
135
+ if (scale === undefined) {
136
+ throw new SourceError(`size takes ${Object.keys(TEXT_SIZES).join(', ')}, not "${named}"`, line);
137
+ }
138
+ return Math.round(fontSize * scale);
139
+ }
140
+ 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 — its label is centred in the box — so neither says
155
+ * anything about one.
156
+ */
157
+ export const LABEL_ENDS = ['top', 'bottom'];
158
+ /**
159
+ * Author's word to the SVG's. Both spellings of the middle one are taken: this
160
+ * is a vocabulary an author types from memory, and being right about the
161
+ * arrangement and wrong about a dialect is not a mistake worth an error.
162
+ */
163
+ const LABEL_ALIGNMENTS = {
164
+ left: 'start',
165
+ centre: 'middle',
166
+ center: 'middle',
167
+ right: 'end',
168
+ };
169
+ /**
170
+ * Read a label's bracketed modifiers. Shared by the resolver, which offsets the
171
+ * contents away from the band, and the renderer, which draws into it, so the two
172
+ * cannot disagree about which end the band is at.
173
+ */
174
+ export function labelStyleFor(label, line) {
175
+ const at = label['at'];
176
+ if (at !== undefined && !LABEL_ENDS.includes(at)) {
177
+ throw new SourceError(`a label's at takes ${LABEL_ENDS.join(' or ')}, not "${at}"`, line);
178
+ }
179
+ const align = label['align'];
180
+ if (align !== undefined && LABEL_ALIGNMENTS[align] === undefined) {
181
+ throw new SourceError(`a label's align takes left, centre or right, not "${align}"`, line);
182
+ }
183
+ return {
184
+ at: at ?? 'top',
185
+ align: align === undefined ? 'start' : LABEL_ALIGNMENTS[align],
186
+ };
187
+ }