@heroiclands/package-build 25.2.1 → 25.4.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/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/assets/styles/content-base.css +97 -116
- package/docs/api.md +30 -6
- package/docs/authoring/links-and-markup.md +214 -126
- package/docs/commands.md +2 -2
- package/docs/content-format.md +31 -6
- package/docs/diagnostics.md +27 -27
- package/docs/guides/writing-in-invented-scripts.md +89 -83
- package/docs/reference/format-details.md +6 -7
- package/docs/reference/note-types.md +15 -15
- package/docs/reference/theme-pages.md +38 -57
- package/engine/anchors.mjs +14 -0
- package/engine/code-fences.mjs +15 -6
- package/engine/content-blocks.mjs +7 -4
- package/engine/content-figures.mjs +19 -5
- package/engine/content-footnotes.mjs +60 -1
- package/engine/content-index.mjs +1 -1
- package/engine/content-render.mjs +66 -48
- package/engine/content-scripts.mjs +357 -101
- package/engine/content-spans.mjs +76 -10
- package/engine/content-verse.mjs +687 -358
- package/engine/first-use.mjs +43 -12
- package/engine/heading-attributes.mjs +2 -1
- package/engine/journals.mjs +8 -1
- package/engine/markdown-blocks.mjs +3 -2
- package/engine/markup-validation.mjs +17 -0
- package/engine/name-markup.mjs +1 -0
- package/engine/note-vocabulary.mjs +2 -2
- package/engine/pdf-render.mjs +89 -130
- package/engine/prose-lint.mjs +14 -2
- package/engine/roman-verse.mjs +68 -57
- package/engine/run-rows.mjs +1 -1
- package/engine/sql-tables.mjs +1 -1
- package/engine/verse-format.mjs +77 -0
- package/engine/verse-metre.mjs +49 -47
- package/engine/verse-regions.mjs +181 -0
- package/engine/wikilink-pattern.mjs +31 -0
- package/engine/wikilink-syntax.mjs +2 -16
- package/package.json +1 -1
- package/site-theme/static/css/style.css +68 -96
- package/types/engine/code-fences.d.mts +1 -3
- package/types/engine/content-blocks.d.mts +3 -1
- package/types/engine/content-footnotes.d.mts +23 -0
- package/types/engine/content-scripts.d.mts +12 -7
- package/types/engine/content-spans.d.mts +3 -1
- package/types/engine/content-verse.d.mts +263 -82
- package/types/engine/name-markup.d.mts +5 -0
- package/types/engine/roman-verse.d.mts +7 -6
- package/types/engine/verse-format.d.mts +9 -0
- package/types/engine/verse-metre.d.mts +4 -4
- package/types/engine/verse-regions.d.mts +78 -0
- package/types/engine/wikilink-pattern.d.mts +17 -0
- package/types/engine/wikilink-syntax.d.mts +1 -41
package/engine/first-use.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import remarkFrontmatter from "remark-frontmatter";
|
|
|
45
45
|
import remarkGfm from "remark-gfm";
|
|
46
46
|
import MarkdownIt from "markdown-it";
|
|
47
47
|
import { readQualifier } from "./address.mjs";
|
|
48
|
+
import { verseRegions } from "./verse-regions.mjs";
|
|
48
49
|
import { scanBlocks } from "./content-blocks.mjs";
|
|
49
50
|
import { scanFigures } from "./content-figures.mjs";
|
|
50
51
|
import { RUN_KEYS } from "./content-scripts.mjs";
|
|
@@ -169,14 +170,37 @@ const overlaps = (values, value) => {
|
|
|
169
170
|
* Explicit inline occurrences, authored strong text and structural exclusions
|
|
170
171
|
* at original source offsets.
|
|
171
172
|
*/
|
|
172
|
-
function prose(source, capturedNames) {
|
|
173
|
+
function prose(source, capturedNames, capturedSpans) {
|
|
173
174
|
const links = [],
|
|
174
175
|
paragraphs = [],
|
|
175
176
|
regions = [],
|
|
176
177
|
excluded = [],
|
|
177
178
|
strong = [],
|
|
178
179
|
html = [];
|
|
179
|
-
const
|
|
180
|
+
const spans = capturedSpans ?? scanSpans(source);
|
|
181
|
+
const verseMasks = verseRegions(source).map((region) => ({
|
|
182
|
+
start: region.from,
|
|
183
|
+
end: region.to,
|
|
184
|
+
block: true,
|
|
185
|
+
}));
|
|
186
|
+
for (const span of spans.spans)
|
|
187
|
+
if (
|
|
188
|
+
span.verseModel ||
|
|
189
|
+
span.attrs.some(
|
|
190
|
+
([key, value]) => key === "class" && value.split(/\s+/).includes("verse"),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
verseMasks.push({ start: span.start, end: span.end, block: false });
|
|
194
|
+
excluded.push(...verseMasks);
|
|
195
|
+
const verseRanges = verseMasks
|
|
196
|
+
.sort((a, b) => a.start - b.start || b.end - a.end)
|
|
197
|
+
.filter(
|
|
198
|
+
(range, index, all) =>
|
|
199
|
+
!all
|
|
200
|
+
.slice(0, index)
|
|
201
|
+
.some((parent) => parent.start <= range.start && parent.end >= range.end),
|
|
202
|
+
);
|
|
203
|
+
let masked = source
|
|
180
204
|
.replace(/!\[\[[^\]\n]+\]\]|\{\{[^}\n]+\}\}/g, (text, start) => {
|
|
181
205
|
excluded.push({ start, end: start + text.length });
|
|
182
206
|
return blank(text);
|
|
@@ -193,6 +217,11 @@ function prose(source, capturedNames) {
|
|
|
193
217
|
});
|
|
194
218
|
return "x".repeat(text.length);
|
|
195
219
|
});
|
|
220
|
+
for (const range of [...verseRanges].reverse())
|
|
221
|
+
masked =
|
|
222
|
+
masked.slice(0, range.start) +
|
|
223
|
+
masked.slice(range.start, range.end).replace(/[^\r\n]/g, range.block ? " " : "x") +
|
|
224
|
+
masked.slice(range.end);
|
|
196
225
|
const tree = parser.parse(masked);
|
|
197
226
|
const env = { references: Object.create(null) };
|
|
198
227
|
const inline = (node) => {
|
|
@@ -285,7 +314,7 @@ function prose(source, capturedNames) {
|
|
|
285
314
|
});
|
|
286
315
|
const authoredLinks = [...new Map(actualLinks.map((link) => [link.start, link])).values()];
|
|
287
316
|
const linked = ranges(authoredLinks);
|
|
288
|
-
const marked = (capturedNames ??
|
|
317
|
+
const marked = (capturedNames ?? spans.names)
|
|
289
318
|
.filter((span) => eligible(span) && !contains(linked, span))
|
|
290
319
|
.map((span) => ({
|
|
291
320
|
...span,
|
|
@@ -373,7 +402,7 @@ export function nameUses(markdown, vocabulary) {
|
|
|
373
402
|
// Every use carries the class, so a body without it is read no further.
|
|
374
403
|
if (!source.includes(".name")) return [];
|
|
375
404
|
const spans = scanSpans(source);
|
|
376
|
-
const { marked, links, strong, inParagraph } = prose(source, spans.names);
|
|
405
|
+
const { marked, links, strong, inParagraph } = prose(source, spans.names, spans);
|
|
377
406
|
const starts = [0];
|
|
378
407
|
for (let i = 0; i < source.length; i++) if (source[i] === "\n") starts.push(i + 1);
|
|
379
408
|
// A caption and a named block's opening and closing lines are not prose.
|
|
@@ -527,14 +556,16 @@ export function createFirstUseChecker({
|
|
|
527
556
|
const { recognize } = catalog;
|
|
528
557
|
return ({ file, raw }) => {
|
|
529
558
|
const authored = parsed?.get(file);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
|
|
559
|
+
let captured;
|
|
560
|
+
if (authored?.raw === raw && Number.isInteger(authored.bodyLine)) {
|
|
561
|
+
const inventory = syntax ? syntax(authored.body).spans : scanSpans(authored.body);
|
|
562
|
+
const context = { raw, bodyLine: authored.bodyLine, bodyColumn: authored.bodyColumn };
|
|
563
|
+
captured = {
|
|
564
|
+
names: rebaseNameSpans(inventory.names, context),
|
|
565
|
+
spans: rebaseNameSpans(inventory.spans, context),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const { marked } = prose(raw, captured?.names, captured);
|
|
538
569
|
const findings = new Map();
|
|
539
570
|
const add = (offset, rule, message) => {
|
|
540
571
|
if (!findings.has(offset)) findings.set(offset, { rules: [], messages: [] });
|
|
@@ -40,7 +40,8 @@ import { blockTokens } from "./markdown-blocks.mjs";
|
|
|
40
40
|
function literalHeadingLines(source) {
|
|
41
41
|
const lines = new Set();
|
|
42
42
|
for (const token of blockTokens(source)) {
|
|
43
|
-
if (!token.map || !["fence", "code_block", "html_block"].includes(token.type))
|
|
43
|
+
if (!token.map || !["fence", "code_block", "html_block", "verse_div"].includes(token.type))
|
|
44
|
+
continue;
|
|
44
45
|
for (let line = token.map[0]; line < token.map[1]; line++) lines.add(line);
|
|
45
46
|
}
|
|
46
47
|
return lines;
|
package/engine/journals.mjs
CHANGED
|
@@ -76,6 +76,7 @@ import { journalHasContent } from "./note-state.mjs";
|
|
|
76
76
|
import { hasLexicalEntries } from "./lexical-sections.mjs";
|
|
77
77
|
import { draftNoticeFor } from "./draft-notice.mjs";
|
|
78
78
|
import { scanFigures } from "./content-figures.mjs";
|
|
79
|
+
import { verseRegions } from "./verse-regions.mjs";
|
|
79
80
|
import { separateFootnotes } from "./content-footnotes.mjs";
|
|
80
81
|
import { WITHHELD_CLASS, pageOpenings } from "./heading-attributes.mjs";
|
|
81
82
|
import { imagesIn } from "./content-images.mjs";
|
|
@@ -154,6 +155,12 @@ export function splitPages(body, leadName = "Introduction", resolveRole, syntax)
|
|
|
154
155
|
scanFigures(markdown, { resolveRole }).figures.map((figure) => [figure.line - 1, figure]),
|
|
155
156
|
);
|
|
156
157
|
const blockLines = namedBlockLines(markdown, syntax);
|
|
158
|
+
const verseLines = new Set();
|
|
159
|
+
for (const region of verseRegions(markdown))
|
|
160
|
+
for (let line = region.start; line <= region.end; line++) {
|
|
161
|
+
blockLines.add(line);
|
|
162
|
+
verseLines.add(line);
|
|
163
|
+
}
|
|
157
164
|
const openings = pageOpenings(markdown, blockLines);
|
|
158
165
|
const pages = [];
|
|
159
166
|
const beforeFirstH1 = [];
|
|
@@ -189,7 +196,7 @@ export function splitPages(body, leadName = "Introduction", resolveRole, syntax)
|
|
|
189
196
|
// carry a shorter fence of its own as literal content, same as
|
|
190
197
|
// {@link module:engine/content-blocks.scanBlocks} and
|
|
191
198
|
// {@link module:engine/content-figures.scanFigures} track it.
|
|
192
|
-
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
199
|
+
const fence = verseLines.has(lineIndex) ? null : /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
193
200
|
if (codeFence) {
|
|
194
201
|
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length) {
|
|
195
202
|
codeFence = null;
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import MarkdownIt from "markdown-it";
|
|
28
|
+
import { verseDivPlugin } from "./verse-regions.mjs";
|
|
28
29
|
|
|
29
30
|
/** The renderer's block grammar, with inline parsing and its passes switched off. */
|
|
30
|
-
const parser = new MarkdownIt({ html: true });
|
|
31
|
+
const parser = new MarkdownIt({ html: true }).use(verseDivPlugin);
|
|
31
32
|
parser.core.ruler.disable(["inline", "linkify", "replacements", "smartquotes", "text_join"]);
|
|
32
33
|
|
|
33
34
|
/** How many recent sources keep their tokens. */
|
|
@@ -51,7 +52,7 @@ export function blockTokens(source) {
|
|
|
51
52
|
recent.set(text, hit);
|
|
52
53
|
return hit;
|
|
53
54
|
}
|
|
54
|
-
const tokens = parser.parse(text, {});
|
|
55
|
+
const tokens = parser.parse(text, { verseSource: text });
|
|
55
56
|
recent.set(text, tokens);
|
|
56
57
|
if (recent.size > RECENT) recent.delete(recent.keys().next().value);
|
|
57
58
|
return tokens;
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import { scanBlocks } from "./content-blocks.mjs";
|
|
15
15
|
import { scanAlerts } from "./content-alerts.mjs";
|
|
16
16
|
import { scanRuns } from "./content-scripts.mjs";
|
|
17
|
+
import { scanSpans } from "./content-spans.mjs";
|
|
18
|
+
import { verseRegions } from "./verse-regions.mjs";
|
|
17
19
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
18
20
|
|
|
19
21
|
/** The opening of a shortcode: `{{<` or `{{%`. */
|
|
@@ -55,9 +57,24 @@ export function markupFindings(source, file, syntax, runs) {
|
|
|
55
57
|
);
|
|
56
58
|
const scanned = syntax?.(body);
|
|
57
59
|
const marked = runs ? scanRuns(body, runs, syntax) : undefined;
|
|
60
|
+
const spans = scanned?.spans ?? scanSpans(body);
|
|
61
|
+
const verseRanges = [
|
|
62
|
+
...verseRegions(body).map((region) => [region.from, region.to]),
|
|
63
|
+
...spans.spans.filter((span) => span.verseModel).map((span) => [span.start, span.end]),
|
|
64
|
+
];
|
|
65
|
+
const starts = [0];
|
|
66
|
+
for (const match of body.matchAll(/\n/g)) starts.push(match.index + 1);
|
|
67
|
+
const verseErrors = spans.errors.filter((error) => {
|
|
68
|
+
const offset = (starts[error.line - 1] ?? 0) + error.column - 1;
|
|
69
|
+
return (
|
|
70
|
+
error.message.includes(".verse") ||
|
|
71
|
+
verseRanges.some(([from, to]) => offset >= from && offset < to)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
58
74
|
const errors = [
|
|
59
75
|
...(scanned?.blocks ?? scanBlocks(body)).errors,
|
|
60
76
|
...(scanned?.alerts ?? scanAlerts(body)).errors,
|
|
77
|
+
...verseErrors,
|
|
61
78
|
...(marked?.errors ?? []),
|
|
62
79
|
...shortcodeErrors(body),
|
|
63
80
|
].map((error) => ({
|
package/engine/name-markup.mjs
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* @property {string} visible - Visible text from the actual parsed children.
|
|
43
43
|
* @property {ReadonlyArray<readonly [string, string]>} attrs - Retained permitted HTML attributes.
|
|
44
44
|
* @property {Record<string, SpanAttributePosition>} attributePositions - Authored key/value sites by retained attribute name.
|
|
45
|
+
* @property {ReturnType<typeof import("./content-verse.mjs").parseVerse>} [verseModel] - Logical model of an inline verse quotation.
|
|
45
46
|
*/
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -2338,7 +2338,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2338
2338
|
...TEXT,
|
|
2339
2339
|
required: true,
|
|
2340
2340
|
describe:
|
|
2341
|
-
"What a verse
|
|
2341
|
+
"What a verse div's `scan=` names the metre by: lowercase " +
|
|
2342
2342
|
"letters, digits and `-`, unique in the package.",
|
|
2343
2343
|
},
|
|
2344
2344
|
{
|
|
@@ -2362,7 +2362,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2362
2362
|
]),
|
|
2363
2363
|
}),
|
|
2364
2364
|
describe:
|
|
2365
|
-
"The metres a verse
|
|
2365
|
+
"The metres a verse div's `scan=` names. A div that states one is " +
|
|
2366
2366
|
"warned at each line that cannot have its beats. See " +
|
|
2367
2367
|
"[metres](../authoring/links-and-markup.md#metre).",
|
|
2368
2368
|
},
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -86,13 +86,7 @@ import { scanSpans } from "./content-spans.mjs";
|
|
|
86
86
|
import { GENERIC_FONTS, renderRuns, scanRuns } from "./content-scripts.mjs";
|
|
87
87
|
import { scanFigures } from "./content-figures.mjs";
|
|
88
88
|
import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
|
|
89
|
-
import {
|
|
90
|
-
hasColumns,
|
|
91
|
-
lineNumber,
|
|
92
|
-
rightToLeft,
|
|
93
|
-
scanVerse,
|
|
94
|
-
verseColumnCount,
|
|
95
|
-
} from "./content-verse.mjs";
|
|
89
|
+
import { rightToLeft } from "./content-verse.mjs";
|
|
96
90
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
97
91
|
import { panelRoman, scanScrolls, scrollPanels, scrollParagraphs } from "./content-scroll.mjs";
|
|
98
92
|
import { BOOK_INK, BOOK_RUBRIC, fontOutlines, scrollPanelSvg } from "./font-outlines.mjs";
|
|
@@ -505,7 +499,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
505
499
|
const given = opts.nested ? String(markdown ?? "") : stripHtmlComments(markdown);
|
|
506
500
|
const separated = opts.footnoteDefinitions === undefined ? separateFootnotes(given) : null;
|
|
507
501
|
const authored = separated?.markdown ?? given;
|
|
508
|
-
|
|
502
|
+
let definitions = opts.footnoteDefinitions ?? separated?.definitions ?? "";
|
|
509
503
|
const footnoteState = opts.footnoteState ?? { seen: new Set() };
|
|
510
504
|
const sharedOptions = {
|
|
511
505
|
...opts,
|
|
@@ -519,10 +513,19 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
519
513
|
// Text marked with a language, a font or a map is rewritten once, ahead of
|
|
520
514
|
// every other pass, line for line; the nested calls below read the result,
|
|
521
515
|
// and the scroll fences' layouts the rewrite returns.
|
|
516
|
+
const markingSource = definitions ? `${authored}\n\n${definitions}` : authored;
|
|
522
517
|
const marked =
|
|
523
|
-
opts.nested ? undefined :
|
|
524
|
-
|
|
525
|
-
|
|
518
|
+
opts.nested ? undefined : (
|
|
519
|
+
renderRuns(markingSource, opts.runs, undefined, { target: "book" })
|
|
520
|
+
);
|
|
521
|
+
const markedFootnotes = marked && definitions ? separateFootnotes(marked.markdown) : undefined;
|
|
522
|
+
const source = markedFootnotes?.markdown ?? marked?.markdown ?? authored;
|
|
523
|
+
if (markedFootnotes) definitions = markedFootnotes.definitions;
|
|
524
|
+
if (marked) {
|
|
525
|
+
sharedOptions.footnoteDefinitions = definitions;
|
|
526
|
+
sharedOptions.scrolls = marked.scrolls;
|
|
527
|
+
sharedOptions.verses = marked.verses;
|
|
528
|
+
}
|
|
526
529
|
// One map for the whole body, not one per block: a heading inside a
|
|
527
530
|
// blockquote or a list item shares the entry's anchor namespace with every
|
|
528
531
|
// other heading in the same body, because `sectionLabel` scopes by entry
|
|
@@ -543,6 +546,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
543
546
|
footnoteState,
|
|
544
547
|
footnotePrefix: opts.footnotePrefix ?? `footnote-${slugify(anchorPrefix || "body")}`,
|
|
545
548
|
scrolls: marked?.scrolls ?? opts.scrolls ?? new Map(),
|
|
549
|
+
verses: marked?.verses ?? opts.verses ?? new Map(),
|
|
546
550
|
};
|
|
547
551
|
// A withheld section is set in the box a `:::secret` is set in, and its
|
|
548
552
|
// heading is set inside it: a printed page honours no reader permission, so
|
|
@@ -691,7 +695,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
691
695
|
/** Render one fragment with the note's footnote definitions available. */
|
|
692
696
|
function renderMarkdownSegment(source, md, ctx, definitions) {
|
|
693
697
|
const input = definitions ? `${source}\n\n${definitions}` : source;
|
|
694
|
-
const tokens = md.parse(input, {});
|
|
698
|
+
const tokens = md.parse(input, { verses: ctx.verses });
|
|
695
699
|
const footnoteStart = tokens.findIndex((token) => token.type === "footnote_block_open");
|
|
696
700
|
const visible = footnoteStart < 0 ? tokens : tokens.slice(0, footnoteStart);
|
|
697
701
|
const footnotes = new Map();
|
|
@@ -777,37 +781,14 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
777
781
|
out.push(scrollBlock(token, ctx));
|
|
778
782
|
return 1;
|
|
779
783
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
const poem = scanned.blocks[0];
|
|
790
|
-
const step = Number(poem?.attributes.numstep ?? 0) || 0;
|
|
791
|
-
const content =
|
|
792
|
-
hasColumns(poem) ? verseGrid(token, poem, step, ctx)
|
|
793
|
-
: step ? numberedVerse(token, step, ctx)
|
|
794
|
-
: token.meta.verseStanzas
|
|
795
|
-
.map((stanza) =>
|
|
796
|
-
stanza[0]?.stanzaBreak ?
|
|
797
|
-
"#v(0.5em)"
|
|
798
|
-
: stanza
|
|
799
|
-
.map(({ children, level }) => {
|
|
800
|
-
const verse = renderInline({ children }, ctx);
|
|
801
|
-
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
802
|
-
})
|
|
803
|
-
.join(" \\\n"),
|
|
804
|
-
)
|
|
805
|
-
.join("\n\n");
|
|
806
|
-
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
807
|
-
out.push(
|
|
808
|
-
`\n#block(width: 100%)[${scriptWrapped(content, poem?.classes, poem?.attributes)}]${anchor}\n`,
|
|
809
|
-
);
|
|
810
|
-
} else out.push(rawBlock(token.content, token.info?.trim() || ""));
|
|
784
|
+
out.push(rawBlock(token.content, token.info?.trim() || ""));
|
|
785
|
+
return 1;
|
|
786
|
+
}
|
|
787
|
+
case "verse_div": {
|
|
788
|
+
const poem = token.meta.verse;
|
|
789
|
+
const content = workVerse(token, poem, ctx);
|
|
790
|
+
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
791
|
+
out.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
|
|
811
792
|
return 1;
|
|
812
793
|
}
|
|
813
794
|
case "hr":
|
|
@@ -1300,90 +1281,58 @@ function typstLanguage(tag) {
|
|
|
1300
1281
|
/** The classes that set a run in a font: a face's run carries `script`. */
|
|
1301
1282
|
const FONT_CLASSES = Object.freeze(["script", ...Object.values(GENERIC_FONTS)]);
|
|
1302
1283
|
|
|
1303
|
-
/**
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
function verseGrid(token, poem, step, ctx) {
|
|
1349
|
-
const count = verseColumnCount(poem);
|
|
1350
|
-
const width = count + (step ? 1 : 0);
|
|
1351
|
-
const columns = [...(step ? ["2.5em"] : []), ...Array(count).fill("1fr")];
|
|
1352
|
-
const rows = [];
|
|
1353
|
-
let row = 0;
|
|
1354
|
-
for (const group of token.meta.verseGroups) {
|
|
1355
|
-
if (group.stanzaBreak) {
|
|
1356
|
-
rows.push(` grid.cell(colspan: ${width})[#v(0.5em)],`);
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
const cells = Array.from({ length: count }, () => []);
|
|
1360
|
-
for (const part of group.parts) {
|
|
1361
|
-
const rtl = rightToLeft(part.text);
|
|
1362
|
-
const secondary = cells[part.column - 1].length > 0;
|
|
1363
|
-
const text = renderInline(
|
|
1364
|
-
{ children: part.children },
|
|
1365
|
-
secondary ? { ...ctx, textFill: "luma(45%)" } : ctx,
|
|
1284
|
+
/** The logical lines of one stanza, with a shared label and paired scripts. */
|
|
1285
|
+
function workVerse(token, poem, ctx) {
|
|
1286
|
+
const paired = token.meta.versePaired;
|
|
1287
|
+
const labelled = token.type !== "verse_inline" && Boolean(poem?.attributes.work);
|
|
1288
|
+
const nativeClasses = paired?.classes ?? poem?.classes;
|
|
1289
|
+
const nativeAttributes = paired?.attributes ?? poem?.attributes;
|
|
1290
|
+
const body = (token.meta.verseGroups ?? [])
|
|
1291
|
+
.map((group) => {
|
|
1292
|
+
if (group.cantoBreak) {
|
|
1293
|
+
const mark =
|
|
1294
|
+
group.children?.length ? renderInline({ children: group.children }, ctx) : "";
|
|
1295
|
+
const heading =
|
|
1296
|
+
group.titleChildren?.length ?
|
|
1297
|
+
`#heading(level: 3, outlined: false, bookmarked: false)[${renderInline({ children: group.titleChildren }, ctx)}]`
|
|
1298
|
+
: "";
|
|
1299
|
+
const anchor = group.id ? ` <${sectionLabel(ctx.anchorPrefix, group.id)}>` : "";
|
|
1300
|
+
return `${mark ? scriptWrapped(mark, nativeClasses, nativeAttributes) + "\n" : ""}#v(0.5em)${heading ? "\n" + heading : ""}${anchor}`;
|
|
1301
|
+
}
|
|
1302
|
+
const label = labelled ? `#text(size: 0.75em, fill: luma(45%))[${group.label}]` : "";
|
|
1303
|
+
const lineText = (line, roman = false) => {
|
|
1304
|
+
const text = renderInline(
|
|
1305
|
+
{ children: roman ? line.romanChildren : line.children },
|
|
1306
|
+
ctx,
|
|
1307
|
+
);
|
|
1308
|
+
const padded = `${line.level ? `#h(${line.level * 1.25}em)` : ""}${text}`;
|
|
1309
|
+
if (roman || !paired) return padded;
|
|
1310
|
+
const wrapped = scriptWrapped(padded, nativeClasses, nativeAttributes);
|
|
1311
|
+
return rightToLeft(line.text) ? `#set text(dir: rtl)\n${wrapped}` : wrapped;
|
|
1312
|
+
};
|
|
1313
|
+
if (!paired) {
|
|
1314
|
+
const lines = scriptWrapped(
|
|
1315
|
+
group.lines.map((line) => lineText(line)).join(" \\\n"),
|
|
1316
|
+
nativeClasses,
|
|
1317
|
+
nativeAttributes,
|
|
1318
|
+
);
|
|
1319
|
+
return labelled ?
|
|
1320
|
+
`#grid(columns: (2.5em, 1fr), column-gutter: 0.5em, [${label}], [${lines}])`
|
|
1321
|
+
: lines;
|
|
1322
|
+
}
|
|
1323
|
+
const cells = group.lines.map((line, index) =>
|
|
1324
|
+
[
|
|
1325
|
+
...(labelled ? [index === 0 ? `[${label}]` : "[]"] : []),
|
|
1326
|
+
`[${lineText(line)}]`,
|
|
1327
|
+
`[${lineText(line, true)}]`,
|
|
1328
|
+
].join(", "),
|
|
1366
1329
|
);
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
const rendered = cells.map((stacked) => {
|
|
1374
|
-
if (!stacked.length) return "[]";
|
|
1375
|
-
if (stacked.length === 1) return `[${stacked[0]}]`;
|
|
1376
|
-
return `[#stack(dir: ttb, spacing: 0.3em, ${stacked.map((part, k) => (k ? `[#text(fill: luma(45%))[${part}]]` : `[${part}]`)).join(", ")})]`;
|
|
1377
|
-
});
|
|
1378
|
-
const number = step ? [typstNumber(lineNumber(++row, step))] : [];
|
|
1379
|
-
rows.push(` ${[...number, ...rendered].join(", ")},`);
|
|
1380
|
-
}
|
|
1381
|
-
return [
|
|
1382
|
-
"#set text(dir: ltr)",
|
|
1383
|
-
`#grid(columns: (${columns.join(", ")}), column-gutter: 1.5em, row-gutter: 0.65em,`,
|
|
1384
|
-
...rows,
|
|
1385
|
-
")",
|
|
1386
|
-
].join("\n");
|
|
1330
|
+
const columns = labelled ? "2.5em, 1fr, 1fr" : "1fr, 1fr";
|
|
1331
|
+
return `#set text(dir: ltr)\n#grid(columns: (${columns}), column-gutter: 1.5em, row-gutter: 0.35em,\n ${cells.join(",\n ")}\n)`;
|
|
1332
|
+
})
|
|
1333
|
+
.join("\n\n");
|
|
1334
|
+
const notes = (token.meta.verseNotes ?? []).map((line) => renderInline(line, ctx)).join("");
|
|
1335
|
+
return body + notes;
|
|
1387
1336
|
}
|
|
1388
1337
|
|
|
1389
1338
|
/**
|
|
@@ -1430,6 +1379,12 @@ function renderInline(token, ctx) {
|
|
|
1430
1379
|
case "code_inline":
|
|
1431
1380
|
out.push(inlineRaw(child.content));
|
|
1432
1381
|
break;
|
|
1382
|
+
case "verse_inline": {
|
|
1383
|
+
const content = workVerse(child, child.meta.verse, ctx);
|
|
1384
|
+
const id = child.attrGet?.("id");
|
|
1385
|
+
out.push(`#box[${content}]${id ? ` <${sectionLabel(ctx.anchorPrefix, id)}>` : ""}`);
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1433
1388
|
case "span_open": {
|
|
1434
1389
|
if (["em", "strong"].includes(child.tag)) {
|
|
1435
1390
|
const classes = (child.attrGet?.("class") ?? "").split(/\s+/);
|
|
@@ -1719,8 +1674,7 @@ function pageOfLabel(label) {
|
|
|
1719
1674
|
* @returns {string} Typst markup.
|
|
1720
1675
|
*/
|
|
1721
1676
|
function renderLink(href, inner, ctx, { bracketed = false } = {}) {
|
|
1722
|
-
// A
|
|
1723
|
-
// link presentation, including any native spans or emphasis in that label.
|
|
1677
|
+
// A caller's explicit text fill applies inside the link's label.
|
|
1724
1678
|
if (ctx.textFill) inner = `#text(fill: ${ctx.textFill})[${inner}]`;
|
|
1725
1679
|
const url = String(href ?? "");
|
|
1726
1680
|
const fragment = /#([^?]*)/.exec(url)?.[1] ?? "";
|
|
@@ -2012,9 +1966,14 @@ export function bookTypstPreamble(runs = {}, families = {}) {
|
|
|
2012
1966
|
" }\n" +
|
|
2013
1967
|
" if lang != none { text(lang: lang, styled) } else { styled }\n" +
|
|
2014
1968
|
"}",
|
|
2015
|
-
//
|
|
1969
|
+
// Plain semantic hooks retain the span classes without choosing their appearance.
|
|
1970
|
+
"#let book-direction(body) = body",
|
|
1971
|
+
"#let book-speaker(body) = body",
|
|
2016
1972
|
"#let book-span(body, classes: (), lang: none) = {\n" +
|
|
2017
|
-
"
|
|
1973
|
+
" let styled = book-script-text(body, classes: classes, lang: lang)\n" +
|
|
1974
|
+
' if "direction" in classes { styled = book-direction(styled) }\n' +
|
|
1975
|
+
' if "speaker" in classes { styled = book-speaker(styled) }\n' +
|
|
1976
|
+
" box(styled)\n" +
|
|
2018
1977
|
"}",
|
|
2019
1978
|
// Use the file's natural measure for auto. Every requested width is
|
|
2020
1979
|
// bounded by the available measure and by the height of a page.
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import fs from "node:fs";
|
|
36
36
|
import path from "node:path";
|
|
37
37
|
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
38
|
+
import { protectVerseSource } from "./verse-format.mjs";
|
|
38
39
|
|
|
39
40
|
import {
|
|
40
41
|
MARKDOWNLINT_CONFIG,
|
|
@@ -161,6 +162,17 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
const format = async (input) => {
|
|
166
|
+
const protectedSource =
|
|
167
|
+
["markdown", "mdx"].includes(info.inferredParser) ?
|
|
168
|
+
protectVerseSource(input)
|
|
169
|
+
: undefined;
|
|
170
|
+
const output = await prettier.format(protectedSource?.markdown ?? input, options);
|
|
171
|
+
return formatNoteFrontmatter(
|
|
172
|
+
protectedSource ? protectedSource.restore(output) : output,
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
164
176
|
// A file Prettier cannot parse is a finding, not a crash. Its own CLI
|
|
165
177
|
// reports the syntax error and carries on to the next file, and a
|
|
166
178
|
// single unparseable file in a large tree must not cost the report on
|
|
@@ -172,7 +184,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
172
184
|
let formatted = source;
|
|
173
185
|
let converged = false;
|
|
174
186
|
for (let pass = 0; pass < MAX_FORMAT_PASSES; pass += 1) {
|
|
175
|
-
const next =
|
|
187
|
+
const next = await format(formatted);
|
|
176
188
|
if (next === formatted) {
|
|
177
189
|
converged = true;
|
|
178
190
|
break;
|
|
@@ -206,7 +218,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
206
218
|
continue;
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
if (
|
|
221
|
+
if ((await format(source)) !== source) {
|
|
210
222
|
findings.push({
|
|
211
223
|
file,
|
|
212
224
|
severity: "error",
|