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/text.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** A stretch of text drawn in one style. `style` is a style name from the file. */
|
|
2
|
+
export interface Run {
|
|
3
|
+
text: string;
|
|
4
|
+
/** The style whose text color this run borrows, or undefined for the node's own. */
|
|
5
|
+
style?: string;
|
|
6
|
+
}
|
|
7
|
+
/** One drawn line, as the runs it is made of. */
|
|
8
|
+
export type Line = Run[];
|
|
9
|
+
/**
|
|
10
|
+
* Read a text into runs, stripping the markup as it goes.
|
|
11
|
+
*
|
|
12
|
+
* `\[` is the escape, and it is why a bare `[` had to be reserved in 0.3.0
|
|
13
|
+
* rather than left alone: a text containing one is legal in 0.2.0 and would
|
|
14
|
+
* change meaning the day this landed.
|
|
15
|
+
*
|
|
16
|
+
* The closer repeats the name — `[/dim]` and not `[/]` — so a mismatch is
|
|
17
|
+
* refused by name rather than by counting, which is the thing a reader can
|
|
18
|
+
* never do reliably. Nesting is refused for the same reason: two colors on one
|
|
19
|
+
* word is not a picture anything here can draw, so it is a mistake rather than
|
|
20
|
+
* a shorthand.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseMarkup(text: string, subject: string, line: number): Run[];
|
|
23
|
+
/**
|
|
24
|
+
* A slash with whitespace on both sides marks a line break, so a text is
|
|
25
|
+
* really a short stack of lines. Each line is trimmed; empty ones are dropped.
|
|
26
|
+
*
|
|
27
|
+
* The whitespace is what makes the marker safe. Splitting on a bare `/` meant
|
|
28
|
+
* no text could contain one, so `TCP/IP` came out as two lines, and so did
|
|
29
|
+
* `16/9`, `I/O` and every path or URL. Requiring the spaces keeps the marker
|
|
30
|
+
* legible where it is meant — `"Computer 1 / Ubuntu"` — while a slash inside a
|
|
31
|
+
* word stays an ordinary character.
|
|
32
|
+
*
|
|
33
|
+
* That leaves the text that wants a spaced slash and no break — `Before / After`
|
|
34
|
+
* — which writes it `\/`. The lexer preserves that escape rather than resolving
|
|
35
|
+
* it, so the backslash is still here to suppress the split, and is dropped once
|
|
36
|
+
* the splitting is done.
|
|
37
|
+
*
|
|
38
|
+
* A break inside a marked-up run is an ordinary break: `[dim]one / two[/dim]`
|
|
39
|
+
* quiets both lines, which is what made the markup general where the slice it
|
|
40
|
+
* replaced could only reach the tail of a text.
|
|
41
|
+
*/
|
|
42
|
+
export declare function splitRuns(runs: Run[]): Line[];
|
|
43
|
+
/** Fold one line onto several at word boundaries, never exceeding `columns`. */
|
|
44
|
+
export declare function wrapLine(line: Line, columns: number): Line[];
|
|
45
|
+
/** The line as it reads, with no markup — what gets measured. */
|
|
46
|
+
export declare function plain(line: Line): string;
|
|
47
|
+
/** Every style name the markup in these lines refers to, without repeats. */
|
|
48
|
+
export declare function markupStyles(lines: Line[]): string[];
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A text and the runs inside it.
|
|
3
|
+
*
|
|
4
|
+
* A text is a string, and the one piece of structure it may carry is markup:
|
|
5
|
+
* `[dim]synced[/dim]` draws that word in the text color of `style dim`. The
|
|
6
|
+
* opener names a *style* and never a color, so the word borrows a meaning the
|
|
7
|
+
* file already has rather than restating a value that goes stale the day the
|
|
8
|
+
* thing it means is recolored.
|
|
9
|
+
*
|
|
10
|
+
* Markup replaced `subtext:`, which colored "every line after the first". That
|
|
11
|
+
* was a positional slice: a reader of `node a "Dropbox / synced"` could not see
|
|
12
|
+
* that line two was quiet, because the rule lived in a style elsewhere and was
|
|
13
|
+
* applied by counting. Markup says what is quiet where it is quiet, and it
|
|
14
|
+
* reaches a word in the middle of a line, which the slice never could.
|
|
15
|
+
*
|
|
16
|
+
* Everything downstream therefore works in *runs* rather than strings: a drawn
|
|
17
|
+
* line is a list of runs, each with the style it borrows its color from, and
|
|
18
|
+
* line breaking and wrapping carry the runs along rather than re-parsing text
|
|
19
|
+
* that has already been read.
|
|
20
|
+
*/
|
|
21
|
+
import { SourceError } from './errors.js';
|
|
22
|
+
/** A markup tag: `[name]` or `[/name]`, with the same name spelling as a style. */
|
|
23
|
+
const TAG = /\[(\/?)([A-Za-z][A-Za-z0-9_-]*)\]/y;
|
|
24
|
+
/**
|
|
25
|
+
* Read a text into runs, stripping the markup as it goes.
|
|
26
|
+
*
|
|
27
|
+
* `\[` is the escape, and it is why a bare `[` had to be reserved in 0.3.0
|
|
28
|
+
* rather than left alone: a text containing one is legal in 0.2.0 and would
|
|
29
|
+
* change meaning the day this landed.
|
|
30
|
+
*
|
|
31
|
+
* The closer repeats the name — `[/dim]` and not `[/]` — so a mismatch is
|
|
32
|
+
* refused by name rather than by counting, which is the thing a reader can
|
|
33
|
+
* never do reliably. Nesting is refused for the same reason: two colors on one
|
|
34
|
+
* word is not a picture anything here can draw, so it is a mistake rather than
|
|
35
|
+
* a shorthand.
|
|
36
|
+
*/
|
|
37
|
+
export function parseMarkup(text, subject, line) {
|
|
38
|
+
const runs = [];
|
|
39
|
+
let open;
|
|
40
|
+
let buffer = '';
|
|
41
|
+
let i = 0;
|
|
42
|
+
const flush = () => {
|
|
43
|
+
if (buffer.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
runs.push(open === undefined ? { text: buffer } : { text: buffer, style: open });
|
|
46
|
+
buffer = '';
|
|
47
|
+
};
|
|
48
|
+
while (i < text.length) {
|
|
49
|
+
const ch = text[i];
|
|
50
|
+
if (ch === '\\' && text[i + 1] === '[') {
|
|
51
|
+
buffer += '[';
|
|
52
|
+
i += 2;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '[') {
|
|
56
|
+
TAG.lastIndex = i;
|
|
57
|
+
const found = TAG.exec(text);
|
|
58
|
+
if (found) {
|
|
59
|
+
const closing = found[1] === '/';
|
|
60
|
+
const name = found[2];
|
|
61
|
+
if (closing) {
|
|
62
|
+
if (open === undefined) {
|
|
63
|
+
throw new SourceError(`${subject}: "[/${name}]" closes markup that was never opened — write \\[ for a literal bracket`, line);
|
|
64
|
+
}
|
|
65
|
+
if (open !== name) {
|
|
66
|
+
throw new SourceError(`${subject}: "[${open}]" is closed by "[/${name}]" — a closer repeats the name it opened with`, line);
|
|
67
|
+
}
|
|
68
|
+
flush();
|
|
69
|
+
open = undefined;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
if (open !== undefined) {
|
|
73
|
+
throw new SourceError(`${subject}: "[${name}]" opens inside "[${open}]", and a run of text takes one style`, line);
|
|
74
|
+
}
|
|
75
|
+
flush();
|
|
76
|
+
open = name;
|
|
77
|
+
}
|
|
78
|
+
i = found.index + found[0].length;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Not a tag at all — a lone bracket in the middle of a word. Refused by
|
|
82
|
+
// name rather than passed through, because `[` is reserved and a text
|
|
83
|
+
// that means one has an escape to say so.
|
|
84
|
+
throw new SourceError(`${subject}: "[" opens markup in a text — write \\[ for a literal bracket`, line);
|
|
85
|
+
}
|
|
86
|
+
buffer += ch;
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
if (open !== undefined) {
|
|
90
|
+
throw new SourceError(`${subject}: "[${open}]" is never closed`, line);
|
|
91
|
+
}
|
|
92
|
+
flush();
|
|
93
|
+
return runs;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A slash with whitespace on both sides marks a line break, so a text is
|
|
97
|
+
* really a short stack of lines. Each line is trimmed; empty ones are dropped.
|
|
98
|
+
*
|
|
99
|
+
* The whitespace is what makes the marker safe. Splitting on a bare `/` meant
|
|
100
|
+
* no text could contain one, so `TCP/IP` came out as two lines, and so did
|
|
101
|
+
* `16/9`, `I/O` and every path or URL. Requiring the spaces keeps the marker
|
|
102
|
+
* legible where it is meant — `"Computer 1 / Ubuntu"` — while a slash inside a
|
|
103
|
+
* word stays an ordinary character.
|
|
104
|
+
*
|
|
105
|
+
* That leaves the text that wants a spaced slash and no break — `Before / After`
|
|
106
|
+
* — which writes it `\/`. The lexer preserves that escape rather than resolving
|
|
107
|
+
* it, so the backslash is still here to suppress the split, and is dropped once
|
|
108
|
+
* the splitting is done.
|
|
109
|
+
*
|
|
110
|
+
* A break inside a marked-up run is an ordinary break: `[dim]one / two[/dim]`
|
|
111
|
+
* quiets both lines, which is what made the markup general where the slice it
|
|
112
|
+
* replaced could only reach the tail of a text.
|
|
113
|
+
*/
|
|
114
|
+
export function splitRuns(runs) {
|
|
115
|
+
const lines = [[]];
|
|
116
|
+
for (const run of runs) {
|
|
117
|
+
const parts = run.text.split(/\s+\/\s+/);
|
|
118
|
+
parts.forEach((part, index) => {
|
|
119
|
+
if (index > 0)
|
|
120
|
+
lines.push([]);
|
|
121
|
+
lines[lines.length - 1].push({ ...run, text: part });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return tidy(lines);
|
|
125
|
+
}
|
|
126
|
+
/** Trim each line's ends, drop the empty ones, and resolve the `\/` escape. */
|
|
127
|
+
function tidy(lines) {
|
|
128
|
+
const kept = [];
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const runs = line
|
|
131
|
+
.map((run, index) => {
|
|
132
|
+
let text = run.text.replace(/\\\//g, '/');
|
|
133
|
+
if (index === 0)
|
|
134
|
+
text = text.replace(/^\s+/, '');
|
|
135
|
+
if (index === line.length - 1)
|
|
136
|
+
text = text.replace(/\s+$/, '');
|
|
137
|
+
return { ...run, text };
|
|
138
|
+
})
|
|
139
|
+
.filter((run) => run.text.length > 0);
|
|
140
|
+
if (runs.length > 0)
|
|
141
|
+
kept.push(runs);
|
|
142
|
+
}
|
|
143
|
+
return kept.length > 0 ? kept : [[{ text: '' }]];
|
|
144
|
+
}
|
|
145
|
+
/** Fold one line onto several at word boundaries, never exceeding `columns`. */
|
|
146
|
+
export function wrapLine(line, columns) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
let current = [];
|
|
149
|
+
let length = 0;
|
|
150
|
+
for (const word of words(line)) {
|
|
151
|
+
const space = length === 0 ? 0 : 1;
|
|
152
|
+
if (length > 0 && length + space + word.text.length > columns) {
|
|
153
|
+
lines.push(current);
|
|
154
|
+
current = [];
|
|
155
|
+
length = 0;
|
|
156
|
+
}
|
|
157
|
+
append(current, length === 0 ? word : { ...word, text: ` ${word.text}` });
|
|
158
|
+
length += (length === 0 ? 0 : 1) + word.text.length;
|
|
159
|
+
}
|
|
160
|
+
if (current.length > 0)
|
|
161
|
+
lines.push(current);
|
|
162
|
+
return lines.length > 0 ? lines : [[{ text: '' }]];
|
|
163
|
+
}
|
|
164
|
+
/** The line's words, each carrying the style of the run it came from. */
|
|
165
|
+
function words(line) {
|
|
166
|
+
const found = [];
|
|
167
|
+
for (const run of line) {
|
|
168
|
+
for (const word of run.text.split(/\s+/).filter(Boolean)) {
|
|
169
|
+
found.push({ ...run, text: word });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
/** Add a word to a line, joining it to the last run when the style is the same. */
|
|
175
|
+
function append(line, word) {
|
|
176
|
+
const last = line[line.length - 1];
|
|
177
|
+
if (last && last.style === word.style) {
|
|
178
|
+
last.text += word.text;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
line.push(word);
|
|
182
|
+
}
|
|
183
|
+
/** The line as it reads, with no markup — what gets measured. */
|
|
184
|
+
export function plain(line) {
|
|
185
|
+
return line.map((run) => run.text).join('');
|
|
186
|
+
}
|
|
187
|
+
/** Every style name the markup in these lines refers to, without repeats. */
|
|
188
|
+
export function markupStyles(lines) {
|
|
189
|
+
const names = new Set();
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
for (const run of line)
|
|
192
|
+
if (run.style !== undefined)
|
|
193
|
+
names.add(run.style);
|
|
194
|
+
}
|
|
195
|
+
return [...names];
|
|
196
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reladraw",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A diagram language where
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A diagram language where you say where things go.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"reladraw": "./dist/cli.js"
|