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