@heroiclands/package-build 25.3.0 → 25.5.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 +39 -0
- package/README.md +1 -1
- package/assets/styles/content-base.css +32 -0
- package/docs/api.md +30 -6
- package/docs/authoring/links-and-markup.md +143 -53
- package/docs/commands.md +8 -2
- package/docs/content-format.md +14 -6
- package/docs/diagnostics.md +12 -11
- package/docs/encoding-core.md +18 -4
- package/docs/guides/writing-in-invented-scripts.md +83 -37
- package/docs/reference/format-details.md +10 -11
- package/docs/reference/note-types.md +15 -15
- package/docs/reference/theme-pages.md +26 -18
- package/engine/anchors.mjs +14 -0
- package/engine/code-fences.mjs +15 -6
- package/engine/content-blocks.mjs +4 -2
- package/engine/content-figures.mjs +19 -5
- package/engine/content-footnotes.mjs +69 -1
- package/engine/content-index.mjs +1 -1
- package/engine/content-render.mjs +59 -43
- package/engine/content-scripts.mjs +301 -58
- package/engine/content-scroll.mjs +8 -3
- package/engine/content-spans.mjs +107 -6
- package/engine/content-verse.mjs +338 -198
- package/engine/encoding-standard.mjs +1 -0
- package/engine/encoding-template.mjs +5 -2
- package/engine/encoding.mjs +19 -14
- 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/markdownlint-run.mjs +190 -5
- 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 +34 -23
- package/engine/prose-lint.mjs +14 -2
- package/engine/roman-characters.mjs +32 -10
- package/engine/roman-run.mjs +3 -0
- package/engine/roman-verse.mjs +1 -0
- package/engine/run-rows.mjs +1 -1
- package/engine/sql-tables.mjs +1 -1
- package/engine/verse-format.mjs +89 -0
- package/engine/verse-metre.mjs +18 -9
- package/engine/verse-regions.mjs +190 -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 +24 -0
- package/types/engine/code-fences.d.mts +1 -3
- package/types/engine/content-footnotes.d.mts +23 -0
- package/types/engine/content-scripts.d.mts +4 -3
- package/types/engine/content-scroll.d.mts +1 -1
- package/types/engine/content-spans.d.mts +3 -1
- package/types/engine/content-verse.d.mts +75 -75
- package/types/engine/name-markup.d.mts +5 -0
- package/types/engine/roman-run.d.mts +3 -1
- package/types/engine/verse-format.d.mts +10 -0
- package/types/engine/verse-metre.d.mts +4 -4
- package/types/engine/verse-regions.d.mts +82 -0
- package/types/engine/wikilink-pattern.d.mts +17 -0
- package/types/engine/wikilink-syntax.d.mts +1 -41
|
@@ -18,6 +18,7 @@ import MarkdownIt from "markdown-it";
|
|
|
18
18
|
import footnotePlugin from "markdown-it-footnote";
|
|
19
19
|
import deflistPlugin from "markdown-it-deflist";
|
|
20
20
|
|
|
21
|
+
import { verseRegions } from "./verse-regions.mjs";
|
|
21
22
|
import { slugify } from "./content-slug.mjs";
|
|
22
23
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
23
24
|
import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
@@ -184,9 +185,20 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
184
185
|
errors = [],
|
|
185
186
|
ids = new Set();
|
|
186
187
|
const counts = {};
|
|
188
|
+
const verse = verseRegions(String(source ?? ""));
|
|
189
|
+
const verseStarts = new Map(verse.map((region) => [region.start, region]));
|
|
190
|
+
const verseLines = new Set(
|
|
191
|
+
verse.flatMap((region) =>
|
|
192
|
+
Array.from(
|
|
193
|
+
{ length: region.end - region.start + 1 },
|
|
194
|
+
(_, index) => region.start + index,
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
);
|
|
187
198
|
let codeFence = null;
|
|
188
199
|
const fault = (line, message) => errors.push({ line: line + 1, column: 1, message });
|
|
189
200
|
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
if (verseLines.has(i)) continue;
|
|
190
202
|
const fence = FENCE.exec(lines[i]);
|
|
191
203
|
if (codeFence) {
|
|
192
204
|
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
|
|
@@ -233,7 +245,11 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
233
245
|
let bodyEnd;
|
|
234
246
|
const firstLine = lines[bodyStart];
|
|
235
247
|
let kind;
|
|
236
|
-
if (
|
|
248
|
+
if (verseStarts.has(bodyStart)) {
|
|
249
|
+
const region = verseStarts.get(bodyStart);
|
|
250
|
+
bodyEnd = region.end + 1;
|
|
251
|
+
kind = "poem";
|
|
252
|
+
} else if (/^ {0,3}:::(?:[ \t]+\{.*\})?[ \t]*$/.test(firstLine)) {
|
|
237
253
|
let depth = 1,
|
|
238
254
|
literal = null;
|
|
239
255
|
bodyEnd = bodyStart + 1;
|
|
@@ -305,10 +321,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
305
321
|
fault(bodyStart, "this block cannot be captioned");
|
|
306
322
|
continue;
|
|
307
323
|
}
|
|
308
|
-
kind =
|
|
309
|
-
first.type === "fence" && /^verse(?:\s|$)/.test(first.info) ?
|
|
310
|
-
"poem"
|
|
311
|
-
: figureKind(first, contents.trim(), resolveRole);
|
|
324
|
+
kind = figureKind(first, contents.trim(), resolveRole);
|
|
312
325
|
}
|
|
313
326
|
if (explicit) kind = explicit === "verse" ? "poem" : explicit;
|
|
314
327
|
const id = attributes.id;
|
|
@@ -338,6 +351,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
338
351
|
}
|
|
339
352
|
let literal = null;
|
|
340
353
|
for (let at = 0; at < lines.length; at++) {
|
|
354
|
+
if (verseLines.has(at)) continue;
|
|
341
355
|
const fence = FENCE.exec(lines[at]);
|
|
342
356
|
if (literal) {
|
|
343
357
|
if (fence && fence[1][0] === literal[0] && fence[1].length >= literal.length)
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* @module
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { inlineSyntaxRanges, verseRegions } from "./verse-regions.mjs";
|
|
33
|
+
import { codeRegions, matchAllOutsideCode } from "./code-fences.mjs";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Where a character offset in `source` falls, as a 1-based line and column.
|
|
@@ -52,9 +53,15 @@ function lineColumn(source, offset) {
|
|
|
52
53
|
export function separateFootnotes(source) {
|
|
53
54
|
const lines = String(source ?? "").split("\n");
|
|
54
55
|
const body = [...lines];
|
|
56
|
+
const verseLines = new Set(
|
|
57
|
+
verseRegions(String(source ?? "")).flatMap((region) =>
|
|
58
|
+
Array.from({ length: region.end - region.start + 1 }, (_, i) => region.start + i),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
55
61
|
const definitions = [];
|
|
56
62
|
let fence = null;
|
|
57
63
|
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
if (verseLines.has(i)) continue;
|
|
58
65
|
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(lines[i]);
|
|
59
66
|
if (fence) {
|
|
60
67
|
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length)
|
|
@@ -107,12 +114,21 @@ const CONTAINER_PREFIX = /^(?:[ \t]{0,3}>[ \t]?|[ \t]{0,3}(?:[-*+]|\d{1,9}[.)])[
|
|
|
107
114
|
*/
|
|
108
115
|
export function misplacedFootnoteDefinitions(source) {
|
|
109
116
|
const lines = String(source ?? "").split("\n");
|
|
117
|
+
const verseLines = new Set(
|
|
118
|
+
verseRegions(String(source ?? "")).flatMap((region) =>
|
|
119
|
+
Array.from({ length: region.end - region.start + 1 }, (_, i) => region.start + i),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
110
122
|
const errors = [];
|
|
111
123
|
let fence = null;
|
|
112
124
|
// Inside the continuation of a genuine top-level definition: a blank
|
|
113
125
|
// line or a four-space indent belongs to it, not to a new mistake.
|
|
114
126
|
let continuation = false;
|
|
115
127
|
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
if (verseLines.has(i)) {
|
|
129
|
+
continuation = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
116
132
|
const line = lines[i];
|
|
117
133
|
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
118
134
|
if (fence) {
|
|
@@ -240,3 +256,55 @@ export function footnoteFindings(source) {
|
|
|
240
256
|
(a, b) => a.line - b.line || a.column - b.column,
|
|
241
257
|
);
|
|
242
258
|
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Labels defined at the top level of this note.
|
|
262
|
+
* @param {string} source - Complete authored note Markdown.
|
|
263
|
+
* @returns {Set<string>} Same-note footnote labels.
|
|
264
|
+
*/
|
|
265
|
+
export function footnoteLabels(source) {
|
|
266
|
+
if (!source.includes("[^")) return new Set();
|
|
267
|
+
return new Set(
|
|
268
|
+
[...separateFootnotes(source).definitions.matchAll(/^\[\^([^\]\s]+)\]:/gm)].map(
|
|
269
|
+
(match) => match[1],
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Actual defined references, excluding literal code, escapes and metadata.
|
|
276
|
+
* @param {string} source - Full note or raw inline fragment.
|
|
277
|
+
* @param {object} [options] - Definition context and source grammar.
|
|
278
|
+
* @param {ReadonlySet<string>} [options.definitions] - Same-note labels; derived from source when omitted.
|
|
279
|
+
* @param {boolean} [options.inline] - Treat all source as inline verse/prose.
|
|
280
|
+
* @returns {Array<{from:number,to:number,key:string,text:string}>} Original UTF-16 source ranges.
|
|
281
|
+
*/
|
|
282
|
+
export function footnoteReferences(
|
|
283
|
+
source,
|
|
284
|
+
{ definitions = footnoteLabels(source), inline = false } = {},
|
|
285
|
+
) {
|
|
286
|
+
if (!definitions.size || !source.includes("[^")) return [];
|
|
287
|
+
const protectedRanges = inlineSyntaxRanges(source).filter(
|
|
288
|
+
([from]) => source[from] !== "{" || source[from - 1] === "]",
|
|
289
|
+
);
|
|
290
|
+
const code = inline ? [] : codeRegions(source);
|
|
291
|
+
return [...source.matchAll(/\[\^([^\]\s]+)\]/g)].flatMap((match) => {
|
|
292
|
+
const from = match.index,
|
|
293
|
+
to = from + match[0].length;
|
|
294
|
+
const escapes = /\\+$/.exec(source.slice(0, from))?.[0].length ?? 0;
|
|
295
|
+
if (
|
|
296
|
+
!definitions.has(match[1]) ||
|
|
297
|
+
escapes % 2 ||
|
|
298
|
+
protectedRanges.some(([first, past]) => from >= first && from < past) ||
|
|
299
|
+
code.some((range) => from >= range.start && from < range.end)
|
|
300
|
+
)
|
|
301
|
+
return [];
|
|
302
|
+
if (
|
|
303
|
+
!inline &&
|
|
304
|
+
source[to] === ":" &&
|
|
305
|
+
/^[ \t]*$/.test(source.slice(source.lastIndexOf("\n", from - 1) + 1, from))
|
|
306
|
+
)
|
|
307
|
+
return [];
|
|
308
|
+
return [{ from, to, key: match[1], text: match[0] }];
|
|
309
|
+
});
|
|
310
|
+
}
|
package/engine/content-index.mjs
CHANGED
|
@@ -1568,7 +1568,7 @@ function dynamicPublicationBody(body) {
|
|
|
1568
1568
|
const source = String(body ?? "");
|
|
1569
1569
|
return (
|
|
1570
1570
|
[...matchAllOutsideCode(source, EXPRESSION)].length > 0 ||
|
|
1571
|
-
codeRegions(source, { spans: false
|
|
1571
|
+
codeRegions(source, { spans: false }).some(({ start, end }) => {
|
|
1572
1572
|
const opener = FENCE_LINE.exec(source.slice(start, end).split("\n", 1)[0]);
|
|
1573
1573
|
const language = opener && parseHeaderArgs(opener[3]).language;
|
|
1574
1574
|
return language === "sql" || language === PAGE_LIST_LANGUAGE;
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* 2. **Captioned items** — each figure's wrapper and label. Its number, kind
|
|
48
48
|
* and role come from the figure scan expansion made, so a figure is
|
|
49
49
|
* numbered once; see {@link module:engine/content-figures.renderFigureBlocks}.
|
|
50
|
-
* 3. **Named blocks, verse and scrolls** — `:::` blocks, verse
|
|
50
|
+
* 3. **Named blocks, verse and scrolls** — `:::` blocks, verse divs and
|
|
51
51
|
* `scroll` fences.
|
|
52
52
|
* 4. **Alerts.**
|
|
53
53
|
* 5. **Spans and withheld sections** — written here on the `web` target; the
|
|
@@ -79,7 +79,8 @@ import { imagePlugin } from "./content-images.mjs";
|
|
|
79
79
|
import { renderRuns } from "./content-scripts.mjs";
|
|
80
80
|
import { renderSpans, spanMarkdownPlugin } from "./content-spans.mjs";
|
|
81
81
|
import { cartoucheFigures, renderSymbolSpans } from "./content-symbols.mjs";
|
|
82
|
-
import { inlineVerse,
|
|
82
|
+
import { inlineVerse, parseVerse, renderVerseQuotes } from "./content-verse.mjs";
|
|
83
|
+
import { verseDivPlugin } from "./verse-regions.mjs";
|
|
83
84
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
84
85
|
import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
|
|
85
86
|
import { headingAttributesPlugin, headingIdsPlugin } from "./heading-attributes.mjs";
|
|
@@ -115,48 +116,59 @@ export function highlightCode(code, language) {
|
|
|
115
116
|
return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
/** Parse logical verse lines in the note's shared inline environment. */
|
|
120
|
+
function parseVerseContent(token, md, env, region) {
|
|
121
|
+
const body = token.type === "verse_inline" ? token.meta.text : token.content;
|
|
122
|
+
const inline = token.type === "verse_inline";
|
|
123
|
+
const paired = inline ? undefined : env.verses?.get(body);
|
|
124
|
+
const footnotes = new Set(Object.keys(env.footnotes?.refs ?? {}).map((key) => key.slice(1)));
|
|
125
|
+
const model = paired?.model ?? region.model ?? parseVerse(body, { inline, footnotes });
|
|
126
|
+
token.meta ??= {};
|
|
127
|
+
token.meta.verse = region;
|
|
128
|
+
token.meta.versePaired = paired;
|
|
129
|
+
const parse = (text) => {
|
|
130
|
+
const children = [];
|
|
131
|
+
md.inline.parse(inlineVerse(text), md, env, children);
|
|
132
|
+
return children;
|
|
133
|
+
};
|
|
134
|
+
token.meta.verseNotes = (model.notes ?? []).map((line) => ({
|
|
135
|
+
children: parse(line.romanText ?? line.text),
|
|
136
|
+
}));
|
|
137
|
+
token.meta.verseGroups = model.groups.map((group) =>
|
|
138
|
+
group.cantoBreak ?
|
|
139
|
+
{
|
|
140
|
+
...group,
|
|
141
|
+
children: group.text ? parse(group.text) : [],
|
|
142
|
+
titleChildren: group.title ? parse(group.title) : [],
|
|
143
|
+
}
|
|
144
|
+
: {
|
|
145
|
+
...group,
|
|
146
|
+
lines: group.lines.map((line) => ({
|
|
147
|
+
...line,
|
|
148
|
+
children: parse(line.text),
|
|
149
|
+
...(paired ? { romanChildren: parse(line.romanText ?? line.text) } : {}),
|
|
150
|
+
})),
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** An opaque quoted verse is parsed once, before footnote collection. */
|
|
156
|
+
function quotedVerse(token, state) {
|
|
157
|
+
const classes = (token.attrGet("class") ?? "")
|
|
158
|
+
.split(/\s+/)
|
|
159
|
+
.filter((name) => name && name !== "verse");
|
|
160
|
+
const attributes = Object.fromEntries(
|
|
161
|
+
(token.attrs ?? []).filter(([key]) => !["id", "class"].includes(key)),
|
|
162
|
+
);
|
|
163
|
+
parseVerseContent(token, state.md, state.env, { id: token.attrGet("id"), classes, attributes });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A verse div's raw body receives inline parsing without block interpretation. */
|
|
128
167
|
function verseInlinePlugin(md) {
|
|
129
168
|
md.core.ruler.after("inline", "verse_inline", (state) => {
|
|
130
169
|
for (const token of state.tokens) {
|
|
131
|
-
if (token.type !== "
|
|
132
|
-
|
|
133
|
-
token.meta ??= {};
|
|
134
|
-
const source = `${token.markup}${token.info}\n${token.content}${token.markup}`;
|
|
135
|
-
const poem = scanVerse(source, { checkWorks: false }).blocks[0];
|
|
136
|
-
if (!poem) continue;
|
|
137
|
-
const paired = state.env.verses?.get(token.content.replace(/\n$/, ""));
|
|
138
|
-
const model = paired?.model ?? poem.model;
|
|
139
|
-
token.meta.verse = poem;
|
|
140
|
-
token.meta.versePaired = paired;
|
|
141
|
-
const parse = (text) => {
|
|
142
|
-
const children = [];
|
|
143
|
-
state.md.inline.parse(inlineVerse(text), state.md, state.env, children);
|
|
144
|
-
return children;
|
|
145
|
-
};
|
|
146
|
-
token.meta.verseGroups = model.groups.map((group) =>
|
|
147
|
-
group.cantoBreak ?
|
|
148
|
-
{ ...group, children: group.text ? parse(group.text) : [] }
|
|
149
|
-
: {
|
|
150
|
-
...group,
|
|
151
|
-
lines: group.lines.map((line) => ({
|
|
152
|
-
...line,
|
|
153
|
-
children: parse(line.text),
|
|
154
|
-
...(paired ?
|
|
155
|
-
{ romanChildren: parse(line.romanText ?? line.text) }
|
|
156
|
-
: {}),
|
|
157
|
-
})),
|
|
158
|
-
},
|
|
159
|
-
);
|
|
170
|
+
if (token.type !== "verse_div") continue;
|
|
171
|
+
parseVerseContent(token, state.md, state.env, token.meta.verseRegion);
|
|
160
172
|
}
|
|
161
173
|
});
|
|
162
174
|
}
|
|
@@ -384,7 +396,8 @@ export function createRenderer(target, options = {}) {
|
|
|
384
396
|
use("footnotes", footnotePlugin);
|
|
385
397
|
if (book) {
|
|
386
398
|
use("alerts", alertMarkdownPlugin);
|
|
387
|
-
use("spans", spanMarkdownPlugin);
|
|
399
|
+
use("spans", spanMarkdownPlugin, { verseInline: quotedVerse });
|
|
400
|
+
use("verse-div", verseDivPlugin);
|
|
388
401
|
use("verse-inline", verseInlinePlugin);
|
|
389
402
|
use("script-emphasis", scriptEmphasisPlugin);
|
|
390
403
|
use("definition-lists", deflistPlugin);
|
|
@@ -456,6 +469,7 @@ export function createRenderer(target, options = {}) {
|
|
|
456
469
|
renderInline,
|
|
457
470
|
},
|
|
458
471
|
).markdown;
|
|
472
|
+
text = renderVerseQuotes(text);
|
|
459
473
|
if (target === "web") text = renderSpans(text).markdown;
|
|
460
474
|
return target === "web" ? renderWithheldSections(text) : text;
|
|
461
475
|
};
|
|
@@ -476,6 +490,8 @@ export function createRenderer(target, options = {}) {
|
|
|
476
490
|
)
|
|
477
491
|
),
|
|
478
492
|
renderInline:
|
|
479
|
-
book ? unsupported : (
|
|
493
|
+
book ? unsupported : (
|
|
494
|
+
(markdown) => restore(md.renderInline(renderVerseQuotes(String(markdown ?? ""))))
|
|
495
|
+
),
|
|
480
496
|
};
|
|
481
497
|
}
|