reladraw 0.8.1 → 0.10.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/NOTICE +2 -0
- package/README.md +1 -1
- package/SYNTAX.md +71 -13
- package/dist/ast.d.ts +2 -2
- package/dist/ast.js +2 -2
- package/dist/grammar.d.ts +6 -3
- package/dist/grammar.js +6 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +14 -3
- package/dist/lexer.d.ts +4 -1
- package/dist/lexer.js +4 -2
- package/dist/parser.d.ts +26 -1
- package/dist/parser.js +115 -8
- package/dist/render.js +30 -26
- package/dist/resolve.js +1 -1
- package/dist/themes.d.ts +26 -0
- package/dist/themes.js +117 -0
- package/package.json +1 -1
package/NOTICE
CHANGED
|
@@ -31,6 +31,8 @@ Nord Copyright (c) 2016-present Sven Greb
|
|
|
31
31
|
https://github.com/nordtheme/nord
|
|
32
32
|
Dracula Copyright (c) 2023 Dracula Theme
|
|
33
33
|
https://github.com/dracula/dracula-theme
|
|
34
|
+
Vesper Copyright (c) 2023 Rauno Freiberg
|
|
35
|
+
https://github.com/raunofreiberg/vesper
|
|
34
36
|
|
|
35
37
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
36
38
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
66
66
|
|
|
67
67
|
## Status
|
|
68
68
|
|
|
69
|
-
Version 0.
|
|
69
|
+
Version 0.10.0. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG.
|
|
70
70
|
|
|
71
71
|
The language isn't stable yet, so expect the syntax to change.
|
|
72
72
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.10.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The parser, resolver and SVG renderer implement all of it; the sections at the end record what is defective, unchecked or undecided.
|
|
4
4
|
|
|
@@ -6,9 +6,19 @@ The worked example is `examples/arch.reladraw`, transcribed from the reference r
|
|
|
6
6
|
|
|
7
7
|
## Shape of the file
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A statement starts at the beginning of a line. A long one may continue onto the lines after it by indenting them:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```
|
|
12
|
+
node desktop1 "Desktop 1 / Linux"
|
|
13
|
+
above-left of hub gap: tight
|
|
14
|
+
badge: desktop style: synced
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A line that starts with whitespace continues the statement above it, and a blank line or the next unindented line ends it. How much whitespace makes no difference — one space and a tab mean the same — so nothing can depend on indentation you cannot see. A bracket may stay open across lines, as in `line: (color: red,` with `pattern: dashed)` on the next. There are no blocks: indentation continues a statement, and never puts one thing inside another.
|
|
18
|
+
|
|
19
|
+
An indented line with no statement to continue — at the top of the file, or after a blank line or an unindented comment — is an error rather than a statement of its own, and so is an indented line starting with a statement keyword such as `node`. Both are almost always a stray indent, and both say to remove it.
|
|
20
|
+
|
|
21
|
+
Blank lines and `//` comments are ignored. A comment runs to the end of the line and may trail a statement, so `node a "Docker" // the one that matters` is fine. An indented comment line sits inside the statement it follows; an unindented one ends it.
|
|
12
22
|
|
|
13
23
|
A lone `/` is an ordinary character rather than the start of a comment, and `#` is ordinary too — it opens a hex color. Comments were spelled `#` in an earlier version and moved to `//` so that `fill: #14532d` could be written the way every other tool writes a color.
|
|
14
24
|
|
|
@@ -116,18 +126,18 @@ They are bracketed onto the text rather than written among the node's attributes
|
|
|
116
126
|
An edge's text takes the same brackets, less `at`: a node's text sits somewhere in a box and an edge's rides at the middle of its line, so there is no position to name.
|
|
117
127
|
|
|
118
128
|
```
|
|
119
|
-
edge a -> b "rclone" (color: muted, size: small)
|
|
129
|
+
edge a -> b "rclone" (color: theme-muted, size: small)
|
|
120
130
|
```
|
|
121
131
|
|
|
122
132
|
A **style** has no text of its own for a bracket to hang off, so it hangs the bracket off a key instead:
|
|
123
133
|
|
|
124
134
|
```
|
|
125
|
-
style aside text: (size: small, color: muted)
|
|
135
|
+
style aside text: (size: small, color: theme-muted)
|
|
126
136
|
```
|
|
127
137
|
|
|
128
138
|
Bundling a text's properties into a style is how they come to mean something: `style aside` applied to several nodes says those are the same kind of remark, which `(size: small)` written out at each of them does not.
|
|
129
139
|
|
|
130
|
-
`color` takes a color written as the viewer will receive it — `#8b8b8b`, or any CSS color — or
|
|
140
|
+
`color` takes a color written as the viewer will receive it — `#8b8b8b`, or any CSS color — or a theme color such as `theme-muted`, the theme's secondary text color, which survives a change of theme. See "Theme colors".
|
|
131
141
|
|
|
132
142
|
`size: small | normal | large` sets how big the text is set, and so does a plain number of pixels, `size: 22`. The names are the default and the preferred form: they follow the document's size if it is ever set differently, where `22` stays 22. A number says nothing about *why* one text is bigger than another, so bundle it into a style that does. `14px` is refused, since the number is already in pixels, and so is zero or less. Every kind of text has a default and `size` overrides it, exactly as `fill:` overrides the theme's color; only `shape: none` defaults to anything but `normal`, and it defaults to `small`.
|
|
133
143
|
|
|
@@ -144,7 +154,7 @@ Where the node is exactly the size of what it holds — which is most leaves, si
|
|
|
144
154
|
A word or a stretch of a text can borrow the look of a style:
|
|
145
155
|
|
|
146
156
|
```
|
|
147
|
-
style dim text: (color: muted)
|
|
157
|
+
style dim text: (color: theme-muted)
|
|
148
158
|
|
|
149
159
|
node pc.files "shared folder / [dim]synced[/dim]"
|
|
150
160
|
```
|
|
@@ -395,7 +405,7 @@ edge <from> <-> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]]
|
|
|
395
405
|
|
|
396
406
|
Endpoints may be nested (`desktop1.files`). An edge never says where a node goes and routing is the renderer's problem, with one exception: an edge with text claims room in the gap it crosses, which is the next section.
|
|
397
407
|
|
|
398
|
-
An edge's text breaks on ` / ` exactly as a node's does, and the block centers on the point the text would otherwise have occupied, so ``"run `deploy` / shell command"`` stacks its two lines around the midpoint of the line rather than running off along it. An edge's text takes the same brackets a node's does, less `at`, so `(wrap: 20)` folds it and `(color: muted)` quiets it.
|
|
408
|
+
An edge's text breaks on ` / ` exactly as a node's does, and the block centers on the point the text would otherwise have occupied, so ``"run `deploy` / shell command"`` stacks its two lines around the midpoint of the line rather than running off along it. An edge's text takes the same brackets a node's does, less `at`, so `(wrap: 20)` folds it and `(color: theme-muted)` quiets it.
|
|
399
409
|
|
|
400
410
|
### A text makes room for itself
|
|
401
411
|
|
|
@@ -872,19 +882,19 @@ A color is never written in quotes, and a quoted one is refused. There is nothin
|
|
|
872
882
|
|
|
873
883
|
There is no list of color words the tool knows. An earlier version had one, and it was wrong in the way such lists always are: `dark-green` existed only because somebody added it to a map in the renderer, and the next color a diagram wanted would have needed a code change to say. Writing the color directly removes both the list and the reason to grow it. `green` still works, because it is a CSS color, not because this tool has heard of it.
|
|
874
884
|
|
|
875
|
-
A style carrying `text: (color: muted)` is what a marked-up word borrows from, which is how a node carries a name with a quieter qualifier under it:
|
|
885
|
+
A style carrying `text: (color: theme-muted)` is what a marked-up word borrows from, which is how a node carries a name with a quieter qualifier under it:
|
|
876
886
|
|
|
877
887
|
```
|
|
878
888
|
style synced fill: #142814 border: #486544
|
|
879
|
-
style dim text: (color: muted)
|
|
889
|
+
style dim text: (color: theme-muted)
|
|
880
890
|
node pc.files "shared folder / [dim]synced[/dim]" style: synced
|
|
881
891
|
```
|
|
882
892
|
|
|
883
893
|
**Removed: `subtext`.** It colored every text line after the first, which is a positional slice: the rule lived in a style elsewhere in the file and was applied by counting, so a reader of `"shared folder / synced"` could not see that the second line was quiet. The mark says what is quiet where it is quiet, and reaches a word in the middle of a line, which the slice never could. An older file carrying it gets an error naming the mark to write instead.
|
|
884
894
|
|
|
885
|
-
`badge`, `icon` and `shape` belong in a style for the same reason a color does: they say what kind of thing this is, and a kind wants to look alike everywhere it appears. `style artifact fill:
|
|
895
|
+
`badge`, `icon` and `shape` belong in a style for the same reason a color does: they say what kind of thing this is, and a kind wants to look alike everywhere it appears. `style artifact fill: theme-secondary-subtle shape: document` puts the folded corner on every dump in the diagram, and the use site stays one word.
|
|
886
896
|
|
|
887
|
-
`muted`
|
|
897
|
+
`theme-muted` means the theme's secondary text color rather than a fixed one, so a quiet line stays readable when the theme changes. Writing `#8b8b8b` instead would pin it to one theme. Say nothing and every line of a text reads alike, which is what most texts want — `Computer 1 / Ubuntu` is two lines of one name, not a name and a qualifier, and the distinction is the author's to make rather than the renderer's to guess.
|
|
888
898
|
|
|
889
899
|
## The diagram itself
|
|
890
900
|
|
|
@@ -921,15 +931,50 @@ diagram theme: solarized-dark
|
|
|
921
931
|
| `catppuccin-mocha`, `catppuccin-latte` | [Catppuccin](https://catppuccin.com) |
|
|
922
932
|
| `nord` | [Nord](https://www.nordtheme.com) |
|
|
923
933
|
| `dracula` | [Dracula](https://draculatheme.com) |
|
|
934
|
+
| `vesper` | [Vesper](https://github.com/raunofreiberg/vesper) |
|
|
924
935
|
| `high-contrast-dark`, `high-contrast-light` | no fills to lean on, every line at full strength |
|
|
925
936
|
| `print` | a white page, black lines and no fills, for paper |
|
|
926
937
|
|
|
927
938
|
The borrowed palettes were made for code, where a color marks a keyword or a string. A diagram needs a page, a box, a border, text and a line, so each theme is its scheme read as a diagram rather than a copy of it. Their licenses are in `NOTICE`.
|
|
928
939
|
|
|
929
|
-
A theme only ever supplies a color, so changing it never moves anything. A color you write yourself — `fill: #14532d`, or a style's — stays that color under every theme, which means a hand-picked color can clash with a theme it was not chosen against.
|
|
940
|
+
A theme only ever supplies a color, so changing it never moves anything. A color you write yourself — `fill: #14532d`, or a style's — stays that color under every theme, which means a hand-picked color can clash with a theme it was not chosen against. To color something and have it follow the theme, name a theme color instead.
|
|
930
941
|
|
|
931
942
|
An unknown name is an error that lists the themes.
|
|
932
943
|
|
|
944
|
+
### Theme colors
|
|
945
|
+
|
|
946
|
+
Every theme defines the same set of colors, and any attribute that takes a color takes their names. All of them begin `theme-`, so a color that follows the theme says so, and no CSS color name is taken: `red` is still CSS red.
|
|
947
|
+
|
|
948
|
+
The theme's own color for each part — what a box or a line gets when the file says nothing:
|
|
949
|
+
|
|
950
|
+
| color | is |
|
|
951
|
+
|---|---|
|
|
952
|
+
| `theme-page` | the page |
|
|
953
|
+
| `theme-text` | text |
|
|
954
|
+
| `theme-muted` | quieter, secondary text |
|
|
955
|
+
| `theme-fill` | a box's fill |
|
|
956
|
+
| `theme-border` | a box's outline |
|
|
957
|
+
| `theme-line` | a line |
|
|
958
|
+
|
|
959
|
+
And its two accents, each at two strengths, with the text that reads on it:
|
|
960
|
+
|
|
961
|
+
| color | is |
|
|
962
|
+
|---|---|
|
|
963
|
+
| `theme-primary` | the theme's signature color — Dracula's purple, Solarized's blue |
|
|
964
|
+
| `theme-primary-subtle` | the same mixed toward the page: dark on a dark theme, pale on a light one |
|
|
965
|
+
| `theme-on-primary` | text that reads on a solid `theme-primary` |
|
|
966
|
+
| `theme-secondary`, `theme-secondary-subtle`, `theme-on-secondary` | the same for its second color |
|
|
967
|
+
|
|
968
|
+
```
|
|
969
|
+
style synced fill: theme-primary-subtle border: theme-primary
|
|
970
|
+
style live fill: theme-primary
|
|
971
|
+
style dim text: (color: theme-muted)
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
A subtle fill is how a box is set apart: its text is the theme's ordinary text, and it reads in every theme. A solid fill is a badge. A box with a theme fill and no text color of its own gets text that reads on that fill, so `style live` above needs no `text:` — its text is `theme-on-primary`. A hand-picked fill gets no such help; its text is the theme's, as before.
|
|
975
|
+
|
|
976
|
+
A `theme-` name the theme does not define is refused with the list above.
|
|
977
|
+
|
|
933
978
|
The command line can render a file in another theme without editing it — `reladraw arch.reladraw --theme light` — and the flag wins over the file's `theme:`. That is how one source becomes a light and a dark image for a README that shows whichever matches the reader's settings. The file's own `background:` and `text:` still win over the flag's theme, as a hand-written color wins over any theme.
|
|
934
979
|
|
|
935
980
|
### Defaults
|
|
@@ -1021,6 +1066,19 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1021
1066
|
|
|
1022
1067
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal below is refused by name with the replacement quoted, rather than dropped in silence — an older file stops with an error saying what to write instead.
|
|
1023
1068
|
|
|
1069
|
+
**0.10.0**
|
|
1070
|
+
|
|
1071
|
+
- A statement may continue onto the lines after it by indenting them. See "Shape of the file". An error in such a statement names the line the mistake is on.
|
|
1072
|
+
- **Breaking:** indentation used to be ignored, so an indented `node` line was a statement of its own. It now continues the statement above and is refused, with a message saying to remove the indentation.
|
|
1073
|
+
|
|
1074
|
+
**0.9.0**
|
|
1075
|
+
|
|
1076
|
+
- Theme colors: `theme-page`, `theme-text`, `theme-muted`, `theme-fill`, `theme-border` and `theme-line` name the theme's own color for each part, and `theme-primary` and `theme-secondary`, with `-subtle` and `theme-on-` forms, name its two accents. Any attribute that takes a color takes them, and they follow the theme when it changes, so a diagram colored with them reads in light and dark themes alike. See "Theme colors".
|
|
1077
|
+
- A box with a theme fill and no text color of its own gets text that reads on that fill.
|
|
1078
|
+
- **Breaking:** `muted` is now `theme-muted`. `color: muted` is refused with the new name.
|
|
1079
|
+
- The examples use theme colors where they used to hand-pick colors for a dark page, so they read in every theme.
|
|
1080
|
+
- A fourteenth theme, `vesper`, from [Vesper](https://github.com/raunofreiberg/vesper).
|
|
1081
|
+
|
|
1024
1082
|
**0.8.1**
|
|
1025
1083
|
|
|
1026
1084
|
- A fix, no syntax change. An edge naming a side at both ends could be drawn through one of its own nodes in two cases. With the nodes stacked and the sides at right angles, such as `from: bottom to: left`, the line went down past the lower node and came back up through it; it is now one curve round the corner. With both ends naming the same side, such as `from: left to: left`, the line ran straight through the node behind; it now goes over the top of that node, or round the right in a column. Existing diagrams with such an edge will look different, and correct.
|
package/dist/ast.d.ts
CHANGED
|
@@ -225,7 +225,7 @@ export declare function describeAxis(axis: Axis): string;
|
|
|
225
225
|
/** `key: value` pairs trailing a statement. Values are always strings here. */
|
|
226
226
|
export type Attrs = Record<string, string>;
|
|
227
227
|
/**
|
|
228
|
-
* What a text's brackets may say: `"Docker" (at: bottom-center, color: muted)`.
|
|
228
|
+
* What a text's brackets may say: `"Docker" (at: bottom-center, color: theme-muted)`.
|
|
229
229
|
*
|
|
230
230
|
* They are bracketed onto the text rather than written among the node's
|
|
231
231
|
* attributes for the same reason a gap is bracketed onto its placement — they
|
|
@@ -234,7 +234,7 @@ export type Attrs = Record<string, string>;
|
|
|
234
234
|
* `shape`, `icon`, `fill`, `border`, `gap`, `overlap`, `style`.
|
|
235
235
|
*
|
|
236
236
|
* In a style, which has no string for a bracket to hang off, the bracket hangs
|
|
237
|
-
* off the key instead: `style synced text: (color: muted)`.
|
|
237
|
+
* off the key instead: `style synced text: (color: theme-muted)`.
|
|
238
238
|
*
|
|
239
239
|
* `at` and `align` are independent and neither implies the other. `at` is where
|
|
240
240
|
* the block of text sits in the node — one of the nine named positions — and
|
package/dist/ast.js
CHANGED
|
@@ -206,7 +206,7 @@ export function describeAxis(axis) {
|
|
|
206
206
|
return axis === 'y' ? 'vertically' : 'horizontally';
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
|
-
* What a text's brackets may say: `"Docker" (at: bottom-center, color: muted)`.
|
|
209
|
+
* What a text's brackets may say: `"Docker" (at: bottom-center, color: theme-muted)`.
|
|
210
210
|
*
|
|
211
211
|
* They are bracketed onto the text rather than written among the node's
|
|
212
212
|
* attributes for the same reason a gap is bracketed onto its placement — they
|
|
@@ -215,7 +215,7 @@ export function describeAxis(axis) {
|
|
|
215
215
|
* `shape`, `icon`, `fill`, `border`, `gap`, `overlap`, `style`.
|
|
216
216
|
*
|
|
217
217
|
* In a style, which has no string for a bracket to hang off, the bracket hangs
|
|
218
|
-
* off the key instead: `style synced text: (color: muted)`.
|
|
218
|
+
* off the key instead: `style synced text: (color: theme-muted)`.
|
|
219
219
|
*
|
|
220
220
|
* `at` and `align` are independent and neither implies the other. `at` is where
|
|
221
221
|
* the block of text sits in the node — one of the nine named positions — and
|
package/dist/grammar.d.ts
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* The scanner is the JavaScript consumer of the same spec, and the playground is
|
|
20
20
|
* its only caller.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* No token spans lines: `//` runs to the end of one, a string closes on one,
|
|
23
|
+
* and there are no blocks. A statement may continue onto indented lines, but
|
|
24
|
+
* each of those is colored by the same rules, and a word that is a keyword at
|
|
25
|
+
* the start of one is refused by the parser anyway. That is the property those
|
|
26
|
+
* editor formats need and the reason a `.reladraw` grammar is small in all of
|
|
27
|
+
* them.
|
|
25
28
|
*/
|
|
26
29
|
/** What a span of source is, for coloring. */
|
|
27
30
|
export type TokenKind =
|
package/dist/grammar.js
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* The scanner is the JavaScript consumer of the same spec, and the playground is
|
|
20
20
|
* its only caller.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* No token spans lines: `//` runs to the end of one, a string closes on one,
|
|
23
|
+
* and there are no blocks. A statement may continue onto indented lines, but
|
|
24
|
+
* each of those is colored by the same rules, and a word that is a keyword at
|
|
25
|
+
* the start of one is refused by the parser anyway. That is the property those
|
|
26
|
+
* editor formats need and the reason a `.reladraw` grammar is small in all of
|
|
27
|
+
* them.
|
|
25
28
|
*/
|
|
26
29
|
import { DIRECTIONS, POSITIONS, SIDES, PASSAGE_AXES } from './ast.js';
|
|
27
30
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -10,5 +10,9 @@ export { render, type RenderOptions } from './render.js';
|
|
|
10
10
|
export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES, type Theme } from './themes.js';
|
|
11
11
|
import { type RenderOptions } from './render.js';
|
|
12
12
|
import { type ResolveOptions } from './resolve.js';
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Source text in, SVG out. The whole pipeline in one call. An error in a
|
|
15
|
+
* statement written over several lines names the line at fault, not the one
|
|
16
|
+
* the statement starts on; see `blame`.
|
|
17
|
+
*/
|
|
14
18
|
export declare function compile(source: string, options?: ResolveOptions & RenderOptions): string;
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,21 @@ export { parse } from './parser.js';
|
|
|
8
8
|
export { resolve } from './resolve.js';
|
|
9
9
|
export { render } from './render.js';
|
|
10
10
|
export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES } from './themes.js';
|
|
11
|
-
import {
|
|
11
|
+
import { SourceError } from './errors.js';
|
|
12
|
+
import { blame, parse } from './parser.js';
|
|
12
13
|
import { render } from './render.js';
|
|
13
14
|
import { resolve } from './resolve.js';
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Source text in, SVG out. The whole pipeline in one call. An error in a
|
|
17
|
+
* statement written over several lines names the line at fault, not the one
|
|
18
|
+
* the statement starts on; see `blame`.
|
|
19
|
+
*/
|
|
15
20
|
export function compile(source, options = {}) {
|
|
16
|
-
|
|
21
|
+
const run = (text) => render(resolve(parse(text), options), options);
|
|
22
|
+
try {
|
|
23
|
+
return run(source);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw error instanceof SourceError ? blame(source, error, run) : error;
|
|
27
|
+
}
|
|
17
28
|
}
|
package/dist/lexer.d.ts
CHANGED
|
@@ -18,8 +18,11 @@ export interface Token {
|
|
|
18
18
|
* opening bracket counts only where a token starts, and a closing one only
|
|
19
19
|
* while a group is open, so an unquoted `rgb(20,20,20)` stays a single token.
|
|
20
20
|
*
|
|
21
|
+
* `depth` is how many brackets are still open from the lines before, when a
|
|
22
|
+
* statement continues onto this one.
|
|
23
|
+
*
|
|
21
24
|
* Returns an empty array for a blank or comment-only line.
|
|
22
25
|
*/
|
|
23
|
-
export declare function tokenizeLine(line: string, lineNumber: number): Token[];
|
|
26
|
+
export declare function tokenizeLine(line: string, lineNumber: number, depth?: number): Token[];
|
|
24
27
|
/** A bare token ending in `:` opens the attribute section of a statement. */
|
|
25
28
|
export declare function isAttrKey(token: Token): boolean;
|
package/dist/lexer.js
CHANGED
|
@@ -14,12 +14,14 @@ import { SourceError } from './errors.js';
|
|
|
14
14
|
* opening bracket counts only where a token starts, and a closing one only
|
|
15
15
|
* while a group is open, so an unquoted `rgb(20,20,20)` stays a single token.
|
|
16
16
|
*
|
|
17
|
+
* `depth` is how many brackets are still open from the lines before, when a
|
|
18
|
+
* statement continues onto this one.
|
|
19
|
+
*
|
|
17
20
|
* Returns an empty array for a blank or comment-only line.
|
|
18
21
|
*/
|
|
19
|
-
export function tokenizeLine(line, lineNumber) {
|
|
22
|
+
export function tokenizeLine(line, lineNumber, depth = 0) {
|
|
20
23
|
const tokens = [];
|
|
21
24
|
let i = 0;
|
|
22
|
-
let depth = 0;
|
|
23
25
|
while (i < line.length) {
|
|
24
26
|
const ch = line[i];
|
|
25
27
|
if (ch === ' ' || ch === '\t') {
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
import type { Document } from './ast.js';
|
|
2
|
-
|
|
2
|
+
import { SourceError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a whole source file. A statement starts on an unindented line and runs
|
|
5
|
+
* on through any line that starts with whitespace; a blank line or the next
|
|
6
|
+
* unindented one ends it. How much whitespace never matters, only whether
|
|
7
|
+
* there is any, so two lines that look alike cannot mean different things.
|
|
8
|
+
*
|
|
9
|
+
* An indented comment stays inside the statement and an unindented one ends
|
|
10
|
+
* it. An indented line with nothing open above it is refused rather than read
|
|
11
|
+
* as a statement of its own: it is a stray indent or a deleted first line, and
|
|
12
|
+
* both are worth hearing about.
|
|
13
|
+
*/
|
|
3
14
|
export declare function parse(source: string): Document;
|
|
15
|
+
/**
|
|
16
|
+
* Find the line at fault for an error reported on the first line of a
|
|
17
|
+
* continued statement. Everything after the parser reports a statement by the
|
|
18
|
+
* line it starts on, which for a continued one is often not where the mistake
|
|
19
|
+
* is, so this asks which line brings the error in: `run` is tried on the
|
|
20
|
+
* source with the statement cut after each of its lines in turn, and the first
|
|
21
|
+
* cut that fails with the same message names the line. A cut that fails
|
|
22
|
+
* differently — a bracket not yet closed, a placement not yet written — is not
|
|
23
|
+
* the culprit and is passed over. Lines keep their numbers, because what is cut
|
|
24
|
+
* is blanked rather than removed.
|
|
25
|
+
*
|
|
26
|
+
* This costs a few extra runs, and only when there is an error to report.
|
|
27
|
+
*/
|
|
28
|
+
export declare function blame(source: string, error: SourceError, run: (source: string) => unknown): SourceError;
|
package/dist/parser.js
CHANGED
|
@@ -1,19 +1,108 @@
|
|
|
1
1
|
import { COLOR_KEYS, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, PASSAGE_AXES, TEXT_KEYS, LINE_KEYS, LINE_VALUES, CONTENTS_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
|
+
import { STATEMENT_KEYWORDS } from './grammar.js';
|
|
3
4
|
import { isAttrKey, tokenizeLine } from './lexer.js';
|
|
4
|
-
import { THEME_NAMES, THEMES } from './themes.js';
|
|
5
|
-
/**
|
|
5
|
+
import { THEME_COLORS, THEME_NAMES, THEMES } from './themes.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse a whole source file. A statement starts on an unindented line and runs
|
|
8
|
+
* on through any line that starts with whitespace; a blank line or the next
|
|
9
|
+
* unindented one ends it. How much whitespace never matters, only whether
|
|
10
|
+
* there is any, so two lines that look alike cannot mean different things.
|
|
11
|
+
*
|
|
12
|
+
* An indented comment stays inside the statement and an unindented one ends
|
|
13
|
+
* it. An indented line with nothing open above it is refused rather than read
|
|
14
|
+
* as a statement of its own: it is a stray indent or a deleted first line, and
|
|
15
|
+
* both are worth hearing about.
|
|
16
|
+
*/
|
|
6
17
|
export function parse(source) {
|
|
7
18
|
const statements = [];
|
|
19
|
+
/** The statement being gathered, and the line it starts on. */
|
|
20
|
+
let open = null;
|
|
21
|
+
// Brackets may close on a later line, and the lexer only reads `)` as a
|
|
22
|
+
// bracket while one is open, so the depth is carried from line to line.
|
|
23
|
+
let depth = 0;
|
|
24
|
+
const close = () => {
|
|
25
|
+
if (open)
|
|
26
|
+
statements.push(parseStatement(open.tokens, open.line));
|
|
27
|
+
open = null;
|
|
28
|
+
depth = 0;
|
|
29
|
+
};
|
|
8
30
|
source.split(/\r?\n/).forEach((line, index) => {
|
|
9
31
|
const lineNumber = index + 1;
|
|
10
|
-
|
|
32
|
+
if (line.trim() === '') {
|
|
33
|
+
close();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const indented = line[0] === ' ' || line[0] === '\t';
|
|
37
|
+
if (!indented)
|
|
38
|
+
close();
|
|
39
|
+
const tokens = tokenizeLine(line, lineNumber, depth);
|
|
11
40
|
if (tokens.length === 0)
|
|
12
41
|
return;
|
|
13
|
-
|
|
42
|
+
if (!indented) {
|
|
43
|
+
open = { tokens, line: lineNumber };
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const first = tokens[0];
|
|
47
|
+
if (!open) {
|
|
48
|
+
throw new SourceError('this line is indented, which continues the statement above, but there is none to ' +
|
|
49
|
+
'continue — remove the indentation to start a new statement', lineNumber);
|
|
50
|
+
}
|
|
51
|
+
if (!first.quoted && STATEMENT_KEYWORDS.includes(first.text)) {
|
|
52
|
+
throw new SourceError(`\`${first.text}\` cannot continue the statement above — an indented line continues ` +
|
|
53
|
+
`the one before it, so remove the indentation to start a new statement`, lineNumber);
|
|
54
|
+
}
|
|
55
|
+
open.tokens.push(...tokens);
|
|
56
|
+
}
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
if (token.quoted)
|
|
59
|
+
continue;
|
|
60
|
+
if (token.text === '(')
|
|
61
|
+
depth += 1;
|
|
62
|
+
else if (token.text === ')')
|
|
63
|
+
depth -= 1;
|
|
64
|
+
}
|
|
14
65
|
});
|
|
66
|
+
close();
|
|
15
67
|
return { statements };
|
|
16
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Find the line at fault for an error reported on the first line of a
|
|
71
|
+
* continued statement. Everything after the parser reports a statement by the
|
|
72
|
+
* line it starts on, which for a continued one is often not where the mistake
|
|
73
|
+
* is, so this asks which line brings the error in: `run` is tried on the
|
|
74
|
+
* source with the statement cut after each of its lines in turn, and the first
|
|
75
|
+
* cut that fails with the same message names the line. A cut that fails
|
|
76
|
+
* differently — a bracket not yet closed, a placement not yet written — is not
|
|
77
|
+
* the culprit and is passed over. Lines keep their numbers, because what is cut
|
|
78
|
+
* is blanked rather than removed.
|
|
79
|
+
*
|
|
80
|
+
* This costs a few extra runs, and only when there is an error to report.
|
|
81
|
+
*/
|
|
82
|
+
export function blame(source, error, run) {
|
|
83
|
+
const lines = source.split(/\r?\n/);
|
|
84
|
+
const start = error.line - 1;
|
|
85
|
+
const first = lines[start];
|
|
86
|
+
if (first === undefined || first.trim() === '' || first[0] === ' ' || first[0] === '\t')
|
|
87
|
+
return error;
|
|
88
|
+
let end = start;
|
|
89
|
+
while (end + 1 < lines.length && lines[end + 1].trim() !== '' && /^[ \t]/.test(lines[end + 1]))
|
|
90
|
+
end += 1;
|
|
91
|
+
if (end === start)
|
|
92
|
+
return error;
|
|
93
|
+
for (let last = start; last < end; last++) {
|
|
94
|
+
const cut = lines.map((line, index) => (index > last && index <= end ? '' : line)).join('\n');
|
|
95
|
+
try {
|
|
96
|
+
run(cut);
|
|
97
|
+
}
|
|
98
|
+
catch (partial) {
|
|
99
|
+
if (partial instanceof SourceError && partial.message === error.message) {
|
|
100
|
+
return last === start ? error : new SourceError(error.message, last + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return new SourceError(error.message, end + 1);
|
|
105
|
+
}
|
|
17
106
|
function parseStatement(tokens, line) {
|
|
18
107
|
const keyword = tokens[0];
|
|
19
108
|
if (!keyword || keyword.quoted) {
|
|
@@ -142,7 +231,7 @@ const BRACKET_KEYS = {
|
|
|
142
231
|
};
|
|
143
232
|
/** How each bracketed key's error quotes itself back, and what it is about. */
|
|
144
233
|
const BRACKET_ABOUT = {
|
|
145
|
-
text: { kind: 'a text', example: 'color: muted' },
|
|
234
|
+
text: { kind: 'a text', example: 'color: theme-muted' },
|
|
146
235
|
contents: { kind: 'a `contents:` bracket', example: 'widths: match' },
|
|
147
236
|
line: { kind: 'a line', example: 'path: square' },
|
|
148
237
|
};
|
|
@@ -183,7 +272,7 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
183
272
|
const key = keyToken.text.slice(0, -1);
|
|
184
273
|
const bracketKeys = BRACKET_KEYS[key];
|
|
185
274
|
if (bracketKeys !== undefined && follows(tokens, at + 1, '(')) {
|
|
186
|
-
// `text: (color: muted)` — the whole bracket belongs to one part, and it is
|
|
275
|
+
// `text: (color: theme-muted)` — the whole bracket belongs to one part, and it is
|
|
187
276
|
// stored under dotted keys so that a style merges into a node exactly the
|
|
188
277
|
// way every other attribute does.
|
|
189
278
|
const read = readBracket(tokens, at + 1, bracketKeys, {
|
|
@@ -298,7 +387,7 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
298
387
|
// applied by counting, so a reader of the text could not see it. Markup
|
|
299
388
|
// says what is quiet where it is quiet, and reaches a word in the middle of
|
|
300
389
|
// a line, which the slice never could.
|
|
301
|
-
throw new SourceError('`subtext:` has been replaced by markup in the text — write `style dim text: (color: muted)` ' +
|
|
390
|
+
throw new SourceError('`subtext:` has been replaced by markup in the text — write `style dim text: (color: theme-muted)` ' +
|
|
302
391
|
'and mark the quiet words as `"Dropbox / [dim]synced[/dim]"`', line);
|
|
303
392
|
}
|
|
304
393
|
if (key === 'text') {
|
|
@@ -342,6 +431,8 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
342
431
|
}
|
|
343
432
|
throw new SourceError(`"${key}" takes a color and a quoted value is text — drop the quotes if ${valueToken.text} is a color`, line);
|
|
344
433
|
}
|
|
434
|
+
if (COLOR_KEYS.includes(key))
|
|
435
|
+
checkColorWord(valueToken.text, subject, line);
|
|
345
436
|
setOnce(attrs, key, valueToken.text, subject, line);
|
|
346
437
|
return at + 2;
|
|
347
438
|
}
|
|
@@ -457,7 +548,7 @@ function parseEdge(head, line) {
|
|
|
457
548
|
subject,
|
|
458
549
|
what: 'the text',
|
|
459
550
|
kind: "an edge's text",
|
|
460
|
-
example: 'color: muted',
|
|
551
|
+
example: 'color: theme-muted',
|
|
461
552
|
line,
|
|
462
553
|
});
|
|
463
554
|
at = bracket.next;
|
|
@@ -839,6 +930,8 @@ function readBracket(tokens, start, keys, about) {
|
|
|
839
930
|
// the comma are the same statement.
|
|
840
931
|
const value = valueToken.text;
|
|
841
932
|
const clean = value.endsWith(',') && value.length > 1 ? value.slice(0, -1) : value;
|
|
933
|
+
if (key === 'color')
|
|
934
|
+
checkColorWord(clean, about.subject, about.line);
|
|
842
935
|
const had = values[key];
|
|
843
936
|
if (had !== undefined) {
|
|
844
937
|
throw new SourceError(`${about.subject}: "${key}" is written twice in the brackets after ${about.what} (${had}, ${clean}) — keep one`, about.line);
|
|
@@ -848,6 +941,20 @@ function readBracket(tokens, start, keys, about) {
|
|
|
848
941
|
}
|
|
849
942
|
return { values, next: i + 1 };
|
|
850
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* A color is any CSS color, which cannot be checked, or a `theme-` word, which
|
|
946
|
+
* can. So a `theme-` word the theme does not define is refused rather than
|
|
947
|
+
* handed to the viewer as a color that draws nothing, and `muted`, the one
|
|
948
|
+
* theme word from before the prefix, is refused with its new name.
|
|
949
|
+
*/
|
|
950
|
+
function checkColorWord(value, subject, line) {
|
|
951
|
+
if (value === 'muted') {
|
|
952
|
+
throw new SourceError(`${subject}: \`muted\` is now \`theme-muted\` — every color that follows the theme begins \`theme-\``, line);
|
|
953
|
+
}
|
|
954
|
+
if (value.startsWith('theme-') && !THEME_COLORS.includes(value)) {
|
|
955
|
+
throw new SourceError(`${subject}: there is no theme color "${value}" — the theme colors are ${THEME_COLORS.join(', ')}`, line);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
851
958
|
function follows(tokens, at, word) {
|
|
852
959
|
const token = tokens[at];
|
|
853
960
|
return token !== undefined && !token.quoted && token.text === word;
|
package/dist/render.js
CHANGED
|
@@ -3,7 +3,7 @@ import { describeAxis } from './ast.js';
|
|
|
3
3
|
import { SourceError } from './errors.js';
|
|
4
4
|
import { ICON_STROKE } from './icons.js';
|
|
5
5
|
import { monospaceMeasurer } from './measure.js';
|
|
6
|
-
import { DARK_THEME, THEMES } from './themes.js';
|
|
6
|
+
import { DARK_THEME, THEMES, textOnFill, themeColor } from './themes.js';
|
|
7
7
|
import { plain } from './text.js';
|
|
8
8
|
const CORNER = 8;
|
|
9
9
|
/** Turn solved geometry into a standalone SVG document. */
|
|
@@ -17,10 +17,12 @@ export function render(layout, options = {}) {
|
|
|
17
17
|
// everything downstream sees one theme.
|
|
18
18
|
const named = layout.diagram['theme'];
|
|
19
19
|
const base = options.theme ?? (named === undefined ? undefined : THEMES[named]) ?? DARK_THEME;
|
|
20
|
+
const page = layout.diagram['background'];
|
|
21
|
+
const words = layout.diagram['text.color'];
|
|
20
22
|
const theme = {
|
|
21
23
|
...base,
|
|
22
|
-
...(
|
|
23
|
-
...(
|
|
24
|
+
...(page !== undefined && { background: themeColor(page, base) }),
|
|
25
|
+
...(words !== undefined && { text: themeColor(words, base) }),
|
|
24
26
|
};
|
|
25
27
|
const body = [];
|
|
26
28
|
for (const root of layout.roots) {
|
|
@@ -73,7 +75,7 @@ export function render(layout, options = {}) {
|
|
|
73
75
|
width: Math.ceil(ink.maxX) - Math.floor(ink.minX),
|
|
74
76
|
height: Math.ceil(ink.maxY) - Math.floor(ink.minY),
|
|
75
77
|
};
|
|
76
|
-
const arrowColors = new Set(layout.edges.map((edge) => lineOf(edge.appearance, theme.edge)));
|
|
78
|
+
const arrowColors = new Set(layout.edges.map((edge) => lineOf(edge.appearance, theme, theme.edge)));
|
|
77
79
|
return [
|
|
78
80
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${canvas.width}" height="${canvas.height}" viewBox="${canvas.x} ${canvas.y} ${canvas.width} ${canvas.height}" font-family=${quote(measurer.fontFamily)} font-size="${fontSize}px">`,
|
|
79
81
|
' <defs>',
|
|
@@ -183,11 +185,13 @@ function nodeSvg(node, theme, measurer, fontSize, markup, url) {
|
|
|
183
185
|
// including a lone badge beside its text. Every rule that tried to tell a
|
|
184
186
|
// badge from contents was a guess; this one is visible in the source.
|
|
185
187
|
const container = node.children.length > 0;
|
|
186
|
-
const border = borderOf(node.appearance, container ? theme.containerStroke : theme.boxStroke);
|
|
187
|
-
const fill = fillOf(node.appearance, container ? theme.containerFill : theme.boxFill);
|
|
188
|
+
const border = borderOf(node.appearance, theme, container ? theme.containerStroke : theme.boxStroke);
|
|
189
|
+
const fill = fillOf(node.appearance, theme, container ? theme.containerFill : theme.boxFill);
|
|
188
190
|
// A box is the one kind with two inkable parts, which is why its text needs
|
|
189
|
-
// a word of its own — `border:` cannot stand in for it.
|
|
190
|
-
|
|
191
|
+
// a word of its own — `border:` cannot stand in for it. Left unsaid on a
|
|
192
|
+
// theme fill, it is whatever reads on that fill: `fill: theme-primary` alone
|
|
193
|
+
// makes a badge whose text is legible in every theme.
|
|
194
|
+
const text = textColorOf(node.textAttrs, theme, textOnFill(node.appearance['fill'] ?? '', theme) ?? theme.text);
|
|
191
195
|
// Deck copies sit behind the front face, furthest back drawn first.
|
|
192
196
|
for (let depth = node.deckTexts.length; depth >= 1; depth -= 1) {
|
|
193
197
|
const x = face.x - depth * DECK_STEP;
|
|
@@ -296,7 +300,7 @@ function drawIcon(icon, x, y, side, theme) {
|
|
|
296
300
|
}
|
|
297
301
|
function drawEdge(edge, ends, corridor, route, nodes, theme, measurer, fontSize, markup) {
|
|
298
302
|
const { start, end } = ends;
|
|
299
|
-
const color = lineOf(edge.appearance, theme.edge);
|
|
303
|
+
const color = lineOf(edge.appearance, theme, theme.edge);
|
|
300
304
|
const stroke = strokeOf(color, edge.look);
|
|
301
305
|
const markerEnd = ` marker-end="url(#${markerId(color)})"`;
|
|
302
306
|
const markerStart = edge.both ? ` marker-start="url(#${markerId(color)}-back)"` : '';
|
|
@@ -409,7 +413,7 @@ function drawEdge(edge, ends, corridor, route, nodes, theme, measurer, fontSize,
|
|
|
409
413
|
parts.push(sized(textBlock(lines, midX - width / 2, top, textHeight, size, { x: 0, y: 0, width, height }, {
|
|
410
414
|
// A colored edge carries its meaning into its text; an uncolored
|
|
411
415
|
// one leaves the words to read as ordinary text.
|
|
412
|
-
color: textColorOf(edge.textAttrs, theme, lineOf(edge.appearance, theme.text)),
|
|
416
|
+
color: textColorOf(edge.textAttrs, theme, lineOf(edge.appearance, theme, theme.text)),
|
|
413
417
|
align: 'middle',
|
|
414
418
|
ink: (run, own) => runInk(run, own, markup, theme),
|
|
415
419
|
}), size, fontSize));
|
|
@@ -2654,37 +2658,37 @@ function textBlock(lines, x, y, lineHeight, fontSize, box, style) {
|
|
|
2654
2658
|
function runInk(run, own, markup, theme) {
|
|
2655
2659
|
if (run.style === undefined)
|
|
2656
2660
|
return own;
|
|
2657
|
-
return
|
|
2661
|
+
return themeColor(markup[run.style], theme);
|
|
2658
2662
|
}
|
|
2659
2663
|
/**
|
|
2660
|
-
* A text's own color
|
|
2661
|
-
*
|
|
2662
|
-
* color, the same as `fill:` and `border:` take.
|
|
2664
|
+
* A text's own color, the same words `fill:` and `border:` take — so
|
|
2665
|
+
* `theme-muted` is a quiet line that stays readable when the theme changes.
|
|
2663
2666
|
*/
|
|
2664
2667
|
function textColorOf(textAttrs, theme, fallback) {
|
|
2665
2668
|
const value = textAttrs['color'];
|
|
2666
|
-
return value === undefined ? fallback :
|
|
2667
|
-
}
|
|
2668
|
-
function namedColor(value, theme) {
|
|
2669
|
-
return value === 'muted' ? theme.mutedText : value;
|
|
2669
|
+
return value === undefined ? fallback : themeColor(value, theme);
|
|
2670
2670
|
}
|
|
2671
2671
|
/**
|
|
2672
2672
|
* A color is written as the viewer will receive it — `#14532d`, or any CSS
|
|
2673
|
-
* color
|
|
2674
|
-
*
|
|
2673
|
+
* color — or as one of the `theme-` words, which the theme answers. The
|
|
2674
|
+
* renderer keeps no list of color words of its own, so a diagram is never
|
|
2675
|
+
* limited to the ones somebody remembered to add here.
|
|
2675
2676
|
*
|
|
2676
2677
|
* Each names the part it colors, so each reads exactly one key. The word these
|
|
2677
2678
|
* replaced, `stroke:`, named no part and meant a different one on every kind,
|
|
2678
2679
|
* which is why a box's text could not be colored at all until `text:`.
|
|
2679
2680
|
*/
|
|
2680
|
-
function borderOf(appearance, fallback) {
|
|
2681
|
-
|
|
2681
|
+
function borderOf(appearance, theme, fallback) {
|
|
2682
|
+
const value = appearance['border'];
|
|
2683
|
+
return value === undefined ? fallback : themeColor(value, theme);
|
|
2682
2684
|
}
|
|
2683
|
-
function lineOf(appearance, fallback) {
|
|
2684
|
-
|
|
2685
|
+
function lineOf(appearance, theme, fallback) {
|
|
2686
|
+
const value = appearance['line'];
|
|
2687
|
+
return value === undefined ? fallback : themeColor(value, theme);
|
|
2685
2688
|
}
|
|
2686
|
-
function fillOf(appearance, fallback) {
|
|
2687
|
-
|
|
2689
|
+
function fillOf(appearance, theme, fallback) {
|
|
2690
|
+
const value = appearance['fill'];
|
|
2691
|
+
return value === undefined ? fallback : themeColor(value, theme);
|
|
2688
2692
|
}
|
|
2689
2693
|
function round(value) {
|
|
2690
2694
|
return Math.round(value * 100) / 100;
|
package/dist/resolve.js
CHANGED
|
@@ -391,7 +391,7 @@ function checkStyleKeys(statements) {
|
|
|
391
391
|
}
|
|
392
392
|
/**
|
|
393
393
|
* The attribute a key belongs to. A bracketed value arrives under dotted keys —
|
|
394
|
-
* `text: (color: muted)` is stored as `text.color` — so that a style merges into
|
|
394
|
+
* `text: (color: theme-muted)` is stored as `text.color` — so that a style merges into
|
|
395
395
|
* a node exactly the way every other attribute does; every check above asks
|
|
396
396
|
* about the part, which is the half before the dot.
|
|
397
397
|
*/
|
package/dist/themes.d.ts
CHANGED
|
@@ -28,7 +28,33 @@ export interface Theme {
|
|
|
28
28
|
iconInk: string;
|
|
29
29
|
/** The body an icon's lines enclose. */
|
|
30
30
|
iconShade: string;
|
|
31
|
+
/** The theme's signature color, which `theme-primary` names. */
|
|
32
|
+
primary: Accent;
|
|
33
|
+
/** Its second color, which `theme-secondary` names. */
|
|
34
|
+
secondary: Accent;
|
|
31
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* An accent at the two strengths a file can ask for, and the text that reads
|
|
38
|
+
* on it. `subtle` is the accent mixed toward the page — dark on a dark theme,
|
|
39
|
+
* pale on a light one — so `fill: theme-primary-subtle` is a box set apart in
|
|
40
|
+
* every theme, and its text is the theme's ordinary text.
|
|
41
|
+
*/
|
|
42
|
+
export interface Accent {
|
|
43
|
+
color: string;
|
|
44
|
+
subtle: string;
|
|
45
|
+
/** Text on a solid fill of `color`: `theme-on-primary`. */
|
|
46
|
+
on: string;
|
|
47
|
+
}
|
|
48
|
+
export declare const THEME_COLORS: readonly string[];
|
|
49
|
+
/** A color as written, with a `theme-` word replaced by the theme's value. */
|
|
50
|
+
export declare function themeColor(value: string, theme: Theme): string;
|
|
51
|
+
/**
|
|
52
|
+
* The text a box gets when its fill is a theme color and its text says
|
|
53
|
+
* nothing: the accent's own `on` color for a solid accent, and otherwise
|
|
54
|
+
* whichever of the theme's text and page colors stands further from the fill.
|
|
55
|
+
* A hand-picked fill is left alone — the theme has no say over it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function textOnFill(fill: string, theme: Theme): string | undefined;
|
|
32
58
|
/**
|
|
33
59
|
* Sampled out of `examples/reference/arch.png` rather than invented,
|
|
34
60
|
* so the benchmark render and the drawing it is measured against differ by
|
package/dist/themes.js
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
+
/** How much of an accent survives in its subtle form; the rest is the page. */
|
|
2
|
+
const SUBTLE = 0.25;
|
|
3
|
+
function accent(color, page, on, subtle = mix(color, page, SUBTLE)) {
|
|
4
|
+
return { color, subtle, on };
|
|
5
|
+
}
|
|
6
|
+
/** `amount` of `color` over `page`, both `#rrggbb`. */
|
|
7
|
+
function mix(color, page, amount) {
|
|
8
|
+
const channel = (hex, at) => parseInt(hex.slice(at, at + 2), 16);
|
|
9
|
+
let out = '#';
|
|
10
|
+
for (const at of [1, 3, 5]) {
|
|
11
|
+
const value = Math.round(channel(page, at) + (channel(color, at) - channel(page, at)) * amount);
|
|
12
|
+
out += value.toString(16).padStart(2, '0');
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Every color a file may name from the theme, and what it is in a given one.
|
|
18
|
+
* The first six are the theme's own defaults for each part — what a line or a
|
|
19
|
+
* box gets when the file says nothing — and the rest are its two accents. All
|
|
20
|
+
* begin `theme-`, so a word that follows the theme says so, and no CSS color
|
|
21
|
+
* name is taken.
|
|
22
|
+
*/
|
|
23
|
+
const THEME_COLOR_OF = {
|
|
24
|
+
'theme-page': (t) => t.background,
|
|
25
|
+
'theme-text': (t) => t.text,
|
|
26
|
+
'theme-muted': (t) => t.mutedText,
|
|
27
|
+
'theme-fill': (t) => t.boxFill,
|
|
28
|
+
'theme-border': (t) => t.boxStroke,
|
|
29
|
+
'theme-line': (t) => t.edge,
|
|
30
|
+
'theme-primary': (t) => t.primary.color,
|
|
31
|
+
'theme-primary-subtle': (t) => t.primary.subtle,
|
|
32
|
+
'theme-on-primary': (t) => t.primary.on,
|
|
33
|
+
'theme-secondary': (t) => t.secondary.color,
|
|
34
|
+
'theme-secondary-subtle': (t) => t.secondary.subtle,
|
|
35
|
+
'theme-on-secondary': (t) => t.secondary.on,
|
|
36
|
+
};
|
|
37
|
+
export const THEME_COLORS = Object.keys(THEME_COLOR_OF);
|
|
38
|
+
/** A color as written, with a `theme-` word replaced by the theme's value. */
|
|
39
|
+
export function themeColor(value, theme) {
|
|
40
|
+
const of = THEME_COLOR_OF[value];
|
|
41
|
+
return of === undefined ? value : of(theme);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The text a box gets when its fill is a theme color and its text says
|
|
45
|
+
* nothing: the accent's own `on` color for a solid accent, and otherwise
|
|
46
|
+
* whichever of the theme's text and page colors stands further from the fill.
|
|
47
|
+
* A hand-picked fill is left alone — the theme has no say over it.
|
|
48
|
+
*/
|
|
49
|
+
export function textOnFill(fill, theme) {
|
|
50
|
+
if (fill === 'theme-primary')
|
|
51
|
+
return theme.primary.on;
|
|
52
|
+
if (fill === 'theme-secondary')
|
|
53
|
+
return theme.secondary.on;
|
|
54
|
+
if (THEME_COLOR_OF[fill] === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
// `diagram background:` and `text:` may be any CSS color, which cannot be
|
|
57
|
+
// measured here, so an overruled theme keeps its ordinary text.
|
|
58
|
+
const measured = [themeColor(fill, theme), theme.text, theme.background];
|
|
59
|
+
if (!measured.every((color) => /^#[0-9a-f]{6}$/i.test(color)))
|
|
60
|
+
return theme.text;
|
|
61
|
+
const under = luminance(themeColor(fill, theme));
|
|
62
|
+
const apart = (color) => Math.abs(luminance(color) - under);
|
|
63
|
+
return apart(theme.text) >= apart(theme.background) ? theme.text : theme.background;
|
|
64
|
+
}
|
|
65
|
+
function luminance(hex) {
|
|
66
|
+
const [r, g, b] = [1, 3, 5].map((at) => {
|
|
67
|
+
const c = parseInt(hex.slice(at, at + 2), 16) / 255;
|
|
68
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
69
|
+
});
|
|
70
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
71
|
+
}
|
|
1
72
|
/**
|
|
2
73
|
* Sampled out of `examples/reference/arch.png` rather than invented,
|
|
3
74
|
* so the benchmark render and the drawing it is measured against differ by
|
|
@@ -20,6 +91,11 @@ export const DARK_THEME = {
|
|
|
20
91
|
// should read as part of the diagram's palette, not as clip art dropped in.
|
|
21
92
|
iconInk: '#8d8d8e',
|
|
22
93
|
iconShade: '#3e3d58',
|
|
94
|
+
// The reference's two colored boxes: `synced` is the green, `dump` the red.
|
|
95
|
+
// Their fills are sampled rather than mixed, so the benchmark written in
|
|
96
|
+
// theme colors renders exactly as it did in hex.
|
|
97
|
+
primary: accent('#486544', '#111111', '#d9d9d9', '#142814'),
|
|
98
|
+
secondary: accent('#8f3a3a', '#111111', '#d9d9d9', '#460000'),
|
|
23
99
|
};
|
|
24
100
|
/** The dark theme's counterpart: the same roles, on a white page. */
|
|
25
101
|
const LIGHT_THEME = {
|
|
@@ -33,6 +109,8 @@ const LIGHT_THEME = {
|
|
|
33
109
|
edge: '#7c83a0',
|
|
34
110
|
iconInk: '#57606a',
|
|
35
111
|
iconShade: '#d6d9e6',
|
|
112
|
+
primary: accent('#3a7d44', '#ffffff', '#ffffff'),
|
|
113
|
+
secondary: accent('#b3423a', '#ffffff', '#ffffff'),
|
|
36
114
|
};
|
|
37
115
|
/**
|
|
38
116
|
* Every theme a file may name, in the order they are offered. Pairs sit
|
|
@@ -53,6 +131,8 @@ export const THEMES = {
|
|
|
53
131
|
edge: '#268bd2',
|
|
54
132
|
iconInk: '#839496',
|
|
55
133
|
iconShade: '#0f4a58',
|
|
134
|
+
primary: accent('#268bd2', '#002b36', '#fdf6e3'),
|
|
135
|
+
secondary: accent('#cb4b16', '#002b36', '#fdf6e3'),
|
|
56
136
|
},
|
|
57
137
|
// base3 page, base2 leaves, the same blue.
|
|
58
138
|
'solarized-light': {
|
|
@@ -66,6 +146,8 @@ export const THEMES = {
|
|
|
66
146
|
edge: '#268bd2',
|
|
67
147
|
iconInk: '#657b83',
|
|
68
148
|
iconShade: '#e0d9c3',
|
|
149
|
+
primary: accent('#268bd2', '#fdf6e3', '#fdf6e3'),
|
|
150
|
+
secondary: accent('#cb4b16', '#fdf6e3', '#fdf6e3'),
|
|
69
151
|
},
|
|
70
152
|
// Gruvbox, Pavel Pertsev. bg0 page, bg1 leaves, the warm yellow for lines.
|
|
71
153
|
'gruvbox-dark': {
|
|
@@ -79,6 +161,8 @@ export const THEMES = {
|
|
|
79
161
|
edge: '#d79921',
|
|
80
162
|
iconInk: '#a89984',
|
|
81
163
|
iconShade: '#504945',
|
|
164
|
+
primary: accent('#fe8019', '#282828', '#282828'),
|
|
165
|
+
secondary: accent('#8ec07c', '#282828', '#282828'),
|
|
82
166
|
},
|
|
83
167
|
'gruvbox-light': {
|
|
84
168
|
background: '#fbf1c7',
|
|
@@ -91,6 +175,8 @@ export const THEMES = {
|
|
|
91
175
|
edge: '#b57614',
|
|
92
176
|
iconInk: '#7c6f64',
|
|
93
177
|
iconShade: '#d5c4a1',
|
|
178
|
+
primary: accent('#af3a03', '#fbf1c7', '#fbf1c7'),
|
|
179
|
+
secondary: accent('#427b58', '#fbf1c7', '#fbf1c7'),
|
|
94
180
|
},
|
|
95
181
|
// Catppuccin. Mocha's base page and surface leaves, blue lines.
|
|
96
182
|
'catppuccin-mocha': {
|
|
@@ -104,6 +190,8 @@ export const THEMES = {
|
|
|
104
190
|
edge: '#89b4fa',
|
|
105
191
|
iconInk: '#a6adc8',
|
|
106
192
|
iconShade: '#45475a',
|
|
193
|
+
primary: accent('#cba6f7', '#1e1e2e', '#1e1e2e'),
|
|
194
|
+
secondary: accent('#fab387', '#1e1e2e', '#1e1e2e'),
|
|
107
195
|
},
|
|
108
196
|
// Latte's base page and crust leaves, lavender lines.
|
|
109
197
|
'catppuccin-latte': {
|
|
@@ -117,6 +205,8 @@ export const THEMES = {
|
|
|
117
205
|
edge: '#7287fd',
|
|
118
206
|
iconInk: '#6c6f85',
|
|
119
207
|
iconShade: '#ccd0da',
|
|
208
|
+
primary: accent('#8839ef', '#eff1f5', '#eff1f5'),
|
|
209
|
+
secondary: accent('#fe640b', '#eff1f5', '#eff1f5'),
|
|
120
210
|
},
|
|
121
211
|
// Nord, Sven Greb. Polar Night page and leaves, Frost lines.
|
|
122
212
|
nord: {
|
|
@@ -130,6 +220,8 @@ export const THEMES = {
|
|
|
130
220
|
edge: '#81a1c1',
|
|
131
221
|
iconInk: '#aeb7c6',
|
|
132
222
|
iconShade: '#434c5e',
|
|
223
|
+
primary: accent('#88c0d0', '#2e3440', '#2e3440'),
|
|
224
|
+
secondary: accent('#d08770', '#2e3440', '#2e3440'),
|
|
133
225
|
},
|
|
134
226
|
// Dracula, the free palette. Current-line leaves, comment borders, purple lines.
|
|
135
227
|
dracula: {
|
|
@@ -143,6 +235,24 @@ export const THEMES = {
|
|
|
143
235
|
edge: '#bd93f9',
|
|
144
236
|
iconInk: '#b6b9cc',
|
|
145
237
|
iconShade: '#565a70',
|
|
238
|
+
primary: accent('#bd93f9', '#282a36', '#282a36'),
|
|
239
|
+
secondary: accent('#ff79c6', '#282a36', '#282a36'),
|
|
240
|
+
},
|
|
241
|
+
// Vesper, Rauno Freiberg. Near-black page, its input surface for leaves, and
|
|
242
|
+
// the peach it spends on everything that matters for lines; mint second.
|
|
243
|
+
vesper: {
|
|
244
|
+
background: '#101010',
|
|
245
|
+
boxFill: '#1c1c1c',
|
|
246
|
+
boxStroke: '#505050',
|
|
247
|
+
containerFill: '#161616',
|
|
248
|
+
containerStroke: '#232323',
|
|
249
|
+
text: '#ffffff',
|
|
250
|
+
mutedText: '#a0a0a0',
|
|
251
|
+
edge: '#ffc799',
|
|
252
|
+
iconInk: '#a0a0a0',
|
|
253
|
+
iconShade: '#282828',
|
|
254
|
+
primary: accent('#ffc799', '#101010', '#101010'),
|
|
255
|
+
secondary: accent('#99ffe4', '#101010', '#101010'),
|
|
146
256
|
},
|
|
147
257
|
// For low vision and projectors: no fills to lean on, every line at full
|
|
148
258
|
// strength, and a container told apart by a gray outline alone.
|
|
@@ -157,6 +267,8 @@ export const THEMES = {
|
|
|
157
267
|
edge: '#ffffff',
|
|
158
268
|
iconInk: '#ffffff',
|
|
159
269
|
iconShade: '#3a3a3a',
|
|
270
|
+
primary: accent('#ffd400', '#000000', '#000000'),
|
|
271
|
+
secondary: accent('#00e5ff', '#000000', '#000000'),
|
|
160
272
|
},
|
|
161
273
|
'high-contrast-light': {
|
|
162
274
|
background: '#ffffff',
|
|
@@ -169,6 +281,8 @@ export const THEMES = {
|
|
|
169
281
|
edge: '#000000',
|
|
170
282
|
iconInk: '#000000',
|
|
171
283
|
iconShade: '#d0d0d0',
|
|
284
|
+
primary: accent('#0033cc', '#ffffff', '#ffffff'),
|
|
285
|
+
secondary: accent('#b00000', '#ffffff', '#ffffff'),
|
|
172
286
|
},
|
|
173
287
|
// For paper: no fill anywhere an ink cartridge would notice, black lines,
|
|
174
288
|
// gray only where the dark theme is quiet.
|
|
@@ -183,6 +297,9 @@ export const THEMES = {
|
|
|
183
297
|
edge: '#333333',
|
|
184
298
|
iconInk: '#000000',
|
|
185
299
|
iconShade: '#ffffff',
|
|
300
|
+
// Grays, and pale ones for fills: a box set apart without spending color.
|
|
301
|
+
primary: accent('#333333', '#ffffff', '#ffffff', '#e6e6e6'),
|
|
302
|
+
secondary: accent('#777777', '#ffffff', '#ffffff', '#f2f2f2'),
|
|
186
303
|
},
|
|
187
304
|
};
|
|
188
305
|
/** The theme a file gets when it names none. */
|