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/LICENSE +202 -0
- package/NOTICE +14 -0
- package/README.md +133 -2
- package/SYNTAX.md +534 -0
- package/dist/ast.d.ts +160 -0
- package/dist/ast.js +78 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +72 -0
- package/dist/constants.d.ts +142 -0
- package/dist/constants.js +187 -0
- package/dist/constrain.d.ts +56 -0
- package/dist/constrain.js +95 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +14 -0
- package/dist/grammar.d.ts +103 -0
- package/dist/grammar.js +215 -0
- package/dist/icons.d.ts +86 -0
- package/dist/icons.js +166 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +16 -0
- package/dist/lexer.d.ts +25 -0
- package/dist/lexer.js +91 -0
- package/dist/measure.d.ts +38 -0
- package/dist/measure.js +66 -0
- package/dist/model.d.ts +78 -0
- package/dist/model.js +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +418 -0
- package/dist/render.d.ts +31 -0
- package/dist/render.js +1164 -0
- package/dist/resolve.d.ts +22 -0
- package/dist/resolve.js +1012 -0
- package/package.json +42 -4
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
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
|
+
}
|