reladraw 0.2.0 → 0.4.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 +30 -22
- package/SYNTAX.md +465 -214
- package/dist/ast.d.ts +204 -61
- package/dist/ast.js +190 -51
- package/dist/constants.d.ts +64 -49
- package/dist/constants.js +91 -71
- package/dist/grammar.d.ts +17 -4
- package/dist/grammar.js +53 -7
- package/dist/icons.d.ts +53 -35
- package/dist/icons.js +81 -43
- package/dist/lexer.js +6 -5
- package/dist/measure.d.ts +3 -3
- package/dist/measure.js +3 -3
- package/dist/model.d.ts +87 -16
- package/dist/parser.js +584 -220
- package/dist/render.d.ts +1 -1
- package/dist/render.js +347 -281
- package/dist/resolve.js +1223 -294
- 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,58 +57,66 @@ 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
|
-
* traveling horizontally needs the
|
|
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
|
/**
|
|
103
|
-
* The named text sizes, each a multiple of the document's own size.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
113
|
+
* The named text sizes, each a multiple of the document's own size. A plain
|
|
114
|
+
* number of pixels is accepted too, as it is for a gap: nothing else in the
|
|
115
|
+
* diagram moving can make it wrong. The names stay the default because they
|
|
116
|
+
* follow the document's size when that is retuned, and a number does not.
|
|
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,73 +127,83 @@ 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
|
-
if (scale
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
if (scale !== undefined)
|
|
147
|
+
return Math.round(fontSize * scale);
|
|
148
|
+
if (/^\d+(\.\d+)?$/.test(named) && Number(named) > 0)
|
|
149
|
+
return Number(named);
|
|
150
|
+
const unit = named.match(/^(\d+(?:\.\d+)?)px$/);
|
|
151
|
+
const hint = unit
|
|
152
|
+
? `; write "size: ${unit[1]}", a size's number is already in pixels`
|
|
153
|
+
: named.startsWith('-') || /^0+(\.0+)?$/.test(named)
|
|
154
|
+
? '; a text size has to be more than zero'
|
|
155
|
+
: '';
|
|
156
|
+
throw new SourceError(`size takes ${Object.keys(TEXT_SIZES).join(', ')} or a number of pixels, not "${named}"${hint}`, line);
|
|
139
157
|
}
|
|
140
158
|
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
159
|
/**
|
|
162
160
|
* Author's word to the SVG's. One spelling of each, per the rule that refuses
|
|
163
|
-
* synonyms for `
|
|
161
|
+
* synonyms for `node` and `edge`: an alias is a variant a reader has to learn,
|
|
164
162
|
* and every document and example has to pick one of them anyway.
|
|
165
163
|
*/
|
|
166
|
-
const
|
|
164
|
+
const TEXT_ALIGNMENTS = {
|
|
167
165
|
left: 'start',
|
|
168
166
|
center: 'middle',
|
|
169
167
|
right: 'end',
|
|
170
168
|
};
|
|
171
169
|
/**
|
|
172
|
-
* Read a
|
|
170
|
+
* Read a text's bracketed modifiers. Shared by the resolver, which offsets the
|
|
173
171
|
* contents away from the band, and the renderer, which draws into it, so the two
|
|
174
172
|
* cannot disagree about which end the band is at.
|
|
175
173
|
*/
|
|
176
|
-
export function
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
|
|
174
|
+
export function textStyleFor(attrs, line, fallbackAlign = 'start', fallbackAt = 'top-left') {
|
|
175
|
+
const written = attrs['at'];
|
|
176
|
+
if (written !== undefined && !isPosition(written)) {
|
|
177
|
+
// A bare side word is the one mistake worth naming rather than only
|
|
178
|
+
// refusing: it was the whole vocabulary until 0.3.0, and the point at the
|
|
179
|
+
// middle of that side is exactly one word away.
|
|
180
|
+
const midpoint = `${written}-center`;
|
|
181
|
+
throw new SourceError(isPosition(midpoint)
|
|
182
|
+
? `a text sits at a *point* of the box, and "${written}" names a side — write "${midpoint}" for the point at the middle of it`
|
|
183
|
+
: `a text's at takes one of ${POSITIONS.join(', ')}, not "${written}"`, line);
|
|
180
184
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
const at = written ?? fallbackAt;
|
|
186
|
+
const align = attrs['align'];
|
|
187
|
+
if (align !== undefined && TEXT_ALIGNMENTS[align] === undefined) {
|
|
188
|
+
throw new SourceError(`a text's align takes left, center or right, not "${align}"`, line);
|
|
184
189
|
}
|
|
190
|
+
const parts = at.split('-');
|
|
185
191
|
return {
|
|
186
|
-
at
|
|
187
|
-
|
|
192
|
+
at,
|
|
193
|
+
end: parts.includes('top') ? 'top' : parts.includes('bottom') ? 'bottom' : 'center',
|
|
194
|
+
side: parts.includes('left') ? 'left' : parts.includes('right') ? 'right' : 'center',
|
|
195
|
+
align: align === undefined ? fallbackAlign : TEXT_ALIGNMENTS[align],
|
|
188
196
|
};
|
|
189
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Where a leaf's text or badge starts vertically, given which end it sits at.
|
|
200
|
+
* Shared, because the resolver works out the text's box and the renderer draws
|
|
201
|
+
* it, and the two must not be able to disagree.
|
|
202
|
+
*/
|
|
203
|
+
export function leafTop(end, y, height, own) {
|
|
204
|
+
if (end === 'top')
|
|
205
|
+
return y + PAD;
|
|
206
|
+
if (end === 'bottom')
|
|
207
|
+
return y + height - PAD - own;
|
|
208
|
+
return y + (height - own) / 2;
|
|
209
|
+
}
|
package/dist/grammar.d.ts
CHANGED
|
@@ -29,13 +29,15 @@ export type TokenKind =
|
|
|
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'
|
|
@@ -43,7 +45,7 @@ export type TokenKind =
|
|
|
43
45
|
| 'value'
|
|
44
46
|
/** `#14532d`, wherever it appears. */
|
|
45
47
|
| 'color'
|
|
46
|
-
/** `(` and `)` around a placement's or a
|
|
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';
|
|
@@ -59,7 +61,7 @@ export interface Span {
|
|
|
59
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", "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
|
|
@@ -83,6 +85,17 @@ export declare const PATTERNS: {
|
|
|
83
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
|
@@ -23,16 +23,16 @@
|
|
|
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
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', 'style', 'diagram'];
|
|
34
34
|
/** Statements whose second word declares a name. `diagram` has none. */
|
|
35
|
-
const DECLARES_NAME = ['
|
|
35
|
+
const DECLARES_NAME = ['node', '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
|
|
@@ -40,11 +40,17 @@ const DECLARES_NAME = ['box', 'note', 'deck', 'style'];
|
|
|
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',
|
|
@@ -75,6 +81,17 @@ export const PATTERNS = {
|
|
|
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
|
|
@@ -159,7 +176,7 @@ 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 border: …` declares a name; `
|
|
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(':'))
|
|
@@ -198,7 +215,16 @@ export function highlightLine(line) {
|
|
|
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.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
/**
|
|
22
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,19 +40,8 @@ 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
|
|
@@ -60,27 +49,56 @@ export declare const ICON_NAMES: string[];
|
|
|
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;
|