reladraw 0.19.1 → 0.21.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 +2 -2
- package/SYNTAX.md +59 -8
- package/dist/ast.d.ts +26 -2
- package/dist/ast.js +15 -2
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +79 -5
- package/dist/cli.js +35 -12
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +8 -0
- package/dist/constrain.d.ts +56 -0
- package/dist/constrain.js +95 -0
- package/dist/grammar.js +2 -0
- package/dist/model.d.ts +16 -1
- package/dist/model.js +7 -0
- package/dist/page.d.ts +14 -0
- package/dist/page.js +488 -0
- package/dist/parser.js +69 -4
- package/dist/place.d.ts +74 -1
- package/dist/place.js +303 -18
- package/dist/render.d.ts +17 -0
- package/dist/render.js +101 -29
- package/dist/resolve.d.ts +14 -0
- package/dist/resolve.js +34 -7
- package/dist/themes.d.ts +1 -0
- package/dist/themes.js +1 -1
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -3,7 +3,7 @@ import { SourceError } from './errors.js';
|
|
|
3
3
|
import { ICON_STROKE } from './icons.js';
|
|
4
4
|
import { insideOutline, isBoxy, isRound, placePath, shapeOf, SUBPROCESS_INSET } from './outline.js';
|
|
5
5
|
import { monospaceMeasurer } from './measure.js';
|
|
6
|
-
import { joinedToText } from './model.js';
|
|
6
|
+
import { joinedToText, outlineStated } from './model.js';
|
|
7
7
|
import { DARK_THEME, THEMES, overlapFill, regionTint, textOnFill, themeColor } from './themes.js';
|
|
8
8
|
import { crossingPoints, planLines, traceRoute, visibleRuns } from './lines.js';
|
|
9
9
|
import { patchPadding, placeLineTexts } from './linetexts.js';
|
|
@@ -11,22 +11,21 @@ import { plain } from './text.js';
|
|
|
11
11
|
const CORNER = 8;
|
|
12
12
|
/** Turn solved geometry into a standalone SVG document. */
|
|
13
13
|
export function render(layout, options = {}) {
|
|
14
|
+
return drawing(layout, options)(false);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The SVG as `render` gives it, and the same drawing with each thing wrapped in
|
|
18
|
+
* a group naming it and its statement's line, for a page that points at things
|
|
19
|
+
* (`--page`). Drawn once, so the two can't disagree.
|
|
20
|
+
*/
|
|
21
|
+
export function renderTagged(layout, options = {}) {
|
|
22
|
+
const assemble = drawing(layout, options);
|
|
23
|
+
return { svg: assemble(false), tagged: assemble(true) };
|
|
24
|
+
}
|
|
25
|
+
function drawing(layout, options) {
|
|
14
26
|
const measurer = options.measurer ?? monospaceMeasurer();
|
|
15
27
|
const fontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
|
|
16
|
-
|
|
17
|
-
// names, so one source renders in either. `diagram background:` and `text:`
|
|
18
|
-
// are the author overruling a color of whichever theme that is, and a color
|
|
19
|
-
// written by hand wins over any theme, so they are folded in afterwards and
|
|
20
|
-
// everything downstream sees one theme.
|
|
21
|
-
const named = layout.diagram['theme'];
|
|
22
|
-
const base = options.theme ?? (named === undefined ? undefined : THEMES[named]) ?? DARK_THEME;
|
|
23
|
-
const page = layout.diagram['background'];
|
|
24
|
-
const words = layout.diagram['text.color'];
|
|
25
|
-
const theme = {
|
|
26
|
-
...base,
|
|
27
|
-
...(page !== undefined && { background: themeColor(page, base) }),
|
|
28
|
-
...(words !== undefined && { text: themeColor(words, base) }),
|
|
29
|
-
};
|
|
28
|
+
const theme = themeOf(layout, options);
|
|
30
29
|
// Guides are shown by the viewer (`--guides`), or by a file that explains
|
|
31
30
|
// its own construction (`diagram guides: show`).
|
|
32
31
|
const guides = {
|
|
@@ -110,8 +109,8 @@ export function render(layout, options = {}) {
|
|
|
110
109
|
}
|
|
111
110
|
const lineSvg = new Map(lines.map((line) => [line.key, line.svg]));
|
|
112
111
|
const body = sequence
|
|
113
|
-
.map((item) => lineSvg.get(item.key) ?? item.svg)
|
|
114
|
-
.filter((svg) => svg.length > 0);
|
|
112
|
+
.map((item) => ({ item, svg: lineSvg.get(item.key) ?? item.svg }))
|
|
113
|
+
.filter(({ svg }) => svg.length > 0);
|
|
115
114
|
// An edge's geometry is measured rather than solved for, so the resolver sized
|
|
116
115
|
// the canvas from the boxes alone. A curve out of a `top` side, or a text
|
|
117
116
|
// riding above one, lands outside that — so the page grows to hold it and the
|
|
@@ -135,8 +134,11 @@ export function render(layout, options = {}) {
|
|
|
135
134
|
markers.set(markerId(mark, color, free), [mark, color, free]);
|
|
136
135
|
}
|
|
137
136
|
}
|
|
138
|
-
|
|
137
|
+
return (tagged) => ownIds([
|
|
139
138
|
`<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">`,
|
|
139
|
+
// `diagram title:` — what a browser shows on the tab when the SVG is opened
|
|
140
|
+
// on its own, and what a screen reader announces.
|
|
141
|
+
...(layout.diagram['title'] === undefined ? [] : [` <title>${escapeXml(layout.diagram['title'])}</title>`]),
|
|
140
142
|
' <defs>',
|
|
141
143
|
...[...markers.values()].map(([mark, color, free]) => markMarker(mark, color, theme.background, free)),
|
|
142
144
|
// Each mask covers the whole page, in page units: the default region is the
|
|
@@ -150,11 +152,51 @@ export function render(layout, options = {}) {
|
|
|
150
152
|
].join('\n')),
|
|
151
153
|
' </defs>',
|
|
152
154
|
` <rect x="${canvas.x}" y="${canvas.y}" width="${canvas.width}" height="${canvas.height}" fill="${theme.background}"/>`,
|
|
153
|
-
...body,
|
|
155
|
+
...body.map(({ item, svg }) => (tagged ? tag(layout, item, svg) : svg)),
|
|
154
156
|
'</svg>',
|
|
155
157
|
'',
|
|
156
|
-
].join('\n');
|
|
157
|
-
|
|
158
|
+
].join('\n'));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The colors a drawing is made in. A theme passed in — the command line's
|
|
162
|
+
* `--theme` — beats the one the file names, so one source renders in either.
|
|
163
|
+
* `diagram background:` and `text:` are the author overruling a color of
|
|
164
|
+
* whichever theme that is, and a color written by hand wins over any theme, so
|
|
165
|
+
* they are folded in afterwards and everything downstream sees one theme.
|
|
166
|
+
*/
|
|
167
|
+
export function themeOf(layout, options = {}) {
|
|
168
|
+
const named = layout.diagram['theme'];
|
|
169
|
+
const base = options.theme ?? (named === undefined ? undefined : THEMES[named]) ?? DARK_THEME;
|
|
170
|
+
const page = layout.diagram['background'];
|
|
171
|
+
const words = layout.diagram['text.color'];
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
...(page !== undefined && { background: themeColor(page, base) }),
|
|
175
|
+
...(words !== undefined && { text: themeColor(words, base) }),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* `<g data-name="api" data-line="12">` round each thing drawn: what a page
|
|
180
|
+
* shows on hover, and what a finding about that line lights up.
|
|
181
|
+
*/
|
|
182
|
+
function tag(layout, item, svg) {
|
|
183
|
+
let name;
|
|
184
|
+
let line;
|
|
185
|
+
if (item.node) {
|
|
186
|
+
({ name, line } = item.node);
|
|
187
|
+
}
|
|
188
|
+
else if (item.key.startsWith('edge ')) {
|
|
189
|
+
const edge = layout.edges[Number(item.key.slice('edge '.length))];
|
|
190
|
+
name = edge.name ?? `${edge.from.name} ${edge.arrow} ${edge.to.name}`;
|
|
191
|
+
line = edge.line;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const mark = layout.marks.find((one) => one.name === item.key);
|
|
195
|
+
if (!mark)
|
|
196
|
+
return svg;
|
|
197
|
+
({ name, line } = mark);
|
|
198
|
+
}
|
|
199
|
+
return `<g data-name="${escapeXml(name).replace(/"/g, '"')}" data-line="${line}">\n${svg}\n</g>`;
|
|
158
200
|
}
|
|
159
201
|
/**
|
|
160
202
|
* Gives every id in a drawing a prefix of its own, taken from a hash of the
|
|
@@ -305,7 +347,7 @@ function drawNode(node, theme, measurer, fontSize, markup, guides, inherited) {
|
|
|
305
347
|
* cards drawn back to front, so the first, the front card, is on top.
|
|
306
348
|
*/
|
|
307
349
|
function drawDeck(node, theme, measurer, fontSize, markup, guides, url) {
|
|
308
|
-
const outlined = node.appearance
|
|
350
|
+
const outlined = outlineStated(node.appearance);
|
|
309
351
|
const drawn = outlined ? node : { ...node, appearance: { ...node.appearance, fill: 'none', border: 'none' } };
|
|
310
352
|
const cards = node.children.filter((child) => child !== node.heading && !joinedToText(child));
|
|
311
353
|
// The cards are the same thing, so each looks like the front one unless it says otherwise.
|
|
@@ -364,8 +406,29 @@ function borderSvg(node, theme) {
|
|
|
364
406
|
if (node.body.kind !== 'shape')
|
|
365
407
|
return '';
|
|
366
408
|
const face = faceOf(node);
|
|
409
|
+
const d = outlinePath(node.body.outline, face.x, face.y, face.width, face.height, node.corners, node.body.look);
|
|
410
|
+
const stroke = borderStroke(node.borderWidth, `<path d="${d}"/>`, `rim-${node.name.replace(/[^\w.-]/g, '_')}`);
|
|
367
411
|
const border = borderOf(node.appearance, theme, theme.containerStroke);
|
|
368
|
-
return
|
|
412
|
+
return `${stroke.clip} <path d="${d}" fill="none" stroke="${border}"${stroke.attr}/>`;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* How a border is stroked. A thick border lies wholly inside the outline: the
|
|
416
|
+
* outline is stroked twice as wide and clipped to itself, so only the inner
|
|
417
|
+
* half shows, and its outer edge is the outline exactly, along every side and
|
|
418
|
+
* at every corner and point of every shape, where a line ends and a gap is
|
|
419
|
+
* measured from (design.org 9.1). A normal border straddles the outline, as
|
|
420
|
+
* every box's always did, and writes nothing here. `detail` is for the lines
|
|
421
|
+
* drawn inside a shape, such as a cylinder's rim, which are as wide as the
|
|
422
|
+
* border and straddle where they run.
|
|
423
|
+
*/
|
|
424
|
+
function borderStroke(width, outline, id) {
|
|
425
|
+
if (width === undefined)
|
|
426
|
+
return { clip: '', attr: '', detail: '' };
|
|
427
|
+
return {
|
|
428
|
+
clip: ` <clipPath id="${id}">${outline}</clipPath>\n`,
|
|
429
|
+
attr: ` stroke-width="${round(width * 2)}" clip-path="url(#${id})"`,
|
|
430
|
+
detail: ` stroke-width="${round(width)}" clip-path="url(#${id})"`,
|
|
431
|
+
};
|
|
369
432
|
}
|
|
370
433
|
/** What a node draws of its own, without its children, which are drawn as items of their own. */
|
|
371
434
|
function nodeSvg(node, theme, measurer, fontSize, markup) {
|
|
@@ -431,18 +494,22 @@ function nodeSvg(node, theme, measurer, fontSize, markup) {
|
|
|
431
494
|
// and the fill, drawn without a stroke, still shows behind the title's inner
|
|
432
495
|
// half.
|
|
433
496
|
const onBorder = node.lines.length > 0 && node.textAttrs['place']?.startsWith('on ') === true;
|
|
497
|
+
const d = outlinePath(outline, face.x, face.y, face.width, face.height, node.corners, look);
|
|
498
|
+
const stroke = borderStroke(node.borderWidth, `<path d="${d}"/>`, `border-${node.name.replace(/[^\w.-]/g, '_')}`);
|
|
499
|
+
if (stroke.clip)
|
|
500
|
+
parts.push(stroke.clip.trimEnd());
|
|
434
501
|
if (onBorder) {
|
|
435
502
|
const id = `title-${node.name.replace(/[^\w.-]/g, '_')}`;
|
|
436
503
|
const room = PAD / 2;
|
|
437
504
|
const hole = node.textBox;
|
|
438
505
|
const grown = { x: face.x - PAD, y: face.y - PAD, w: face.width + PAD * 2, h: face.height + PAD * 2 };
|
|
439
|
-
parts.push(` <mask id="${id}" maskUnits="userSpaceOnUse" x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}">`, ` <rect x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}" fill="white"/>`, ` <rect x="${round(node.x + hole.x - room)}" y="${round(node.y + hole.y)}" width="${round(hole.width + room * 2)}" height="${round(hole.height)}" fill="black"/>`, ' </mask>', ` <path d="${
|
|
506
|
+
parts.push(` <mask id="${id}" maskUnits="userSpaceOnUse" x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}">`, ` <rect x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}" fill="white"/>`, ` <rect x="${round(node.x + hole.x - room)}" y="${round(node.y + hole.y)}" width="${round(hole.width + room * 2)}" height="${round(hole.height)}" fill="black"/>`, ' </mask>', ` <path d="${d}" fill="${fill}" stroke="none"/>`, ` <path d="${d}" fill="none" stroke="${border}"${stroke.attr} mask="url(#${id})"/>`);
|
|
440
507
|
}
|
|
441
508
|
else {
|
|
442
|
-
parts.push(` <path d="${
|
|
509
|
+
parts.push(` <path d="${d}" fill="${fill}" stroke="${border}"${stroke.attr}/>`);
|
|
443
510
|
}
|
|
444
511
|
for (const extra of outlineDetail(outline, face.x, face.y, face.width, face.height, look)) {
|
|
445
|
-
parts.push(` <path d="${extra}" fill="none" stroke="${border}"/>`);
|
|
512
|
+
parts.push(` <path d="${extra}" fill="none" stroke="${border}"${stroke.detail}/>`);
|
|
446
513
|
}
|
|
447
514
|
// A leaf's text defaults to the middle of its box, a container's to the top
|
|
448
515
|
// left of the band; both then read `at` for where it really goes. Where the
|
|
@@ -493,6 +560,11 @@ function markSvg(mark, theme, guides, markup, fontSize) {
|
|
|
493
560
|
? `d="M${round(mark.x)} ${round(mark.y)}L${round(mark.x + mark.width)} ${round(mark.y + mark.height)}"`
|
|
494
561
|
: `x="${round(mark.x)}" y="${round(mark.y)}" width="${round(mark.width)}" height="${round(mark.height)}" rx="4"`;
|
|
495
562
|
const shape = flat ? 'path' : 'rect';
|
|
563
|
+
// A thick border lies inside the region's stated edge, as a box's does; one
|
|
564
|
+
// drawn as a line has no inside, so it is only wider.
|
|
565
|
+
const edge = flat
|
|
566
|
+
? { clip: '', attr: mark.borderWidth === undefined ? '' : ` stroke-width="${round(mark.borderWidth)}"` }
|
|
567
|
+
: borderStroke(mark.borderWidth, `<rect ${box}/>`, `edge-${mark.name.replace(/[^\w.-]/g, '_')}`);
|
|
496
568
|
// A text on the border breaks it, as a title on a box's border does.
|
|
497
569
|
const hole = mark.text && mark.text.textAttrs['place']?.startsWith('on ') ? mark.text.box : undefined;
|
|
498
570
|
const id = `region-${mark.name.replace(/[^\w.-]/g, '_')}`;
|
|
@@ -506,13 +578,13 @@ function markSvg(mark, theme, guides, markup, fontSize) {
|
|
|
506
578
|
if (mark.guide) {
|
|
507
579
|
const fill = color(said('fill')) ?? 'none';
|
|
508
580
|
const border = color(said('border')) ?? guides.color;
|
|
509
|
-
return `${masked} <${shape} ${box} fill="${fill}" stroke="${border}" stroke-dasharray="${GUIDE_DASH}"${mask}/>${label}`;
|
|
581
|
+
return `${edge.clip}${masked} <${shape} ${box} fill="${fill}" stroke="${border}"${edge.attr} stroke-dasharray="${GUIDE_DASH}"${mask}/>${label}`;
|
|
510
582
|
}
|
|
511
583
|
const fill = regionFill(mark, theme);
|
|
512
584
|
const border = color(said('border'));
|
|
513
585
|
const opacity = fill.color === 'none' || fill.opacity >= 1 ? '' : ` fill-opacity="${round(fill.opacity)}"`;
|
|
514
|
-
return (`${masked} <${shape} ${box} fill="${fill.color}"${opacity}` +
|
|
515
|
-
` stroke="${border ?? theme.primary.color}"${border === undefined ? ' stroke-opacity="0.5"' : ''} stroke-width="1"${mask}/>${label}`);
|
|
586
|
+
return (`${edge.clip}${masked} <${shape} ${box} fill="${fill.color}"${opacity}` +
|
|
587
|
+
` stroke="${border ?? theme.primary.color}"${border === undefined ? ' stroke-opacity="0.5"' : ''}${edge.attr || ' stroke-width="1"'}${mask}/>${label}`);
|
|
516
588
|
}
|
|
517
589
|
/**
|
|
518
590
|
* A region's fill (design.org 8.3): what it states, or the theme's tint, at
|
package/dist/resolve.d.ts
CHANGED
|
@@ -26,3 +26,17 @@ export interface ResolveOptions {
|
|
|
26
26
|
* sits on is ever decided here, because the author wrote it down.
|
|
27
27
|
*/
|
|
28
28
|
export declare function resolve(doc: Document, options?: ResolveOptions): Layout;
|
|
29
|
+
/**
|
|
30
|
+
* How a node's corners are drawn. A value is checked wherever it came from — a
|
|
31
|
+
* style's `corners: sharpp` is as much a typo as a node's — but a circle refuses
|
|
32
|
+
* `corners:` only when the node itself says it, since a circle has none. From a
|
|
33
|
+
* style or a default it lands on the outlines that have corners, the way a
|
|
34
|
+
* line's `corners: rounded` from a style lands only on lines with corners. A
|
|
35
|
+
* node drawn as a picture or with no body has no outline; `checkAttrs` has
|
|
36
|
+
* already refused the word written on one, and a style's is unused.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* A border's thickness in pixels: `thin`, `normal`, `thick`, a multiple of
|
|
40
|
+
* normal, or `px`, read as a line's is (design.org 3.8).
|
|
41
|
+
*/
|
|
42
|
+
export declare function borderWidthOf(what: string, value: string | undefined, line: number): number;
|
package/dist/resolve.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { TEXT_GAP } from './linetexts.js';
|
|
2
2
|
import { POINT_KEYS, REGION_KEYS, ALL_ATTR_KEYS, ATTR_KEYS, MARKS, COLOR_KEYS, COLOR_PARTS, CHILD_ALIGNMENTS, GATHER_PLACES, CONTENT_WIDTHS, SIZE_KEY_NAMES, LINE_VALUES, NODE_CORNERS, PADDING_SIDES, describePlacement, nameTarget, partWordHint, } from './ast.js';
|
|
3
|
-
import { DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, ICON_GAP, ICON_LINES, LINE_WIDTH, PAD, THICKNESS, fontSizeFor, widestLine, } from './constants.js';
|
|
3
|
+
import { DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, ICON_GAP, ICON_LINES, LINE_WIDTH, PAD, THICKNESS, BORDER_THICKNESS, BORDER_WIDTH, fontSizeFor, widestLine, } from './constants.js';
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
5
5
|
import { bodyFor, pastedIcon } from './icons.js';
|
|
6
6
|
import { monospaceMeasurer } from './measure.js';
|
|
7
7
|
import { isBoxy, middleOf, sizeFor, SUBPROCESS_INSET } from './outline.js';
|
|
8
8
|
import { markupStyles, parseMarkup, plain, splitRuns, wrapLine } from './text.js';
|
|
9
|
-
import { joinedToText } from './model.js';
|
|
9
|
+
import { joinedToText, outlineStated } from './model.js';
|
|
10
10
|
import { checkOpacity, gapPixels, Placer, regionTextAt } from './place.js';
|
|
11
11
|
/**
|
|
12
12
|
* What a node's own contents make of it, before anything is placed: a leaf's
|
|
@@ -31,12 +31,15 @@ function factsFor(node, measurer, fontSize) {
|
|
|
31
31
|
const venn = vennOf(node);
|
|
32
32
|
const circle = isCircle(node);
|
|
33
33
|
// A deck that draws no outline of its own is its cards, with nothing round them.
|
|
34
|
-
const undrawnDeck = node.appearance['layout'] === 'deck' && node.appearance
|
|
34
|
+
const undrawnDeck = node.appearance['layout'] === 'deck' && !outlineStated(node.appearance);
|
|
35
|
+
// A thick border is drawn inside the outline, so the padding is measured
|
|
36
|
+
// from its inner edge and the box grows by the extra (design.org 9.1).
|
|
37
|
+
const extra = (node.borderWidth ?? BORDER_WIDTH) - BORDER_WIDTH;
|
|
35
38
|
const padding = node.body.kind === 'none' || (undrawnDeck && node.appearance['padding'] === undefined)
|
|
36
39
|
? 0
|
|
37
|
-
: node.appearance['padding'] !== undefined
|
|
40
|
+
: (node.appearance['padding'] !== undefined
|
|
38
41
|
? gapPixels(node.appearance['padding'], `"${node.name}" padding:`, node.line)
|
|
39
|
-
: PAD;
|
|
42
|
+
: PAD) + extra;
|
|
40
43
|
// `padding-right: 4`: one side's padding, over `padding:` there.
|
|
41
44
|
const pad = { top: padding, right: padding, bottom: padding, left: padding };
|
|
42
45
|
for (const side of PADDING_SIDES) {
|
|
@@ -46,7 +49,7 @@ function factsFor(node, measurer, fontSize) {
|
|
|
46
49
|
if (circle) {
|
|
47
50
|
throw new SourceError(`"${node.name}" padding-${side}: a circle has no sides to pad one of — use padding:`, node.line);
|
|
48
51
|
}
|
|
49
|
-
pad[side] = gapPixels(written, `"${node.name}" padding-${side}:`, node.line);
|
|
52
|
+
pad[side] = gapPixels(written, `"${node.name}" padding-${side}:`, node.line) + extra;
|
|
50
53
|
}
|
|
51
54
|
// A subprocess's bars stand inside its sides, so its text keeps its padding clear of them.
|
|
52
55
|
if (node.body.kind === 'shape' && node.body.outline === 'flow-subprocess') {
|
|
@@ -486,6 +489,7 @@ function buildMarks(named, placed, styles, texts = new Map()) {
|
|
|
486
489
|
else if (text) {
|
|
487
490
|
box = pointTextBox(at.x, at.y, text);
|
|
488
491
|
}
|
|
492
|
+
const borderWidth = borderWidthOf(`region "${name}"`, appearance['border.thickness'], stmt.line);
|
|
489
493
|
marks.push({
|
|
490
494
|
kind: stmt.kind,
|
|
491
495
|
name,
|
|
@@ -493,6 +497,7 @@ function buildMarks(named, placed, styles, texts = new Map()) {
|
|
|
493
497
|
names,
|
|
494
498
|
appearance,
|
|
495
499
|
guide: stmt.guide === true,
|
|
500
|
+
...(borderWidth !== BORDER_WIDTH ? { borderWidth } : {}),
|
|
496
501
|
...(text && box ? { text: { lines: text.lines, textAttrs: text.textAttrs, size: text.size, lineHeight: text.lineHeight, box } } : {}),
|
|
497
502
|
line: stmt.line,
|
|
498
503
|
});
|
|
@@ -771,6 +776,7 @@ function buildTree(statements, styles, defaults, icons) {
|
|
|
771
776
|
attrs: stmt.attrs,
|
|
772
777
|
appearance,
|
|
773
778
|
placements: stmt.placements,
|
|
779
|
+
...(stmt.limits ? { limits: stmt.limits } : {}),
|
|
774
780
|
...(stmt.guide ? { guide: true } : {}),
|
|
775
781
|
line: stmt.line,
|
|
776
782
|
};
|
|
@@ -780,6 +786,10 @@ function buildTree(statements, styles, defaults, icons) {
|
|
|
780
786
|
// the renderer does for its colors; its own words beat both.
|
|
781
787
|
const guideLook = stmt.guide ? defaults.get('guide node')?.['corners'] : undefined;
|
|
782
788
|
node.corners = cornersOf(node, stmt.attrs['corners'] ?? guideLook ?? appearance['corners'], stmt.attrs['corners'] !== undefined);
|
|
789
|
+
const guideBorder = stmt.guide ? defaults.get('guide node')?.['border.thickness'] : undefined;
|
|
790
|
+
const borderWidth = borderWidthOf(`"${node.name}"`, stmt.attrs['border.thickness'] ?? guideBorder ?? appearance['border.thickness'], stmt.line);
|
|
791
|
+
if (borderWidth !== BORDER_WIDTH)
|
|
792
|
+
node.borderWidth = borderWidth;
|
|
783
793
|
const cut = stmt.name.lastIndexOf('.');
|
|
784
794
|
if (cut === -1) {
|
|
785
795
|
roots.push(node);
|
|
@@ -1096,7 +1106,7 @@ function checkAttrs(kind, name, attrs, line) {
|
|
|
1096
1106
|
// Quoted back the way it was written. A bracketed value arrives one dotted
|
|
1097
1107
|
// key at a time, and `contents: match` is not a line anybody could look for.
|
|
1098
1108
|
// A deck's cards are stored joined on a line break; quote them back as written.
|
|
1099
|
-
const shown = key === 'cards' ? value.split('\n').map((text) => `"${text}"`).join(' ') : value;
|
|
1109
|
+
const shown = key === 'cards' ? value.split('\n').map((text) => `"${text}"`).join(' ') : key === 'title' ? `"${value}"` : value;
|
|
1100
1110
|
const wrote = written === key ? `${key}: ${shown}` : `${key}: (${written.slice(key.length + 1)}: ${shown})`;
|
|
1101
1111
|
if (!ALL_ATTR_KEYS.includes(key)) {
|
|
1102
1112
|
// Nothing anywhere in the language answers to this word, so the only
|
|
@@ -1143,6 +1153,23 @@ function checkAttrs(kind, name, attrs, line) {
|
|
|
1143
1153
|
* node drawn as a picture or with no body has no outline; `checkAttrs` has
|
|
1144
1154
|
* already refused the word written on one, and a style's is unused.
|
|
1145
1155
|
*/
|
|
1156
|
+
/**
|
|
1157
|
+
* A border's thickness in pixels: `thin`, `normal`, `thick`, a multiple of
|
|
1158
|
+
* normal, or `px`, read as a line's is (design.org 3.8).
|
|
1159
|
+
*/
|
|
1160
|
+
export function borderWidthOf(what, value, line) {
|
|
1161
|
+
if (value === undefined)
|
|
1162
|
+
return BORDER_WIDTH;
|
|
1163
|
+
const named = BORDER_THICKNESS[value];
|
|
1164
|
+
if (named !== undefined)
|
|
1165
|
+
return named;
|
|
1166
|
+
const number = /^(\d*\.?\d+)(px)?$/.exec(value);
|
|
1167
|
+
if (!number || Number(number[1]) <= 0) {
|
|
1168
|
+
throw new SourceError(`${what}: "border: (thickness: ${value})" — a thickness is thin, normal, thick, ` +
|
|
1169
|
+
'a multiple of normal greater than zero such as `thickness: 2`, or pixels such as `thickness: 3px`', line);
|
|
1170
|
+
}
|
|
1171
|
+
return number[2] ? Number(number[1]) : BORDER_WIDTH * Number(number[1]);
|
|
1172
|
+
}
|
|
1146
1173
|
function cornersOf(node, value, written) {
|
|
1147
1174
|
if (node.body.kind !== 'shape' || value === undefined)
|
|
1148
1175
|
return NODE_CORNERS[0];
|
package/dist/themes.d.ts
CHANGED
|
@@ -72,6 +72,7 @@ export declare function themeColor(value: string, theme: Theme): string;
|
|
|
72
72
|
* A hand-picked fill is left alone — the theme has no say over it.
|
|
73
73
|
*/
|
|
74
74
|
export declare function textOnFill(fill: string, theme: Theme): string | undefined;
|
|
75
|
+
export declare function luminance(hex: string): number;
|
|
75
76
|
/**
|
|
76
77
|
* Sampled out of `examples/reference/arch.png` rather than invented,
|
|
77
78
|
* so the benchmark render and the drawing it is measured against differ by
|
package/dist/themes.js
CHANGED
|
@@ -86,7 +86,7 @@ export function textOnFill(fill, theme) {
|
|
|
86
86
|
const apart = (color) => Math.abs(luminance(color) - under);
|
|
87
87
|
return apart(theme.text) >= apart(theme.background) ? theme.text : theme.background;
|
|
88
88
|
}
|
|
89
|
-
function luminance(hex) {
|
|
89
|
+
export function luminance(hex) {
|
|
90
90
|
const [r, g, b] = [1, 3, 5].map((at) => {
|
|
91
91
|
const c = parseInt(hex.slice(at, at + 2), 16) / 255;
|
|
92
92
|
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|