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/resolve.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { TEXT_GAP } from './linetexts.js';
|
|
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, BORDER_THICKNESS, BORDER_WIDTH, fontSizeFor, widestLine, } from './constants.js';
|
|
2
|
+
import { DESCRIPTION_NAME_KEY, LINK_NAMES_KEY, 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, BORDER_THICKNESS, BORDER_WIDTH, fontSizeFor, linesTall, widestLine, } from './constants.js';
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
5
5
|
import { bodyFor, iconNamed, pastedIcon } from './icons.js';
|
|
6
|
-
import { linksOf } from './links.js';
|
|
6
|
+
import { LINKS_KEY, linksOf } from './links.js';
|
|
7
|
+
import { unsafeLinks } from './markdown.js';
|
|
7
8
|
import { monospaceMeasurer } from './measure.js';
|
|
8
9
|
import { isBoxy, middleOf, sizeFor, SUBPROCESS_INSET } from './outline.js';
|
|
9
10
|
import { markupStyles, parseMarkup, plain, splitRuns, wrapLine } from './text.js';
|
|
@@ -20,7 +21,7 @@ function factsFor(node, measurer, fontSize) {
|
|
|
20
21
|
const lineHeight = measurer.lineHeight(textSize);
|
|
21
22
|
const hasText = node.lines.some((line) => plain(line).length > 0);
|
|
22
23
|
const across = hasText ? widestLine(node.lines, measurer, textSize) : 0;
|
|
23
|
-
const tall = hasText ? node.lines
|
|
24
|
+
const tall = hasText ? linesTall(node.lines, measurer, textSize) : 0;
|
|
24
25
|
// A turned text (design.org 7.2) takes the room of its turned ink: as wide
|
|
25
26
|
// as its lines are tall, as tall as its widest line.
|
|
26
27
|
const turned = node.textAttrs['reads'] !== undefined;
|
|
@@ -299,6 +300,10 @@ export function resolve(doc, options = {}) {
|
|
|
299
300
|
const measurer = options.measurer ?? monospaceMeasurer();
|
|
300
301
|
const fontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
|
|
301
302
|
const margin = options.margin ?? DEFAULT_MARGIN;
|
|
303
|
+
// `description: notes` becomes the text it names, and `description for …`
|
|
304
|
+
// waits until the boxes and lines it names exist. Nothing else reads them.
|
|
305
|
+
const described = nameDescriptions(doc.statements);
|
|
306
|
+
doc = { statements: nameLinks(described.statements) };
|
|
302
307
|
const styles = collectStyles(doc.statements);
|
|
303
308
|
checkStyleKeys(doc.statements);
|
|
304
309
|
const defaults = collectDefaults(doc.statements, styles);
|
|
@@ -315,7 +320,7 @@ export function resolve(doc, options = {}) {
|
|
|
315
320
|
}
|
|
316
321
|
}
|
|
317
322
|
}
|
|
318
|
-
const { nodes, byName, roots } = buildTree(writeOutCards(doc.statements), styles, defaults, icons);
|
|
323
|
+
const { nodes, byName, roots } = buildTree(writeOutCards(doc.statements), styles, defaults, icons, fontSize);
|
|
319
324
|
// From here on, `edge each child of a -> b` and `edge a -> b, c` are the lines they stand for.
|
|
320
325
|
const statements = expandEach(doc.statements, byName);
|
|
321
326
|
// Edges are resolved to nodes before anything is sized, because a labeled
|
|
@@ -323,8 +328,9 @@ export function resolve(doc, options = {}) {
|
|
|
323
328
|
// gaps are being worked out. Nothing here reads geometry.
|
|
324
329
|
const named = collectNamed(doc.statements, byName);
|
|
325
330
|
const edges = buildEdges(statements, byName, named, styles, defaults, measurer, fontSize);
|
|
331
|
+
attachDescriptions(described.attached, byName, edges);
|
|
326
332
|
const facts = new Map(nodes.map((node) => [node, factsFor(node, measurer, fontSize)]));
|
|
327
|
-
const markTexts = measureMarkTexts(named, measurer, fontSize);
|
|
333
|
+
const markTexts = measureMarkTexts(named, measurer, fontSize, styles);
|
|
328
334
|
const regionTexts = new Map([...markTexts].flatMap(([name, text]) => (text.region ? [[name, text.region]] : [])));
|
|
329
335
|
const placer = new Placer(roots, byName, facts, named, regionTexts);
|
|
330
336
|
placer.run();
|
|
@@ -433,18 +439,18 @@ function namesIn(target, named, seen = new Set()) {
|
|
|
433
439
|
* an inside text grows its region (design.org 7.4). Set small, as an aside is:
|
|
434
440
|
* it labels a spot or an area rather than being a thing in the diagram.
|
|
435
441
|
*/
|
|
436
|
-
function measureMarkTexts(named, measurer, fontSize) {
|
|
442
|
+
function measureMarkTexts(named, measurer, fontSize, styles) {
|
|
437
443
|
const measured = new Map();
|
|
438
444
|
for (const [name, stmt] of named) {
|
|
439
445
|
if (!stmt.text)
|
|
440
446
|
continue;
|
|
441
447
|
const { text, textAttrs } = stmt.text;
|
|
442
448
|
const subject = `${stmt.kind} "${name}"`;
|
|
443
|
-
const lines = linesFor(text, textAttrs, subject, stmt.line);
|
|
444
449
|
const size = fontSizeFor('none', textAttrs, fontSize, stmt.line);
|
|
450
|
+
const lines = linesFor(text, textAttrs, subject, stmt.line, { styles, fontSize, base: size });
|
|
445
451
|
const lineHeight = measurer.lineHeight(size);
|
|
446
452
|
const across = widestLine(lines, measurer, size);
|
|
447
|
-
const tall = lines
|
|
453
|
+
const tall = linesTall(lines, measurer, size);
|
|
448
454
|
const turned = textAttrs['reads'] !== undefined;
|
|
449
455
|
const width = turned ? tall : across;
|
|
450
456
|
const height = turned ? across : tall;
|
|
@@ -598,6 +604,154 @@ function collectStyles(statements) {
|
|
|
598
604
|
return styles;
|
|
599
605
|
}
|
|
600
606
|
/** The file's own icons, by name, each pasted in or read from its file. */
|
|
607
|
+
/**
|
|
608
|
+
* Swaps each `description: notes` for the text `description notes` declares,
|
|
609
|
+
* and sets aside the `description for …` statements, which name boxes and
|
|
610
|
+
* lines that don't exist until the tree is built. Every description's links
|
|
611
|
+
* are checked here, inline ones included.
|
|
612
|
+
*/
|
|
613
|
+
function nameDescriptions(statements) {
|
|
614
|
+
const declared = new Map();
|
|
615
|
+
const attached = [];
|
|
616
|
+
const checkLinks = (text, line) => {
|
|
617
|
+
const bad = unsafeLinks(text)[0];
|
|
618
|
+
if (bad !== undefined) {
|
|
619
|
+
throw new SourceError(`a description links to "${bad}" — a link goes to a web or mail address (https:, http:, mailto:) or a path`, line);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
for (const stmt of statements) {
|
|
623
|
+
if (stmt.kind !== 'description')
|
|
624
|
+
continue;
|
|
625
|
+
checkLinks(stmt.text, stmt.line);
|
|
626
|
+
if (stmt.targets !== undefined) {
|
|
627
|
+
attached.push(stmt);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const before = declared.get(stmt.name);
|
|
631
|
+
if (before !== undefined) {
|
|
632
|
+
throw new SourceError(`description "${stmt.name}" is declared twice, on lines ${before.line} and ${stmt.line} — keep one`, stmt.line);
|
|
633
|
+
}
|
|
634
|
+
declared.set(stmt.name, stmt);
|
|
635
|
+
}
|
|
636
|
+
const rest = statements.flatMap((stmt) => {
|
|
637
|
+
if (stmt.kind === 'description')
|
|
638
|
+
return [];
|
|
639
|
+
if (!('attrs' in stmt) || stmt.attrs === undefined)
|
|
640
|
+
return [stmt];
|
|
641
|
+
if (stmt.attrs['description'] !== undefined)
|
|
642
|
+
checkLinks(stmt.attrs['description'], stmt.line);
|
|
643
|
+
const name = stmt.attrs[DESCRIPTION_NAME_KEY];
|
|
644
|
+
if (name === undefined)
|
|
645
|
+
return [stmt];
|
|
646
|
+
const found = declared.get(name);
|
|
647
|
+
if (found === undefined) {
|
|
648
|
+
const known = [...declared.keys()];
|
|
649
|
+
throw new SourceError(`there is no description named "${name}" — ` +
|
|
650
|
+
(known.length === 0
|
|
651
|
+
? 'declare it with `description ' + name + ' """…"""`, or quote the text to write it here'
|
|
652
|
+
: `the file declares ${known.map((one) => `"${one}"`).join(', ')}; quote the text to write it here`), stmt.line);
|
|
653
|
+
}
|
|
654
|
+
const { [DESCRIPTION_NAME_KEY]: _, ...attrs } = stmt.attrs;
|
|
655
|
+
return [{ ...stmt, attrs: { ...attrs, description: found.text } }];
|
|
656
|
+
});
|
|
657
|
+
return { statements: rest, attached };
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Takes out the `url` statements and swaps each declared name in a `url:` for
|
|
661
|
+
* the links it declares, a bundle's in order, so nothing after this sees a name.
|
|
662
|
+
*/
|
|
663
|
+
function nameLinks(statements) {
|
|
664
|
+
const declared = new Map();
|
|
665
|
+
for (const stmt of statements) {
|
|
666
|
+
if (stmt.kind !== 'url')
|
|
667
|
+
continue;
|
|
668
|
+
const before = declared.get(stmt.name);
|
|
669
|
+
if (before !== undefined) {
|
|
670
|
+
throw new SourceError(`url "${stmt.name}" is declared twice, on lines ${before.line} and ${stmt.line} — keep one`, stmt.line);
|
|
671
|
+
}
|
|
672
|
+
declared.set(stmt.name, stmt);
|
|
673
|
+
}
|
|
674
|
+
const unknown = (name, line) => {
|
|
675
|
+
const known = [...declared.keys()];
|
|
676
|
+
return new SourceError(`there is no link named "${name}" — ` +
|
|
677
|
+
(known.length === 0
|
|
678
|
+
? 'declare it with `url ' + name + ' "https://…"`, or quote an address to write it here'
|
|
679
|
+
: `the file declares ${known.map((one) => `"${one}"`).join(', ')}; quote an address to write it here`), line);
|
|
680
|
+
};
|
|
681
|
+
const expanded = new Map();
|
|
682
|
+
const expand = (stmt, path) => {
|
|
683
|
+
const done = expanded.get(stmt.name);
|
|
684
|
+
if (done !== undefined)
|
|
685
|
+
return done;
|
|
686
|
+
if (path.includes(stmt.name)) {
|
|
687
|
+
const loop = [...path.slice(path.indexOf(stmt.name)), stmt.name];
|
|
688
|
+
throw new SourceError(`url "${stmt.name}" reaches itself (${loop.join(' → ')}), so it never says where it goes — take one of them out`, stmt.line);
|
|
689
|
+
}
|
|
690
|
+
const links = flatten(stmt.entries, `url "${stmt.name}"`, stmt.line, [...path, stmt.name]);
|
|
691
|
+
expanded.set(stmt.name, links);
|
|
692
|
+
return links;
|
|
693
|
+
};
|
|
694
|
+
const flatten = (entries, subject, line, path) => {
|
|
695
|
+
const links = [];
|
|
696
|
+
for (const entry of entries) {
|
|
697
|
+
let more;
|
|
698
|
+
if ('href' in entry)
|
|
699
|
+
more = [entry];
|
|
700
|
+
else {
|
|
701
|
+
const found = declared.get(entry.ref);
|
|
702
|
+
if (found === undefined)
|
|
703
|
+
throw unknown(entry.ref, line);
|
|
704
|
+
more = expand(found, path);
|
|
705
|
+
}
|
|
706
|
+
for (const link of more) {
|
|
707
|
+
if (links.some((had) => had.href === link.href)) {
|
|
708
|
+
const via = 'ref' in entry ? ` (through "${entry.ref}")` : '';
|
|
709
|
+
throw new SourceError(`${subject}: "${link.href}" is linked twice${via} — keep one`, line);
|
|
710
|
+
}
|
|
711
|
+
links.push(link);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return links;
|
|
715
|
+
};
|
|
716
|
+
for (const stmt of declared.values())
|
|
717
|
+
expand(stmt, []);
|
|
718
|
+
return statements.flatMap((stmt) => {
|
|
719
|
+
if (stmt.kind === 'url')
|
|
720
|
+
return [];
|
|
721
|
+
if (!('attrs' in stmt) || stmt.attrs === undefined)
|
|
722
|
+
return [stmt];
|
|
723
|
+
const listed = stmt.attrs[LINK_NAMES_KEY];
|
|
724
|
+
if (listed === undefined)
|
|
725
|
+
return [stmt];
|
|
726
|
+
const subject = stmt.kind === 'node' ? `node "${stmt.name}"` : stmt.kind;
|
|
727
|
+
const links = flatten(JSON.parse(listed), subject, stmt.line, []);
|
|
728
|
+
const { [LINK_NAMES_KEY]: _, ...attrs } = stmt.attrs;
|
|
729
|
+
attrs['url'] = links[0].href;
|
|
730
|
+
// As the parser does: the list is kept only when there is more than one bare address.
|
|
731
|
+
if (links.length > 1 || links.some((link) => link.name !== undefined || link.icon !== undefined))
|
|
732
|
+
attrs[LINKS_KEY] = JSON.stringify(links);
|
|
733
|
+
return [{ ...stmt, attrs }];
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
/** `description for proxy, cache`: each name a box, or a named line, that has no description yet. */
|
|
737
|
+
function attachDescriptions(attached, byName, edges) {
|
|
738
|
+
for (const stmt of attached) {
|
|
739
|
+
for (const target of stmt.targets) {
|
|
740
|
+
const node = byName.get(target);
|
|
741
|
+
const lines = node ? [] : edges.filter((edge) => edge.name === target);
|
|
742
|
+
const things = node ? [node] : lines;
|
|
743
|
+
if (things.length === 0) {
|
|
744
|
+
throw new SourceError(`description for "${target}": there is no box or named line called "${target}"`, stmt.line);
|
|
745
|
+
}
|
|
746
|
+
for (const thing of things) {
|
|
747
|
+
if (thing.attrs['description'] !== undefined) {
|
|
748
|
+
throw new SourceError(`description for "${target}": "${target}" already has a description — keep one`, stmt.line);
|
|
749
|
+
}
|
|
750
|
+
thing.attrs['description'] = stmt.text;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
601
755
|
function collectIcons(statements, readIconFile) {
|
|
602
756
|
const icons = new Map();
|
|
603
757
|
for (const stmt of statements) {
|
|
@@ -678,7 +832,7 @@ function collectDiagram(statements) {
|
|
|
678
832
|
}
|
|
679
833
|
return found ?? {};
|
|
680
834
|
}
|
|
681
|
-
function buildTree(statements, styles, defaults, icons) {
|
|
835
|
+
function buildTree(statements, styles, defaults, icons, fontSize) {
|
|
682
836
|
const nodes = [];
|
|
683
837
|
const byName = new Map();
|
|
684
838
|
const roots = [];
|
|
@@ -778,7 +932,11 @@ function buildTree(statements, styles, defaults, icons) {
|
|
|
778
932
|
body,
|
|
779
933
|
corners: 'rounded',
|
|
780
934
|
text,
|
|
781
|
-
lines: linesFor(text, textAttrs, `"${stmt.name}"`, stmt.line
|
|
935
|
+
lines: linesFor(text, textAttrs, `"${stmt.name}"`, stmt.line, {
|
|
936
|
+
styles,
|
|
937
|
+
fontSize,
|
|
938
|
+
base: fontSizeFor(kind, textAttrs, fontSize, stmt.line),
|
|
939
|
+
}),
|
|
782
940
|
children: [],
|
|
783
941
|
x: 0,
|
|
784
942
|
y: 0,
|
|
@@ -1258,6 +1416,10 @@ function checkStyleKeys(statements) {
|
|
|
1258
1416
|
if (topKey(key) === 'cards') {
|
|
1259
1417
|
throw new SourceError(`style "${stmt.name}" has cards:, which are one deck's own children, so it is written on that deck`, stmt.line);
|
|
1260
1418
|
}
|
|
1419
|
+
if (topKey(key) === 'description') {
|
|
1420
|
+
throw new SourceError(`style "${stmt.name}" has description:, which is about the things it describes rather than how they look — ` +
|
|
1421
|
+
'to give several one text, write `description for a, b and c """…"""`', stmt.line);
|
|
1422
|
+
}
|
|
1261
1423
|
if (topKey(key) === 'from' || topKey(key) === 'to') {
|
|
1262
1424
|
throw new SourceError(`style "${stmt.name}" has ${key}:, which says where one line ends, so it is written on that edge`, stmt.line);
|
|
1263
1425
|
}
|
|
@@ -1445,8 +1607,8 @@ function buildEdges(statements, byName, named, styles, defaults, measurer, fontS
|
|
|
1445
1607
|
// page selects a line by the same, so it is accepted.
|
|
1446
1608
|
const texts = stmt.texts.map(({ text, textAttrs: own, place }) => {
|
|
1447
1609
|
const textAttrs = { ...bracketOf('text', appearance), ...own };
|
|
1448
|
-
const lines = linesFor(text, textAttrs, `edge ${what}`, stmt.line);
|
|
1449
1610
|
const size = fontSizeFor('edge', textAttrs, fontSize, stmt.line);
|
|
1611
|
+
const lines = linesFor(text, textAttrs, `edge ${what}`, stmt.line, { styles, fontSize, base: size });
|
|
1450
1612
|
return {
|
|
1451
1613
|
text,
|
|
1452
1614
|
lines,
|
|
@@ -1454,7 +1616,7 @@ function buildEdges(statements, byName, named, styles, defaults, measurer, fontS
|
|
|
1454
1616
|
place,
|
|
1455
1617
|
size,
|
|
1456
1618
|
width: widestLine(lines, measurer, size),
|
|
1457
|
-
height: lines
|
|
1619
|
+
height: linesTall(lines, measurer, size),
|
|
1458
1620
|
};
|
|
1459
1621
|
});
|
|
1460
1622
|
edges.push({
|
|
@@ -1775,13 +1937,15 @@ function bracketOf(key, attrs) {
|
|
|
1775
1937
|
return found;
|
|
1776
1938
|
}
|
|
1777
1939
|
/**
|
|
1778
|
-
* Every style the markup in this file names
|
|
1940
|
+
* Every style the markup in this file names that lends a color, resolved to
|
|
1941
|
+
* that color. A style may lend a size instead or as well; the runs carry that
|
|
1942
|
+
* themselves, from `sizeRuns`.
|
|
1779
1943
|
*
|
|
1780
1944
|
* A style is the only thing markup may name — never a color — so that a marked
|
|
1781
1945
|
* word borrows a meaning the file already has instead of restating a value that
|
|
1782
1946
|
* goes stale the day the thing it means is recolored. Both ways that can fail
|
|
1783
|
-
* are refused by name: a style nobody declared, and one that
|
|
1784
|
-
*
|
|
1947
|
+
* are refused by name: a style nobody declared, and one that gives text neither
|
|
1948
|
+
* a color nor a size and so would lend nothing.
|
|
1785
1949
|
*/
|
|
1786
1950
|
function markupColors(nodes, edges, styles, marks = []) {
|
|
1787
1951
|
const colors = {};
|
|
@@ -1797,11 +1961,12 @@ function markupColors(nodes, edges, styles, marks = []) {
|
|
|
1797
1961
|
throw new SourceError(`${what}: its text marks [${name}], and there is no style called "${name}"`, line);
|
|
1798
1962
|
}
|
|
1799
1963
|
const color = style['text.color'];
|
|
1800
|
-
if (color === undefined) {
|
|
1801
|
-
throw new SourceError(`${what}: its text marks [${name}], and style "${name}"
|
|
1802
|
-
`write \`style ${name} text: (color: …)\``, line);
|
|
1964
|
+
if (color === undefined && style['text.size'] === undefined) {
|
|
1965
|
+
throw new SourceError(`${what}: its text marks [${name}], and style "${name}" gives text no color or size — ` +
|
|
1966
|
+
`write \`style ${name} text: (color: …)\` or \`text: (size: …)\``, line);
|
|
1803
1967
|
}
|
|
1804
|
-
|
|
1968
|
+
if (color !== undefined)
|
|
1969
|
+
colors[name] = color;
|
|
1805
1970
|
}
|
|
1806
1971
|
}
|
|
1807
1972
|
return colors;
|
|
@@ -1820,8 +1985,10 @@ function markupColors(nodes, edges, styles, marks = []) {
|
|
|
1820
1985
|
* what makes markup general where the `subtext:` slice it replaced could only
|
|
1821
1986
|
* ever reach the tail of a text.
|
|
1822
1987
|
*/
|
|
1823
|
-
function linesFor(text, textAttrs, subject, line) {
|
|
1988
|
+
function linesFor(text, textAttrs, subject, line, sizes) {
|
|
1824
1989
|
const lines = splitRuns(parseMarkup(text, subject, line));
|
|
1990
|
+
if (sizes)
|
|
1991
|
+
sizeRuns(lines, sizes, line);
|
|
1825
1992
|
const stated = textAttrs['wrap'];
|
|
1826
1993
|
if (stated === undefined)
|
|
1827
1994
|
return lines;
|
|
@@ -1829,7 +1996,25 @@ function linesFor(text, textAttrs, subject, line) {
|
|
|
1829
1996
|
if (!Number.isInteger(columns) || columns < 1) {
|
|
1830
1997
|
throw new SourceError(`wrap must be a whole number of characters, not "${stated}"`, line);
|
|
1831
1998
|
}
|
|
1832
|
-
return lines.flatMap((part) => wrapLine(part, columns));
|
|
1999
|
+
return lines.flatMap((part) => wrapLine(part, columns, sizes?.base));
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Give each marked run the size its style states (design.org 7.1). A run whose
|
|
2003
|
+
* style states no size, or the same size as the text it sits in, keeps none,
|
|
2004
|
+
* so a text marked only for color measures exactly as it always did. A style
|
|
2005
|
+
* that doesn't exist is left for `markupColors` to refuse by name.
|
|
2006
|
+
*/
|
|
2007
|
+
function sizeRuns(lines, sizes, line) {
|
|
2008
|
+
for (const run of lines.flat()) {
|
|
2009
|
+
if (run.style === undefined)
|
|
2010
|
+
continue;
|
|
2011
|
+
const stated = sizes.styles.get(run.style)?.['text.size'];
|
|
2012
|
+
if (stated === undefined)
|
|
2013
|
+
continue;
|
|
2014
|
+
const size = fontSizeFor('node', { size: stated }, sizes.fontSize, line);
|
|
2015
|
+
if (size !== sizes.base)
|
|
2016
|
+
run.size = size;
|
|
2017
|
+
}
|
|
1833
2018
|
}
|
|
1834
2019
|
function bounds(nodes, marks) {
|
|
1835
2020
|
let minX = Infinity;
|
package/dist/text.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/** A stretch of text drawn in one style. `style` is a style name from the file. */
|
|
2
2
|
export interface Run {
|
|
3
3
|
text: string;
|
|
4
|
-
/** The style whose text color this run borrows, or undefined for the node's own. */
|
|
4
|
+
/** The style whose text color and size this run borrows, or undefined for the node's own. */
|
|
5
5
|
style?: string;
|
|
6
|
+
/**
|
|
7
|
+
* The run's text size in pixels, when its style states one; undefined for
|
|
8
|
+
* the size of the text it sits in. Filled in by the resolver, which knows the
|
|
9
|
+
* styles and the document's size.
|
|
10
|
+
*/
|
|
11
|
+
size?: number;
|
|
6
12
|
}
|
|
7
13
|
/** One drawn line, as the runs it is made of. */
|
|
8
14
|
export type Line = Run[];
|
|
@@ -40,8 +46,14 @@ export declare function parseMarkup(text: string, subject: string, line: number)
|
|
|
40
46
|
* replaced could only reach the tail of a text.
|
|
41
47
|
*/
|
|
42
48
|
export declare function splitRuns(runs: Run[]): Line[];
|
|
43
|
-
/**
|
|
44
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Fold one line onto several at word boundaries, never exceeding `columns`.
|
|
51
|
+
*
|
|
52
|
+
* A column is a character of the text's own size (`base`). A marked run of
|
|
53
|
+
* another size counts each of its characters by the ratio of the two, so a
|
|
54
|
+
* small word takes less of the line than a normal one, as it does when drawn.
|
|
55
|
+
*/
|
|
56
|
+
export declare function wrapLine(line: Line, columns: number, base?: number): Line[];
|
|
45
57
|
/** The line as it reads, with no markup — what gets measured. */
|
|
46
58
|
export declare function plain(line: Line): string;
|
|
47
59
|
/** Every style name the markup in these lines refers to, without repeats. */
|
package/dist/text.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* A text and the runs inside it.
|
|
3
3
|
*
|
|
4
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
|
|
5
|
+
* `[dim]synced[/dim]` draws that word in the text color and size of `style dim`. The
|
|
6
6
|
* opener names a *style* and never a color, so the word borrows a meaning the
|
|
7
7
|
* file already has rather than restating a value that goes stale the day the
|
|
8
8
|
* thing it means is recolored.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* reaches a word in the middle of a line, which the slice never could.
|
|
15
15
|
*
|
|
16
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
|
|
17
|
+
* line is a list of runs, each with the style it borrows its color and size from, and
|
|
18
18
|
* line breaking and wrapping carry the runs along rather than re-parsing text
|
|
19
19
|
* that has already been read.
|
|
20
20
|
*/
|
|
@@ -142,20 +142,29 @@ function tidy(lines) {
|
|
|
142
142
|
}
|
|
143
143
|
return kept.length > 0 ? kept : [[{ text: '' }]];
|
|
144
144
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Fold one line onto several at word boundaries, never exceeding `columns`.
|
|
147
|
+
*
|
|
148
|
+
* A column is a character of the text's own size (`base`). A marked run of
|
|
149
|
+
* another size counts each of its characters by the ratio of the two, so a
|
|
150
|
+
* small word takes less of the line than a normal one, as it does when drawn.
|
|
151
|
+
*/
|
|
152
|
+
export function wrapLine(line, columns, base) {
|
|
147
153
|
const lines = [];
|
|
148
154
|
let current = [];
|
|
149
155
|
let length = 0;
|
|
156
|
+
const weight = (run) => (run.size === undefined || base === undefined ? 1 : run.size / base);
|
|
150
157
|
for (const word of words(line)) {
|
|
151
|
-
const
|
|
152
|
-
|
|
158
|
+
const scale = weight(word);
|
|
159
|
+
const space = length === 0 ? 0 : scale;
|
|
160
|
+
const wide = word.text.length * scale;
|
|
161
|
+
if (length > 0 && length + space + wide > columns + 1e-9) {
|
|
153
162
|
lines.push(current);
|
|
154
163
|
current = [];
|
|
155
164
|
length = 0;
|
|
156
165
|
}
|
|
157
166
|
append(current, length === 0 ? word : { ...word, text: ` ${word.text}` });
|
|
158
|
-
length += (length === 0 ? 0 :
|
|
167
|
+
length += (length === 0 ? 0 : scale) + wide;
|
|
159
168
|
}
|
|
160
169
|
if (current.length > 0)
|
|
161
170
|
lines.push(current);
|