reladraw 0.24.0 → 0.25.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/.claude/skills/reladraw/SKILL.md +26 -3
- package/.claude/skills/reladraw/reference/syntax.md +101 -11
- package/README.md +1 -1
- package/SYNTAX.md +101 -11
- package/dist/ast.d.ts +47 -1
- package/dist/ast.js +12 -4
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +34 -1
- package/dist/format.js +5 -1
- package/dist/grammar.d.ts +1 -1
- package/dist/grammar.js +16 -3
- package/dist/linetexts.d.ts +4 -1
- package/dist/linetexts.js +13 -3
- package/dist/links.d.ts +4 -0
- package/dist/markdown.d.ts +14 -0
- package/dist/markdown.js +102 -0
- package/dist/model.d.ts +4 -3
- package/dist/page.js +21 -3
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +244 -27
- package/dist/place.js +3 -5
- package/dist/render.js +52 -28
- package/dist/resolve.js +207 -22
- package/dist/text.d.ts +15 -3
- package/dist/text.js +16 -7
- package/package.json +1 -1
package/dist/constants.js
CHANGED
|
@@ -88,7 +88,40 @@ export function arrowLength(thickness) {
|
|
|
88
88
|
* places that block in the room the node gave it.
|
|
89
89
|
*/
|
|
90
90
|
export function widestLine(lines, measurer, fontSize) {
|
|
91
|
-
return lines.reduce((widest, line) => Math.max(widest,
|
|
91
|
+
return lines.reduce((widest, line) => Math.max(widest, lineWidth(line, measurer, fontSize)), 0);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* How wide one line is. A line all at the text's own size is measured whole;
|
|
95
|
+
* a line with a marked run of another size is the runs side by side, each at
|
|
96
|
+
* its own size.
|
|
97
|
+
*/
|
|
98
|
+
export function lineWidth(line, measurer, fontSize) {
|
|
99
|
+
if (line.every((run) => run.size === undefined))
|
|
100
|
+
return measurer.measure(plain(line), fontSize).width;
|
|
101
|
+
return line.reduce((sum, run) => sum + measurer.measure(run.text, run.size ?? fontSize).width, 0);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The largest size on a line: its own, or a marked run's. Only runs with
|
|
105
|
+
* something to draw count, so a plain space between two small marked words
|
|
106
|
+
* doesn't make their line a normal one.
|
|
107
|
+
*/
|
|
108
|
+
export function lineSize(line, fontSize) {
|
|
109
|
+
const inked = line.filter((run) => run.text.trim().length > 0);
|
|
110
|
+
if (inked.length === 0)
|
|
111
|
+
return fontSize;
|
|
112
|
+
return Math.max(...inked.map((run) => run.size ?? fontSize));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* How tall one line is: as tall as its largest text (design.org 7.1), so a
|
|
116
|
+
* line that is all small takes a small line's spacing and a line mixing sizes
|
|
117
|
+
* takes the largest's.
|
|
118
|
+
*/
|
|
119
|
+
export function lineTall(line, measurer, fontSize) {
|
|
120
|
+
return measurer.lineHeight(lineSize(line, fontSize));
|
|
121
|
+
}
|
|
122
|
+
/** How tall a block of lines is: each line as tall as its own largest text. */
|
|
123
|
+
export function linesTall(lines, measurer, fontSize) {
|
|
124
|
+
return lines.reduce((sum, line) => sum + lineTall(line, measurer, fontSize), 0);
|
|
92
125
|
}
|
|
93
126
|
/**
|
|
94
127
|
* An icon is two lines of the text tall, and that ratio is what makes it a
|
package/dist/format.js
CHANGED
|
@@ -164,8 +164,12 @@ function statementCells(all, keywordCount, starts, settings) {
|
|
|
164
164
|
const result = [];
|
|
165
165
|
const keyword = all.slice(0, keywordCount);
|
|
166
166
|
result.push({ text: joinTokens(keyword), kind: 'keyword' });
|
|
167
|
-
|
|
167
|
+
let runs = clauseRuns(all.slice(keywordCount), starts);
|
|
168
168
|
const head = runs[0] && !isAttrKey(runs[0][0].token) && !starts.has(runs[0][0].token) ? runs.shift() : undefined;
|
|
169
|
+
// A `url` declaration's links are one list, addresses and names alike, kept
|
|
170
|
+
// together after its name rather than spread over the columns of texts.
|
|
171
|
+
if (all[0].token.text === 'url' && runs.length > 1)
|
|
172
|
+
runs = [runs.flat()];
|
|
169
173
|
if (head) {
|
|
170
174
|
if (all[0].token.text === 'edge' && settings['edge-name-and-ends'] === 'own-columns')
|
|
171
175
|
result.push(...edgeHeadCells(head));
|
package/dist/grammar.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface Span {
|
|
|
64
64
|
* it. A word missing here is a word that draws in the plain color, which is a
|
|
65
65
|
* dull page rather than a wrong one.
|
|
66
66
|
*/
|
|
67
|
-
export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "style", "diagram", "default", "icon", "point", "region", "bundle", "guide"];
|
|
67
|
+
export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "style", "diagram", "default", "icon", "description", "url", "point", "region", "bundle", "guide"];
|
|
68
68
|
/**
|
|
69
69
|
* Every word that says something about where a thing goes. Assembled from the
|
|
70
70
|
* lists the parser itself reads, so a direction or a passage axis added there
|
package/dist/grammar.js
CHANGED
|
@@ -33,9 +33,9 @@ import { DEFAULT_TARGETS, DIRECTIONS, MARKS, PARTS, POINT_TEXT_SIDES, POSITIONS,
|
|
|
33
33
|
* it. A word missing here is a word that draws in the plain color, which is a
|
|
34
34
|
* dull page rather than a wrong one.
|
|
35
35
|
*/
|
|
36
|
-
export const STATEMENT_KEYWORDS = ['node', 'edge', 'style', 'diagram', 'default', 'icon', 'point', 'region', 'bundle', 'guide'];
|
|
36
|
+
export const STATEMENT_KEYWORDS = ['node', 'edge', 'style', 'diagram', 'default', 'icon', 'description', 'url', 'point', 'region', 'bundle', 'guide'];
|
|
37
37
|
/** Statements whose second word declares a name. `diagram` has none. */
|
|
38
|
-
const DECLARES_NAME = ['node', 'style', 'icon', 'point', 'region', 'bundle'];
|
|
38
|
+
const DECLARES_NAME = ['node', 'style', 'icon', 'point', 'region', 'bundle', 'url'];
|
|
39
39
|
/**
|
|
40
40
|
* Keywords that take another after them: `guide region channel`, `default
|
|
41
41
|
* leaf`, `default guide edge`. The words that may follow are the kinds a
|
|
@@ -233,7 +233,20 @@ export function highlightLine(line) {
|
|
|
233
233
|
push('keyword', at + next.length);
|
|
234
234
|
keyword = next;
|
|
235
235
|
}
|
|
236
|
-
if (
|
|
236
|
+
if (keyword === 'description') {
|
|
237
|
+
// `description notes """…"""` declares a name; `description for a, b`
|
|
238
|
+
// names things declared elsewhere, so `for` colors as a keyword.
|
|
239
|
+
const space = match(SPACE, line, at);
|
|
240
|
+
if (space !== null) {
|
|
241
|
+
push('plain', at + space.length);
|
|
242
|
+
const word = match(WORD, line, at);
|
|
243
|
+
if (word === 'for')
|
|
244
|
+
push('keyword', at + word.length);
|
|
245
|
+
else if (word !== null && !word.endsWith(':'))
|
|
246
|
+
push('name', at + word.length);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (DECLARES_NAME.includes(keyword)) {
|
|
237
250
|
const space = match(SPACE, line, at);
|
|
238
251
|
if (space !== null) {
|
|
239
252
|
push('plain', at + space.length);
|
package/dist/linetexts.d.ts
CHANGED
|
@@ -32,7 +32,10 @@ export interface PlacedText {
|
|
|
32
32
|
maxX: number;
|
|
33
33
|
maxY: number;
|
|
34
34
|
};
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Moved off its line by `right of edge` and the rest, rather than sitting on it,
|
|
37
|
+
* and meant to be clear of it: not a stated distance or part of the way clear.
|
|
38
|
+
*/
|
|
36
39
|
beside: boolean;
|
|
37
40
|
}
|
|
38
41
|
/**
|
package/dist/linetexts.js
CHANGED
|
@@ -335,9 +335,18 @@ export function placeLineTexts(edge, route) {
|
|
|
335
335
|
const out = { above: [0, -1], below: [0, 1], 'left of': [-1, 0], 'right of': [1, 0] }[place.by.side];
|
|
336
336
|
center = { x: spot.x + (out[0] * size.width) / 2, y: spot.y + (out[1] * size.height) / 2 };
|
|
337
337
|
}
|
|
338
|
-
if (place.beside) {
|
|
338
|
+
if (place.beside && place.rise?.distance !== undefined) {
|
|
339
|
+
// `2 above edge`: that far from its spot on the line, the stated way, whatever the line does.
|
|
340
|
+
const by = gapPixels(place.rise.distance, `${subject}: the text "${text.text}" ${place.rise.written}`, edge.line);
|
|
341
|
+
const out = { above: [0, -1], below: [0, 1], 'left of': [-1, 0], 'right of': [1, 0] }[place.beside];
|
|
342
|
+
center = { x: center.x + out[0] * by, y: center.y + out[1] * by };
|
|
343
|
+
}
|
|
344
|
+
else if (place.beside) {
|
|
339
345
|
const gap = place.edgeGap === undefined ? TEXT_GAP : gapPixels(place.edgeGap, `${subject}: the text "${text.text}" edge-gap`, edge.line);
|
|
340
|
-
|
|
346
|
+
const clear = besideLine(whole, s, center, size, place.beside, gap);
|
|
347
|
+
// `1/2 clear above edge`: that fraction of the move that clears it.
|
|
348
|
+
const fraction = place.rise?.clear ?? 1;
|
|
349
|
+
center = { x: center.x + (clear.x - center.x) * fraction, y: center.y + (clear.y - center.y) * fraction };
|
|
341
350
|
}
|
|
342
351
|
const pad = patchPadding(text, subject, edge.line);
|
|
343
352
|
const knockout = turned(text.width + pad.x * 2, text.height + pad.y * 2, angle);
|
|
@@ -359,7 +368,8 @@ export function placeLineTexts(edge, route) {
|
|
|
359
368
|
maxX: center.x + knockout.width / 2,
|
|
360
369
|
maxY: center.y + knockout.height / 2,
|
|
361
370
|
},
|
|
362
|
-
|
|
371
|
+
// A stated distance, or less than the whole way clear, may leave it across its line on purpose.
|
|
372
|
+
beside: place.beside !== undefined && place.rise?.distance === undefined && (place.rise?.clear ?? 1) >= 1,
|
|
363
373
|
};
|
|
364
374
|
});
|
|
365
375
|
}
|
package/dist/links.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface Link {
|
|
|
21
21
|
name?: string;
|
|
22
22
|
icon?: LinkIcon;
|
|
23
23
|
}
|
|
24
|
+
/** One item in a `url:` list or a `url` declaration: an address with its bracket, or a declared link's name. */
|
|
25
|
+
export type LinkEntry = Link | {
|
|
26
|
+
ref: string;
|
|
27
|
+
};
|
|
24
28
|
/** Every link a thing carries, in the order the source writes them. */
|
|
25
29
|
export declare function linksOf(attrs: Attrs): Link[] | undefined;
|
|
26
30
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown a description is written in: paragraphs, lists (`-`, `*` or
|
|
3
|
+
* `1.`), `**bold**`, `*italic*` or `_italic_`, `` `code` `` and
|
|
4
|
+
* `[text](address)`. Anything else is shown as the characters written.
|
|
5
|
+
*
|
|
6
|
+
* Two readings of one text: HTML for the page's panel, and plain text for an
|
|
7
|
+
* SVG's hover text, which can't show formatting.
|
|
8
|
+
*/
|
|
9
|
+
/** Every link address in a description that the page would refuse to follow. */
|
|
10
|
+
export declare function unsafeLinks(text: string): string[];
|
|
11
|
+
/** A description as HTML, for the page's panel. */
|
|
12
|
+
export declare function markdownHtml(text: string): string;
|
|
13
|
+
/** A description with its marks taken out, for an SVG's hover text. A link keeps its address after its text. */
|
|
14
|
+
export declare function markdownPlain(text: string): string;
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown a description is written in: paragraphs, lists (`-`, `*` or
|
|
3
|
+
* `1.`), `**bold**`, `*italic*` or `_italic_`, `` `code` `` and
|
|
4
|
+
* `[text](address)`. Anything else is shown as the characters written.
|
|
5
|
+
*
|
|
6
|
+
* Two readings of one text: HTML for the page's panel, and plain text for an
|
|
7
|
+
* SVG's hover text, which can't show formatting.
|
|
8
|
+
*/
|
|
9
|
+
const BULLET = /^\s*[-*]\s+(.*)$/;
|
|
10
|
+
const NUMBERED = /^\s*\d+[.)]\s+(.*)$/;
|
|
11
|
+
function blocksOf(text) {
|
|
12
|
+
const blocks = [];
|
|
13
|
+
let open;
|
|
14
|
+
for (const line of text.split('\n')) {
|
|
15
|
+
if (line.trim() === '') {
|
|
16
|
+
open = undefined;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const bullet = line.match(BULLET);
|
|
20
|
+
const numbered = bullet ? null : line.match(NUMBERED);
|
|
21
|
+
const item = bullet ?? numbered;
|
|
22
|
+
if (item) {
|
|
23
|
+
const ordered = numbered !== null;
|
|
24
|
+
if (open?.kind !== 'list' || open.ordered !== ordered) {
|
|
25
|
+
open = { kind: 'list', ordered, items: [] };
|
|
26
|
+
blocks.push(open);
|
|
27
|
+
}
|
|
28
|
+
open.items.push(item[1].trim());
|
|
29
|
+
}
|
|
30
|
+
else if (open?.kind === 'list' && /^\s/.test(line)) {
|
|
31
|
+
// An indented line under an item continues it.
|
|
32
|
+
open.items[open.items.length - 1] += ` ${line.trim()}`;
|
|
33
|
+
}
|
|
34
|
+
else if (open?.kind === 'paragraph') {
|
|
35
|
+
open.lines.push(line.trim());
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
open = { kind: 'paragraph', lines: [line.trim()] };
|
|
39
|
+
blocks.push(open);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return blocks;
|
|
43
|
+
}
|
|
44
|
+
const LINK = /\[([^\]]+)\]\(([^)\s]+)\)/g;
|
|
45
|
+
/** The addresses a description may link to: the web, mail, or a path with no scheme. */
|
|
46
|
+
const SAFE_ADDRESS = /^(https?:|mailto:|[^:]*$)/i;
|
|
47
|
+
/** Every link address in a description that the page would refuse to follow. */
|
|
48
|
+
export function unsafeLinks(text) {
|
|
49
|
+
return [...text.matchAll(LINK)].map((match) => match[2]).filter((href) => !SAFE_ADDRESS.test(href));
|
|
50
|
+
}
|
|
51
|
+
function escapeHtml(text) {
|
|
52
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
53
|
+
}
|
|
54
|
+
/** One line's marks as HTML. Code is set aside first, so nothing inside it is read as a mark. */
|
|
55
|
+
function inlineHtml(text) {
|
|
56
|
+
const codes = [];
|
|
57
|
+
let out = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
58
|
+
codes.push(`<code>${escapeHtml(code)}</code>`);
|
|
59
|
+
return `\u0000${codes.length - 1}\u0000`;
|
|
60
|
+
});
|
|
61
|
+
const links = [];
|
|
62
|
+
out = out.replace(LINK, (_, label, href) => {
|
|
63
|
+
const safe = SAFE_ADDRESS.test(href) ? href : '#';
|
|
64
|
+
links.push(`<a href="${escapeHtml(safe)}" target="_blank" rel="noopener">${marks(escapeHtml(label))}</a>`);
|
|
65
|
+
return `\u0001${links.length - 1}\u0001`;
|
|
66
|
+
});
|
|
67
|
+
out = marks(escapeHtml(out));
|
|
68
|
+
return out
|
|
69
|
+
.replace(/\u0001(\d+)\u0001/g, (_, index) => links[Number(index)])
|
|
70
|
+
.replace(/\u0000(\d+)\u0000/g, (_, index) => codes[Number(index)]);
|
|
71
|
+
}
|
|
72
|
+
function marks(html) {
|
|
73
|
+
return html
|
|
74
|
+
.replace(/\*\*(?=\S)(.+?)(?<=\S)\*\*/g, '<strong>$1</strong>')
|
|
75
|
+
.replace(/(^|[^*\w])\*(?=\S)(.+?)(?<=\S)\*(?![*\w])/g, '$1<em>$2</em>')
|
|
76
|
+
.replace(/(^|[^_\w])_(?=\S)(.+?)(?<=\S)_(?![_\w])/g, '$1<em>$2</em>');
|
|
77
|
+
}
|
|
78
|
+
/** A description as HTML, for the page's panel. */
|
|
79
|
+
export function markdownHtml(text) {
|
|
80
|
+
return blocksOf(text)
|
|
81
|
+
.map((block) => {
|
|
82
|
+
if (block.kind === 'paragraph')
|
|
83
|
+
return `<p>${block.lines.map(inlineHtml).join(' ')}</p>`;
|
|
84
|
+
const tag = block.ordered ? 'ol' : 'ul';
|
|
85
|
+
return `<${tag}>${block.items.map((item) => `<li>${inlineHtml(item)}</li>`).join('')}</${tag}>`;
|
|
86
|
+
})
|
|
87
|
+
.join('');
|
|
88
|
+
}
|
|
89
|
+
/** A description with its marks taken out, for an SVG's hover text. A link keeps its address after its text. */
|
|
90
|
+
export function markdownPlain(text) {
|
|
91
|
+
const inline = (line) => line
|
|
92
|
+
.replace(LINK, (_, label, href) => `${label} (${href})`)
|
|
93
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
94
|
+
.replace(/\*\*(?=\S)(.+?)(?<=\S)\*\*/g, '$1')
|
|
95
|
+
.replace(/(^|[^*\w])\*(?=\S)(.+?)(?<=\S)\*(?![*\w])/g, '$1$2')
|
|
96
|
+
.replace(/(^|[^_\w])_(?=\S)(.+?)(?<=\S)_(?![_\w])/g, '$1$2');
|
|
97
|
+
return blocksOf(text)
|
|
98
|
+
.map((block) => block.kind === 'paragraph'
|
|
99
|
+
? block.lines.map(inline).join(' ')
|
|
100
|
+
: block.items.map((item, index) => `${block.ordered ? `${index + 1}.` : '•'} ${inline(item)}`).join('\n'))
|
|
101
|
+
.join('\n\n');
|
|
102
|
+
}
|
package/dist/model.d.ts
CHANGED
|
@@ -307,9 +307,10 @@ export interface Layout {
|
|
|
307
307
|
roots: LayoutNode[];
|
|
308
308
|
edges: LayoutEdge[];
|
|
309
309
|
/**
|
|
310
|
-
* Every style the file's markup names
|
|
311
|
-
* renderer needs it because a marked run borrows its color from a
|
|
312
|
-
* rather than stating one, and styles are otherwise merged away by
|
|
310
|
+
* Every style the file's markup names that lends a color, resolved to that
|
|
311
|
+
* color. The renderer needs it because a marked run borrows its color from a
|
|
312
|
+
* style rather than stating one, and styles are otherwise merged away by
|
|
313
|
+
* here. A size a style lends is carried on the run itself.
|
|
313
314
|
*/
|
|
314
315
|
markup: Record<string, string>;
|
|
315
316
|
/** The pictures links' badges name, by name, for the page that draws the badges. */
|
package/dist/page.js
CHANGED
|
@@ -2,6 +2,7 @@ import { fileChecks, findings } from './checks.js';
|
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
3
|
import { blame, parse } from './parser.js';
|
|
4
4
|
import { linkLabel, linksOf } from './links.js';
|
|
5
|
+
import { markdownHtml } from './markdown.js';
|
|
5
6
|
import { iconSvg, renderTagged, themeOf } from './render.js';
|
|
6
7
|
import { resolve } from './resolve.js';
|
|
7
8
|
import { luminance } from './themes.js';
|
|
@@ -38,6 +39,8 @@ export function drawPage(source, file, options = {}, at = new Date()) {
|
|
|
38
39
|
throw error instanceof SourceError ? blame(source, error, run) : error;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
/** A description as the panel shows it: its Markdown as HTML, or nothing. */
|
|
43
|
+
const describe = (attrs) => attrs['description'] === undefined ? {} : { description: markdownHtml(attrs['description']) };
|
|
41
44
|
const BADGE_SIDE = 20;
|
|
42
45
|
function graphOf(layout, theme) {
|
|
43
46
|
const views = (links) => (links ?? []).map((link) => {
|
|
@@ -62,7 +65,7 @@ function graphOf(layout, theme) {
|
|
|
62
65
|
const nodes = {};
|
|
63
66
|
for (const node of layout.nodes) {
|
|
64
67
|
const text = plainText(node.text);
|
|
65
|
-
nodes[node.name] = { label: text === '' ? node.name.split('.').pop() : text, links: views(linksFor(node)) };
|
|
68
|
+
nodes[node.name] = { label: text === '' ? node.name.split('.').pop() : text, links: views(linksFor(node)), ...describe(node.attrs) };
|
|
66
69
|
}
|
|
67
70
|
const edges = layout.edges.map((edge) => {
|
|
68
71
|
// The upstream end is the one a line runs from: the end without a mark
|
|
@@ -71,7 +74,7 @@ function graphOf(layout, theme) {
|
|
|
71
74
|
const backwards = start !== '' && end === '';
|
|
72
75
|
const [up, down] = backwards ? [edge.to, edge.from] : [edge.from, edge.to];
|
|
73
76
|
const text = edge.texts.map((one) => plainText(one.text)).filter((one) => one !== '').join(' · ');
|
|
74
|
-
return { up: up.name, down: down.name, label: text, links: views(linksOf(edge.attrs)) };
|
|
77
|
+
return { up: up.name, down: down.name, label: text, links: views(linksOf(edge.attrs)), ...describe(edge.attrs) };
|
|
75
78
|
});
|
|
76
79
|
return { nodes, edges };
|
|
77
80
|
}
|
|
@@ -202,6 +205,11 @@ const PAGE = `<!doctype html>
|
|
|
202
205
|
}
|
|
203
206
|
#selected h3 svg { flex: none; }
|
|
204
207
|
#selected .none { color: var(--muted); font-size: 13px; }
|
|
208
|
+
#selected .description { font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
209
|
+
#selected .description p, #selected .description ul, #selected .description ol { margin: 0 0 8px; }
|
|
210
|
+
#selected .description ul, #selected .description ol { padding-left: 20px; }
|
|
211
|
+
#selected .description code { font-size: 12px; padding: 0 3px; border-radius: 3px; background: color-mix(in srgb, var(--ink) 12%, transparent); }
|
|
212
|
+
#selected .description a { color: inherit; text-decoration: underline; }
|
|
205
213
|
#selected .link {
|
|
206
214
|
display: flex; align-items: center; gap: 9px; padding: 4px 6px; margin: 0 -6px;
|
|
207
215
|
border-radius: 5px; color: var(--ink); text-decoration: none;
|
|
@@ -689,11 +697,12 @@ const PAGE = `<!doctype html>
|
|
|
689
697
|
selectedPanel.append(element('p', 'empty', 'Click a box or a line to see its links and what it connects to.'));
|
|
690
698
|
return;
|
|
691
699
|
}
|
|
692
|
-
var heading, links, from, to;
|
|
700
|
+
var heading, links, from, to, description;
|
|
693
701
|
if (selection.kind === 'node') {
|
|
694
702
|
var node = graph.nodes[selection.key];
|
|
695
703
|
heading = node ? node.label : selection.key;
|
|
696
704
|
links = node ? node.links : [];
|
|
705
|
+
description = node && node.description;
|
|
697
706
|
from = []; to = [];
|
|
698
707
|
graph.edges.forEach(function (edge) {
|
|
699
708
|
if (edge.down === selection.key && from.indexOf(edge.up) < 0) from.push(edge.up);
|
|
@@ -704,6 +713,7 @@ const PAGE = `<!doctype html>
|
|
|
704
713
|
var name = function (key) { return graph.nodes[key] ? graph.nodes[key].label : key; };
|
|
705
714
|
heading = edge.label || name(edge.up) + ' \\u2192 ' + name(edge.down);
|
|
706
715
|
links = edge.links;
|
|
716
|
+
description = edge.description;
|
|
707
717
|
from = [edge.up]; to = [edge.down];
|
|
708
718
|
}
|
|
709
719
|
selectedPanel.append(element('h2', '', heading));
|
|
@@ -730,6 +740,14 @@ const PAGE = `<!doctype html>
|
|
|
730
740
|
all.setAttribute('aria-pressed', String(everything));
|
|
731
741
|
all.onclick = function () { everything = !everything; select(selection); };
|
|
732
742
|
selectedPanel.append(all);
|
|
743
|
+
// Last, so a long description never pushes the rest out of view, and the
|
|
744
|
+
// sections above stay where they are whatever is selected.
|
|
745
|
+
if (description) {
|
|
746
|
+
// Written by reladraw from the file's Markdown, every character escaped.
|
|
747
|
+
var about = element('div', 'description');
|
|
748
|
+
about.innerHTML = description;
|
|
749
|
+
selectedPanel.append(element('h3', '', 'Description'), about);
|
|
750
|
+
}
|
|
733
751
|
}
|
|
734
752
|
|
|
735
753
|
// The pan and zoom capture the pointer, so the click lands on the view; what
|
package/dist/parser.d.ts
CHANGED
|
@@ -54,3 +54,10 @@ export declare function parse(source: string, seen?: (statement: Gathered) => vo
|
|
|
54
54
|
export declare function blame(source: string, error: SourceError, run: (source: string) => unknown): SourceError;
|
|
55
55
|
/** Whether a token is an arrow, or begins one: a bracketed mark, or a run of dashes with marks at its ends. */
|
|
56
56
|
export declare function looksLikeArrow(token: Token): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* A description's text as written between `"""` marks, without the line break
|
|
59
|
+
* after the opening marks, the blank lines at either end, or the indentation
|
|
60
|
+
* every line shares, so a description indented under its statement reads as
|
|
61
|
+
* Markdown from the left margin.
|
|
62
|
+
*/
|
|
63
|
+
export declare function descriptionText(raw: string): string;
|