reladraw 0.0.1 → 0.2.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 +135 -2
- package/SYNTAX.md +617 -0
- package/dist/ast.d.ts +228 -0
- package/dist/ast.js +171 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +72 -0
- package/dist/constants.d.ts +145 -0
- package/dist/constants.js +189 -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 +459 -0
- package/dist/render.d.ts +31 -0
- package/dist/render.js +1180 -0
- package/dist/resolve.d.ts +22 -0
- package/dist/resolve.js +1130 -0
- package/package.json +42 -4
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
* 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);
|
|
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 color.
|
|
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, 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
|
+
/**
|
|
162
|
+
* 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,
|
|
164
|
+
* and every document and example has to pick one of them anyway.
|
|
165
|
+
*/
|
|
166
|
+
const LABEL_ALIGNMENTS = {
|
|
167
|
+
left: 'start',
|
|
168
|
+
center: 'middle',
|
|
169
|
+
right: 'end',
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Read a label's bracketed modifiers. Shared by the resolver, which offsets the
|
|
173
|
+
* contents away from the band, and the renderer, which draws into it, so the two
|
|
174
|
+
* cannot disagree about which end the band is at.
|
|
175
|
+
*/
|
|
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);
|
|
180
|
+
}
|
|
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);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
at: at ?? 'top',
|
|
187
|
+
align: align === undefined ? fallbackAlign : LABEL_ALIGNMENTS[align],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Placement } from './ast.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tightest arrangement satisfying a set of relative distances.
|
|
4
|
+
*
|
|
5
|
+
* Every placement in the language turns into the same shape of fact: one thing sits
|
|
6
|
+
* at least so far along an axis from another. Written as `pos[to] >= pos[from]
|
|
7
|
+
* + weight`, a whole diagram is a system of those, and the arrangement the
|
|
8
|
+
* author meant is the one where nothing is further apart than it has to be.
|
|
9
|
+
*
|
|
10
|
+
* That arrangement is unique and is found by longest paths, so no search
|
|
11
|
+
* happens and no alternative is ever weighed. Distances come out of the
|
|
12
|
+
* arithmetic; which side of what a thing sits on came from the author.
|
|
13
|
+
*/
|
|
14
|
+
export interface Constraint {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
weight: number;
|
|
18
|
+
/** The placement this came from, so a failure can be reported in the author's words. */
|
|
19
|
+
placement?: Placement;
|
|
20
|
+
}
|
|
21
|
+
/** An exact distance, which is two inequalities pointing opposite ways. */
|
|
22
|
+
export declare function fix(from: number, to: number, distance: number, placement?: Placement): Constraint[];
|
|
23
|
+
export interface Contradiction {
|
|
24
|
+
/** The placements that cannot all hold. Empty if the loop is entirely implicit. */
|
|
25
|
+
placements: Placement[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Solve, or report the placements that fight.
|
|
29
|
+
*
|
|
30
|
+
* Longest path from a virtual source joined to everything at zero, which is
|
|
31
|
+
* Bellman-Ford with the comparison flipped. A distance that keeps growing after
|
|
32
|
+
* one pass per node means the constraints run in a circle that demands ever
|
|
33
|
+
* more room, and that circle is exactly the set of placements to quote back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function tightest(count: number, constraints: Constraint[]): {
|
|
36
|
+
positions: number[];
|
|
37
|
+
} | {
|
|
38
|
+
contradiction: Contradiction;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Which members can be driven apart from which, along one axis.
|
|
42
|
+
*
|
|
43
|
+
* A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
|
|
44
|
+
* below and leaves it free to move further away. Follow those edges and the
|
|
45
|
+
* question "may this one end up beyond that one?" becomes plain reachability:
|
|
46
|
+
* if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
|
|
47
|
+
* lets the distance from `i` to `j` grow without limit and never lets it be
|
|
48
|
+
* closed from the other side. Pushing `j` past `i` is then the one separation
|
|
49
|
+
* the file allows, and no choice was made.
|
|
50
|
+
*
|
|
51
|
+
* Reachable both ways means the two are pinned at a fixed distance, so they
|
|
52
|
+
* cannot be separated along this axis at all. Reachable neither way means the
|
|
53
|
+
* file said nothing that orders them, which is the case the caller must refuse
|
|
54
|
+
* rather than guess at.
|
|
55
|
+
*/
|
|
56
|
+
export declare function reachability(count: number, constraints: Constraint[]): boolean[][];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** An exact distance, which is two inequalities pointing opposite ways. */
|
|
2
|
+
export function fix(from, to, distance, placement) {
|
|
3
|
+
return [
|
|
4
|
+
{ from, to, weight: distance, ...(placement ? { placement } : {}) },
|
|
5
|
+
{ from: to, to: from, weight: -distance, ...(placement ? { placement } : {}) },
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Solve, or report the placements that fight.
|
|
10
|
+
*
|
|
11
|
+
* Longest path from a virtual source joined to everything at zero, which is
|
|
12
|
+
* Bellman-Ford with the comparison flipped. A distance that keeps growing after
|
|
13
|
+
* one pass per node means the constraints run in a circle that demands ever
|
|
14
|
+
* more room, and that circle is exactly the set of placements to quote back.
|
|
15
|
+
*/
|
|
16
|
+
export function tightest(count, constraints) {
|
|
17
|
+
const positions = new Array(count).fill(0);
|
|
18
|
+
const cameFrom = new Array(count).fill(undefined);
|
|
19
|
+
for (let pass = 0; pass < count; pass += 1) {
|
|
20
|
+
let moved = false;
|
|
21
|
+
for (const constraint of constraints) {
|
|
22
|
+
const candidate = positions[constraint.from] + constraint.weight;
|
|
23
|
+
if (candidate > positions[constraint.to] + 1e-9) {
|
|
24
|
+
positions[constraint.to] = candidate;
|
|
25
|
+
cameFrom[constraint.to] = constraint;
|
|
26
|
+
moved = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!moved)
|
|
30
|
+
return { positions };
|
|
31
|
+
}
|
|
32
|
+
// Still moving after a full pass per node, so some loop demands more room
|
|
33
|
+
// every time round it. Walk backwards to find it.
|
|
34
|
+
for (const constraint of constraints) {
|
|
35
|
+
if (positions[constraint.from] + constraint.weight > positions[constraint.to] + 1e-9) {
|
|
36
|
+
return { contradiction: { placements: loopFrom(constraint.to, cameFrom, count) } };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { contradiction: { placements: [] } };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Which members can be driven apart from which, along one axis.
|
|
43
|
+
*
|
|
44
|
+
* A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
|
|
45
|
+
* below and leaves it free to move further away. Follow those edges and the
|
|
46
|
+
* question "may this one end up beyond that one?" becomes plain reachability:
|
|
47
|
+
* if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
|
|
48
|
+
* lets the distance from `i` to `j` grow without limit and never lets it be
|
|
49
|
+
* closed from the other side. Pushing `j` past `i` is then the one separation
|
|
50
|
+
* the file allows, and no choice was made.
|
|
51
|
+
*
|
|
52
|
+
* Reachable both ways means the two are pinned at a fixed distance, so they
|
|
53
|
+
* cannot be separated along this axis at all. Reachable neither way means the
|
|
54
|
+
* file said nothing that orders them, which is the case the caller must refuse
|
|
55
|
+
* rather than guess at.
|
|
56
|
+
*/
|
|
57
|
+
export function reachability(count, constraints) {
|
|
58
|
+
const reach = Array.from({ length: count }, () => new Array(count).fill(false));
|
|
59
|
+
for (const constraint of constraints)
|
|
60
|
+
reach[constraint.from][constraint.to] = true;
|
|
61
|
+
for (let via = 0; via < count; via += 1) {
|
|
62
|
+
for (let from = 0; from < count; from += 1) {
|
|
63
|
+
if (!reach[from][via])
|
|
64
|
+
continue;
|
|
65
|
+
for (let to = 0; to < count; to += 1) {
|
|
66
|
+
if (reach[via][to])
|
|
67
|
+
reach[from][to] = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return reach;
|
|
72
|
+
}
|
|
73
|
+
/** The placements on the loop reached by following each position back to what set it. */
|
|
74
|
+
function loopFrom(start, cameFrom, count) {
|
|
75
|
+
let at = start;
|
|
76
|
+
for (let step = 0; step < count; step += 1) {
|
|
77
|
+
const previous = cameFrom[at];
|
|
78
|
+
if (previous === undefined)
|
|
79
|
+
break;
|
|
80
|
+
at = previous.from;
|
|
81
|
+
}
|
|
82
|
+
const placements = [];
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
let cursor = at;
|
|
85
|
+
while (!seen.has(cursor)) {
|
|
86
|
+
seen.add(cursor);
|
|
87
|
+
const previous = cameFrom[cursor];
|
|
88
|
+
if (previous === undefined)
|
|
89
|
+
break;
|
|
90
|
+
if (previous.placement)
|
|
91
|
+
placements.push(previous.placement);
|
|
92
|
+
cursor = previous.from;
|
|
93
|
+
}
|
|
94
|
+
return placements.reverse();
|
|
95
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A problem in the source, reported in the vocabulary of the source. */
|
|
2
|
+
export declare class SourceError extends Error {
|
|
3
|
+
readonly line: number;
|
|
4
|
+
constructor(message: string, line: number);
|
|
5
|
+
/** `12: two placements for "server"` — the form the command-line tool prints. */
|
|
6
|
+
format(file?: string): string;
|
|
7
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A problem in the source, reported in the vocabulary of the source. */
|
|
2
|
+
export class SourceError extends Error {
|
|
3
|
+
line;
|
|
4
|
+
constructor(message, line) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.line = line;
|
|
7
|
+
this.name = 'SourceError';
|
|
8
|
+
}
|
|
9
|
+
/** `12: two placements for "server"` — the form the command-line tool prints. */
|
|
10
|
+
format(file) {
|
|
11
|
+
const where = file ? `${file}:${this.line}` : `line ${this.line}`;
|
|
12
|
+
return `${where}: ${this.message}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language's *lexical* vocabulary, and a scanner that classifies one line of
|
|
3
|
+
* source into colored spans.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately separate from `parser.ts`, and it is not a second parser.
|
|
6
|
+
* The parser answers "what does this file mean" and refuses anything it cannot
|
|
7
|
+
* answer for; a highlighter has to color a half-typed line without complaint,
|
|
8
|
+
* so it answers only "what kind of word is this" and never fails. Every rule
|
|
9
|
+
* below is a regex applied to a single line, in priority order, with one bit of
|
|
10
|
+
* carried state (whether anything has been seen on the line yet, and whether the
|
|
11
|
+
* previous token was an attribute key).
|
|
12
|
+
*
|
|
13
|
+
* **That restriction is the point, and it is what makes this reusable.** An
|
|
14
|
+
* Emacs `font-lock-keywords` list, a Vim `syntax match` file and a TextMate
|
|
15
|
+
* grammar are all exactly this: an ordered list of single-line regexes with a
|
|
16
|
+
* face attached. So the vocabulary here — which is the half that goes stale, and
|
|
17
|
+
* which is imported from `ast.ts`, `constants.ts` and `icons.ts` rather than
|
|
18
|
+
* retyped — can be emitted into any of those without this file being ported.
|
|
19
|
+
* The scanner is the JavaScript consumer of the same spec, and the playground is
|
|
20
|
+
* its only caller.
|
|
21
|
+
*
|
|
22
|
+
* Nothing in the language spans lines: `//` runs to the end of one, a string
|
|
23
|
+
* closes on one, and there are no blocks. That is the property those editor
|
|
24
|
+
* formats need and the reason a `.reladraw` grammar is small in all of them.
|
|
25
|
+
*/
|
|
26
|
+
/** What a span of source is, for coloring. */
|
|
27
|
+
export type TokenKind =
|
|
28
|
+
/** `// to the end of the line` */
|
|
29
|
+
'comment'
|
|
30
|
+
/** A quoted string, quotes included. Unterminated ones count, so typing is quiet. */
|
|
31
|
+
| 'string'
|
|
32
|
+
/** The word a statement opens with: `box`, `link`, … */
|
|
33
|
+
| 'keyword'
|
|
34
|
+
/** The name a statement declares, right after its keyword. */
|
|
35
|
+
| 'name'
|
|
36
|
+
/** `->` and `<->`. */
|
|
37
|
+
| 'arrow'
|
|
38
|
+
/** Placement and link vocabulary: `right`, `of`, `level`, `with`, `and`, `between`, … */
|
|
39
|
+
| 'relation'
|
|
40
|
+
/** A `key:` opening an attribute or a bracketed modifier. */
|
|
41
|
+
| 'attribute'
|
|
42
|
+
/** The single word an attribute takes. */
|
|
43
|
+
| 'value'
|
|
44
|
+
/** `#14532d`, wherever it appears. */
|
|
45
|
+
| 'color'
|
|
46
|
+
/** `(` and `)` around a placement's or a label's modifiers. */
|
|
47
|
+
| 'bracket'
|
|
48
|
+
/** Everything else: node names being referred to, and whitespace. */
|
|
49
|
+
| 'plain';
|
|
50
|
+
export interface Span {
|
|
51
|
+
kind: TokenKind;
|
|
52
|
+
/** Half-open, in characters, into the line it came from. */
|
|
53
|
+
start: number;
|
|
54
|
+
end: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The words a statement may open with. `parseStatement` in `parser.ts` is the
|
|
58
|
+
* authority — its switch is what actually accepts them — and this list mirrors
|
|
59
|
+
* it. A word missing here is a word that draws in the plain color, which is a
|
|
60
|
+
* dull page rather than a wrong one.
|
|
61
|
+
*/
|
|
62
|
+
export declare const STATEMENT_KEYWORDS: readonly ["box", "note", "link", "deck", "style", "diagram"];
|
|
63
|
+
/**
|
|
64
|
+
* Every word that says something about where a thing goes. Assembled from the
|
|
65
|
+
* lists the parser itself reads, so a direction or a passage axis added there
|
|
66
|
+
* colors here without anybody remembering to come back.
|
|
67
|
+
*/
|
|
68
|
+
export declare const RELATION_WORDS: string[];
|
|
69
|
+
/**
|
|
70
|
+
* The patterns, as source strings, so a generator can emit them into another
|
|
71
|
+
* editor's grammar without reaching into a compiled RegExp. Each is written to
|
|
72
|
+
* match at the point the scanner has reached; the scanner adds the sticky flag.
|
|
73
|
+
*
|
|
74
|
+
* A name may contain dots (containment) and hyphens, which is why the word
|
|
75
|
+
* pattern is what it is rather than `\w+`.
|
|
76
|
+
*/
|
|
77
|
+
export declare const PATTERNS: {
|
|
78
|
+
readonly comment: "\\/\\/.*";
|
|
79
|
+
readonly string: "\"(?:\\\\.|[^\"\\\\])*\"?";
|
|
80
|
+
readonly keyword: `(?:${string})\\b`;
|
|
81
|
+
readonly arrow: "<->|->|<-";
|
|
82
|
+
readonly attribute: "[A-Za-z][A-Za-z0-9_-]*:";
|
|
83
|
+
readonly color: "#[0-9A-Fa-f]{3,8}\\b";
|
|
84
|
+
readonly relation: `(?:${string})\\b`;
|
|
85
|
+
readonly bracket: "[()]";
|
|
86
|
+
readonly word: "(?:[^\\s()\"\\/]|\\/(?!\\/))(?:[^\\s\"\\/]|\\/(?!\\/))*";
|
|
87
|
+
/**
|
|
88
|
+
* The same, inside an open bracket, where `)` closes the group instead of
|
|
89
|
+
* being an ordinary character. An editor grammar that cannot count brackets
|
|
90
|
+
* should use this one throughout: mistaking `rgb(20,20,20)` for three tokens
|
|
91
|
+
* is a smaller wrong than swallowing the `)` that ends `(gap: tight)`.
|
|
92
|
+
*/
|
|
93
|
+
readonly wordInGroup: "(?:[^\\s()\"\\/]|\\/(?!\\/))(?:[^\\s)\"\\/]|\\/(?!\\/))*";
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Classify one line. Always returns spans covering it end to end, in order, so a
|
|
97
|
+
* caller can rebuild the text by concatenation and can trust that nothing was
|
|
98
|
+
* dropped — which is what a highlighter drawn *behind* a textarea needs, since
|
|
99
|
+
* a lost character would slide every following one out of register.
|
|
100
|
+
*/
|
|
101
|
+
export declare function highlightLine(line: string): Span[];
|
|
102
|
+
/** Every line of a source file, classified. Line endings are not included. */
|
|
103
|
+
export declare function highlight(source: string): Span[][];
|