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/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.1.0",
4
- "description": "A diagram language where placement is stated, not computed.",
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"