@heroiclands/package-build 22.25.0 → 22.27.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 +17 -0
- package/README.md +4 -0
- package/assets/styles/content-base.css +46 -3
- package/docs/api.md +35 -12
- package/docs/authoring/frontmatter.md +21 -0
- package/docs/authoring/links-and-markup.md +96 -149
- package/docs/content-format.md +6 -2
- package/docs/guides/government-migration.md +52 -0
- package/docs/guides/site.md +5 -5
- package/docs/reference/format-details.md +152 -246
- package/docs/reference/note-types.md +27 -28
- package/docs/reference/theme-pages.md +33 -29
- package/engine/anchors.mjs +85 -13
- package/engine/art-fields.mjs +1 -1
- package/engine/base-compiler.mjs +9 -1
- package/engine/bundles.mjs +1 -1
- package/engine/calendar-notes.mjs +1 -1
- package/engine/code-fences.mjs +6 -2
- package/engine/content-alerts.mjs +195 -0
- package/engine/content-blocks.mjs +119 -241
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-figures.mjs +166 -230
- package/engine/content-format-check.mjs +3 -2
- package/engine/content-format.yaml +1 -5
- package/engine/content-images.mjs +3 -3
- package/engine/content-poetry.mjs +141 -0
- package/engine/content-spans.mjs +177 -0
- package/engine/foundry-entries.mjs +2 -0
- package/engine/frontmatter-lint.mjs +2 -1
- package/engine/heading-attributes.mjs +1 -1
- package/engine/helpers.mjs +8 -3
- package/engine/holdings.mjs +197 -333
- package/engine/index.mjs +1 -1
- package/engine/infobox.mjs +11 -1
- package/engine/journals.mjs +8 -6
- package/engine/markdown-expressions.mjs +9 -3
- package/engine/metadata-index.mjs +5 -3
- package/engine/note-vocabulary.mjs +30 -13
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-render.mjs +90 -31
- package/engine/populations.mjs +17 -83
- package/engine/site-build.mjs +44 -16
- package/engine/site-preview.mjs +22 -6
- package/engine/wikilinks.mjs +18 -4
- package/hugo-theme/layouts/partials/holdings/entry.html +2 -2
- package/hugo-theme/layouts/partials/holdings.html +14 -14
- package/hugo-theme/static/css/style.css +40 -2
- package/package.json +1 -1
- package/sohl/item-fields.mjs +0 -16
- package/types/engine/anchors.d.mts +13 -4
- package/types/engine/art-fields.d.mts +1 -1
- package/types/engine/calendar-notes.d.mts +1 -1
- package/types/engine/code-fences.d.mts +3 -1
- package/types/engine/content-alerts.d.mts +62 -0
- package/types/engine/content-blocks.d.mts +3 -10
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-figures.d.mts +13 -20
- package/types/engine/content-images.d.mts +1 -1
- package/types/engine/content-poetry.d.mts +32 -0
- package/types/engine/content-spans.d.mts +18 -0
- package/types/engine/holdings.d.mts +75 -118
- package/types/engine/infobox.d.mts +1 -0
- package/types/engine/markdown-expressions.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +14 -2
- package/types/engine/populations.d.mts +4 -13
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/wikilinks.d.mts +5 -1
package/engine/infobox.mjs
CHANGED
|
@@ -427,6 +427,7 @@ export function isUnsetSentinel(value) {
|
|
|
427
427
|
/**
|
|
428
428
|
* Whether a value is worth a row.
|
|
429
429
|
*
|
|
430
|
+
* A field declaring `nullText` handles explicit null before this general test.
|
|
430
431
|
* Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
|
|
431
432
|
* writes "nobody filled this in" — a note that declares every key of its type
|
|
432
433
|
* and leaves most of them empty is the ordinary shape, not the exception — and
|
|
@@ -722,7 +723,7 @@ function structuredRows(field, raw, resolve, label, contentPackage) {
|
|
|
722
723
|
// Address: an authored plain list of addresses normalises to a list of
|
|
723
724
|
// Address tuples, which `isMapping` also reports true for, and treating
|
|
724
725
|
// one as a relationship keyed off a `to` it never carries is what emptied
|
|
725
|
-
// `lore`, `parents
|
|
726
|
+
// `lore`, `parents` and `economy` silently.
|
|
726
727
|
const relations =
|
|
727
728
|
Array.isArray(raw) ? raw.filter((entry) => isMapping(entry) && !isAddressTuple(entry)) : [];
|
|
728
729
|
if (relations.length) {
|
|
@@ -943,6 +944,15 @@ function noteBox(
|
|
|
943
944
|
if (structured.length) shown.add(field.name);
|
|
944
945
|
continue;
|
|
945
946
|
}
|
|
947
|
+
if (raw === null && field.nullText) {
|
|
948
|
+
rows.push({
|
|
949
|
+
label: overlay.label ?? humanizeFieldName(field.name),
|
|
950
|
+
kind: "text",
|
|
951
|
+
value: field.nullText,
|
|
952
|
+
});
|
|
953
|
+
shown.add(field.name);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
946
956
|
if (!hasValue(raw)) continue;
|
|
947
957
|
|
|
948
958
|
if (overlay.group) {
|
package/engine/journals.mjs
CHANGED
|
@@ -196,7 +196,7 @@ export function splitPages(body, leadName = "Introduction", resolveRole) {
|
|
|
196
196
|
if (figure) {
|
|
197
197
|
closeCurrent();
|
|
198
198
|
current = {
|
|
199
|
-
name: figure.label,
|
|
199
|
+
name: figure.label || figure.caption,
|
|
200
200
|
anchorSlug: figure.id || null,
|
|
201
201
|
level: 1,
|
|
202
202
|
classes: [],
|
|
@@ -404,7 +404,7 @@ function figureIsWholePage(figure, page) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
/**
|
|
407
|
-
* The fields particular to the Foundry page a
|
|
407
|
+
* The fields particular to the Foundry page a captioned item begins.
|
|
408
408
|
*
|
|
409
409
|
* A fence holding exactly one picture, captioned with no inline markup, and
|
|
410
410
|
* standing alone on its page becomes a page of type `image`: `src` the
|
|
@@ -442,14 +442,16 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
|
|
|
442
442
|
// image's own render takes when no configuration resolves it.
|
|
443
443
|
}
|
|
444
444
|
return {
|
|
445
|
-
name: figure.label,
|
|
445
|
+
name: figure.label || figure.caption,
|
|
446
446
|
type: "image",
|
|
447
447
|
src,
|
|
448
448
|
image: figure.hasCaption ? { caption: figure.caption } : {},
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
451
|
const label =
|
|
452
|
-
figure.hasCaption ?
|
|
452
|
+
figure.hasCaption ?
|
|
453
|
+
`${figure.label ? `${figure.label}: ` : ""}${md.renderInline(figure.caption)}`
|
|
454
|
+
: figure.label;
|
|
453
455
|
const content = renderFoundryMarkdown(
|
|
454
456
|
page.markdown,
|
|
455
457
|
undefined,
|
|
@@ -458,7 +460,7 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
|
|
|
458
460
|
resolveRole,
|
|
459
461
|
);
|
|
460
462
|
return {
|
|
461
|
-
name: figure.label,
|
|
463
|
+
name: figure.label || figure.caption,
|
|
462
464
|
type: "text",
|
|
463
465
|
text: {
|
|
464
466
|
format: 1,
|
|
@@ -763,7 +765,7 @@ export class Journals extends BasePackCompiler {
|
|
|
763
765
|
|
|
764
766
|
// The markdown this pass reads has already had its embeds rewritten
|
|
765
767
|
// into ordinary images, each `src` the pathname `pathnameRoles` keys
|
|
766
|
-
// its roles by — so a
|
|
768
|
+
// its roles by — so a captioned map names itself `Map 1` here
|
|
767
769
|
// exactly as the website and the book name the same picture.
|
|
768
770
|
const roles = pathnameRoles(this.linkIndex);
|
|
769
771
|
|
|
@@ -154,7 +154,7 @@ const HELPERS = Object.freeze([
|
|
|
154
154
|
name: "ref",
|
|
155
155
|
params: 'address [form="number"|"full"|"title"]',
|
|
156
156
|
summary:
|
|
157
|
-
'A link to a
|
|
157
|
+
'A link to a captioned item, by its anchor — `"#thorn"` on this note, ' +
|
|
158
158
|
'`"note-address#thorn"` on another. `form` is `number` (the default, and ' +
|
|
159
159
|
'admitted explicitly), rendering "Figure 13"; `full`, the number and the ' +
|
|
160
160
|
"caption; or `title`, the caption alone. Always renders as a link to the " +
|
|
@@ -173,6 +173,10 @@ const HELPERS = Object.freeze([
|
|
|
173
173
|
const { parsed, figure, crossNote, noteFound, url } = refFigure(address, figures);
|
|
174
174
|
if (!figure)
|
|
175
175
|
throw new RangeError(refNotFoundFault(address, parsed, crossNote, noteFound));
|
|
176
|
+
if (form === "number" && !figure.label)
|
|
177
|
+
throw new RangeError(
|
|
178
|
+
'a reference to an unnumbered caption requires form="title" or form="full"',
|
|
179
|
+
);
|
|
176
180
|
if (form !== "number" && !figure.hasCaption)
|
|
177
181
|
throw new RangeError(refNoCaptionFault(form, parsed));
|
|
178
182
|
return renderRef(figure, parsed, form, { url, link: figures?.link });
|
|
@@ -271,7 +275,7 @@ function flattenCaptionLinks(caption) {
|
|
|
271
275
|
function refLabel(figure, form) {
|
|
272
276
|
if (form === "number") return figure.label;
|
|
273
277
|
const title = flattenCaptionLinks(figure.caption);
|
|
274
|
-
return form === "title" ? title : `${figure.label}: ${title}`;
|
|
278
|
+
return form === "title" || !figure.label ? title : `${figure.label}: ${title}`;
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
/**
|
|
@@ -394,6 +398,8 @@ function refCallFault(node, figures) {
|
|
|
394
398
|
if (typeof address !== "string" || !address.trim()) return null;
|
|
395
399
|
const { parsed, figure, crossNote, noteFound } = refFigure(address, figures);
|
|
396
400
|
if (!figure) return refNotFoundFault(address, parsed, crossNote, noteFound);
|
|
401
|
+
if (form === "number" && !figure.label)
|
|
402
|
+
return 'a reference to an unnumbered caption requires form="title" or form="full"';
|
|
397
403
|
if (form !== "number" && !figure.hasCaption) return refNoCaptionFault(form, parsed);
|
|
398
404
|
return null;
|
|
399
405
|
}
|
|
@@ -448,7 +454,7 @@ function positionAt(body, offset, bodyLine) {
|
|
|
448
454
|
* {url: string|null, figures: Map<string, object>}|undefined,
|
|
449
455
|
* link?: Function}, file?: string, bodyLine?: number}} [options] -
|
|
450
456
|
* `figures` is the `ref` helper's own view of the corpus: `get` reads this
|
|
451
|
-
* note's
|
|
457
|
+
* note's captioned items by id, and `note` resolves another note's the
|
|
452
458
|
* way a wikilink would — omitted, a cross-note address is refused rather
|
|
453
459
|
* than looked up. `link` overrides how a resolved reference renders; the
|
|
454
460
|
* default is a Markdown link to the target's own anchor.
|
|
@@ -424,17 +424,19 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
424
424
|
// the record naming a path at all.
|
|
425
425
|
asset: record.asset ?? undefined,
|
|
426
426
|
// What a place is next to, reachable from and within, and
|
|
427
|
-
//
|
|
427
|
+
// which affiliation governs it, carried so a consumer's lint
|
|
428
428
|
// checks a border across the package boundary from both ends,
|
|
429
429
|
// a consumer's map draws the dependency's places beside its
|
|
430
430
|
// own, and a consumer's pages list what a dependency's
|
|
431
|
-
// affiliation
|
|
431
|
+
// affiliation governs and what lies within its regions. A
|
|
432
432
|
// population lets the same checks include dependency figures.
|
|
433
433
|
borders: record.data?.borders ?? undefined,
|
|
434
434
|
routes: record.data?.routes ?? undefined,
|
|
435
435
|
parents: record.data?.parents ?? undefined,
|
|
436
|
-
domains: record.data?.domains ?? undefined,
|
|
437
436
|
population: record.data?.population ?? undefined,
|
|
437
|
+
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
438
|
+
{ government: record.data.government }
|
|
439
|
+
: {}),
|
|
438
440
|
package: pkg,
|
|
439
441
|
});
|
|
440
442
|
const entry = index.get(key);
|
|
@@ -112,7 +112,7 @@ import { checkCultureChoice } from "./culture-choice.mjs";
|
|
|
112
112
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
113
113
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
114
114
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
115
|
-
import {
|
|
115
|
+
import { checkGovernment } from "./holdings.mjs";
|
|
116
116
|
import {
|
|
117
117
|
checkBeingMeasurement,
|
|
118
118
|
parseBeingHeight,
|
|
@@ -153,6 +153,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
153
153
|
* so each takes a row labelled and anchored by the post's own name.
|
|
154
154
|
* @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
|
|
155
155
|
* @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
|
|
156
|
+
* @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
|
|
156
157
|
* @property {string} [shape] - Human-readable shape, for a finding and for
|
|
157
158
|
* documentation.
|
|
158
159
|
* @property {string} [entryShape] - For a `scalar-or-map` field, what one
|
|
@@ -208,8 +209,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
208
209
|
* @property {(note: object, opts: {index?: object}) => object[]} [check] - A
|
|
209
210
|
* check of the whole note, run by the frontmatter lint beside the field
|
|
210
211
|
* checks with the same index. For a rule that is about the note rather than
|
|
211
|
-
* one of its fields
|
|
212
|
-
* `domains`, and is located at the note's `type:` line.
|
|
212
|
+
* one of its fields, located at the note's `type:` line.
|
|
213
213
|
*/
|
|
214
214
|
|
|
215
215
|
/* --------------------------------------------------------------------- */
|
|
@@ -512,8 +512,8 @@ const CHARGES = Object.freeze([
|
|
|
512
512
|
/**
|
|
513
513
|
* Everything `place` asks of a whole note.
|
|
514
514
|
*
|
|
515
|
-
* Two questions about one subject, and they are independent: who
|
|
516
|
-
*
|
|
515
|
+
* Two questions about one subject, and they are independent: who governs this
|
|
516
|
+
* inhabited place, and — where the note is a body rather than somewhere within one — what
|
|
517
517
|
* the body states about itself. A type declares one whole-note check, so the
|
|
518
518
|
* two are composed here rather than either being folded into the other.
|
|
519
519
|
*
|
|
@@ -522,7 +522,7 @@ const CHARGES = Object.freeze([
|
|
|
522
522
|
* @returns {object[]} Findings from both.
|
|
523
523
|
*/
|
|
524
524
|
function checkPlace(note, opts) {
|
|
525
|
-
return [...
|
|
525
|
+
return [...checkGovernment(note), ...checkWorldFacts(note, opts)];
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
/** Validate a being's authored date using the shared note-date grammar. */
|
|
@@ -1089,13 +1089,6 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1089
1089
|
accepts: ["place"],
|
|
1090
1090
|
describe: "Where the affiliation's authority sits.",
|
|
1091
1091
|
},
|
|
1092
|
-
{
|
|
1093
|
-
name: "domains",
|
|
1094
|
-
...LINKS,
|
|
1095
|
-
ref: "place",
|
|
1096
|
-
accepts: ["place"],
|
|
1097
|
-
describe: "Places over which it holds sway.",
|
|
1098
|
-
},
|
|
1099
1092
|
{ name: "population", ...NUM, describe: "How many people it counts." },
|
|
1100
1093
|
{
|
|
1101
1094
|
name: "economy",
|
|
@@ -1563,6 +1556,18 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1563
1556
|
check: checkPopulation,
|
|
1564
1557
|
describe: "Approximate population, to two significant digits.",
|
|
1565
1558
|
},
|
|
1559
|
+
{
|
|
1560
|
+
name: "government",
|
|
1561
|
+
...LINK,
|
|
1562
|
+
shape: "an Address or null",
|
|
1563
|
+
ref: "affiliation",
|
|
1564
|
+
accepts: ["affiliation"],
|
|
1565
|
+
nullText: "Complete anarchy",
|
|
1566
|
+
describe:
|
|
1567
|
+
"The governing affiliation Address; the default target type is affiliation. " +
|
|
1568
|
+
"Explicit null means complete anarchy. " +
|
|
1569
|
+
"A positive population with no government key produces an advisory.",
|
|
1570
|
+
},
|
|
1566
1571
|
{
|
|
1567
1572
|
name: "market",
|
|
1568
1573
|
...NUM,
|
|
@@ -1856,3 +1861,15 @@ export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
|
|
|
1856
1861
|
if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
|
|
1857
1862
|
return entry.subTypes;
|
|
1858
1863
|
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* Accepted migration input with no canonical field or emitted value.
|
|
1867
|
+
*
|
|
1868
|
+
* Legacy affiliation domains are ignored. They never supply a government.
|
|
1869
|
+
* @param {string} type - The note type.
|
|
1870
|
+
* @param {string} key - An authored data key.
|
|
1871
|
+
* @returns {boolean} Whether the obsolete key is accepted during migration.
|
|
1872
|
+
*/
|
|
1873
|
+
export function isLegacyDataField(type, key) {
|
|
1874
|
+
return currentType(type) === "affiliation" && key === "domains";
|
|
1875
|
+
}
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -745,7 +745,7 @@ export async function buildPdf({
|
|
|
745
745
|
* @returns {string} Typst markup.
|
|
746
746
|
*/
|
|
747
747
|
const figureCounts = { code: 0, table: 0, figure: 0, map: 0, poem: 0, prose: 0 };
|
|
748
|
-
// The role lookup a
|
|
748
|
+
// The role lookup a captioned item's `map` counter reaches through —
|
|
749
749
|
// see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for a
|
|
750
750
|
// page's own scan below, which runs before its embeds are rewritten into
|
|
751
751
|
// ordinary images; `pathRoles` is the same lookup keyed by the pathname
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -85,10 +85,13 @@ export const BOOK_IMAGE_WIDTHS = Object.freeze({
|
|
|
85
85
|
xlarge: "8cm",
|
|
86
86
|
"full-width": '"full-width"',
|
|
87
87
|
});
|
|
88
|
+
import { markupAnchorFindings } from "./anchors.mjs";
|
|
88
89
|
import { slugify } from "./content-slug.mjs";
|
|
90
|
+
import { alertMarkdownPlugin, scanAlerts, ALERT_TYPES } from "./content-alerts.mjs";
|
|
91
|
+
import { spanMarkdownPlugin, scanSpans } from "./content-spans.mjs";
|
|
89
92
|
import { scanFigures } from "./content-figures.mjs";
|
|
90
93
|
import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
|
|
91
|
-
import { inlineVerse, poetryStanzas } from "./content-poetry.mjs";
|
|
94
|
+
import { inlineVerse, poetryStanzas, scanPoetry } from "./content-poetry.mjs";
|
|
92
95
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
93
96
|
import { HTML_TAG, htmlMessage } from "./content-html.mjs";
|
|
94
97
|
import { EXPRESSION } from "./markdown-expressions.mjs";
|
|
@@ -108,9 +111,7 @@ import {
|
|
|
108
111
|
* a class and let CSS decide.
|
|
109
112
|
*/
|
|
110
113
|
const BLOCK_PRINT = Object.freeze({
|
|
111
|
-
info: Object.freeze({ color: "#2f6f9f", background: "#eef6fb", symbol: "i" }),
|
|
112
114
|
secret: Object.freeze({ color: "#5c4b8a", background: "#f2eefb", symbol: "!" }),
|
|
113
|
-
warn: Object.freeze({ color: "#9a6700", background: "#fff5db", symbol: "!" }),
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
import {
|
|
@@ -197,6 +198,24 @@ export function labelFor(anchor) {
|
|
|
197
198
|
export function createParser(registry) {
|
|
198
199
|
const md = new MarkdownIt({ html: false, linkify: false, typographer: false });
|
|
199
200
|
md.use(footnotePlugin);
|
|
201
|
+
md.use(alertMarkdownPlugin);
|
|
202
|
+
md.use(spanMarkdownPlugin);
|
|
203
|
+
// Verse is held inside a literal fence. Parse its inline content before
|
|
204
|
+
// footnote_tail collects definitions, using the surrounding note's env.
|
|
205
|
+
md.core.ruler.after("inline", "poetry_inline", (state) => {
|
|
206
|
+
for (const token of state.tokens) {
|
|
207
|
+
if (token.type !== "fence" || !/^poetry(?:\s|$)/.test(token.info?.trim() || ""))
|
|
208
|
+
continue;
|
|
209
|
+
token.meta ??= {};
|
|
210
|
+
token.meta.poetryStanzas = poetryStanzas(token.content).map((stanza) =>
|
|
211
|
+
stanza.map(({ text, level }) => {
|
|
212
|
+
const children = [];
|
|
213
|
+
state.md.inline.parse(inlineVerse(text), state.md, state.env, children);
|
|
214
|
+
return { children, level };
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
200
219
|
md.use(deflistPlugin);
|
|
201
220
|
md.use(iconPlugin(registry));
|
|
202
221
|
// The same plugin the HTML surfaces use, so one directive is read once and
|
|
@@ -298,7 +317,7 @@ function offsetPosition(source, offset, opts = {}) {
|
|
|
298
317
|
* those passes did not run — `opts.prepared` — because each of them leaves the
|
|
299
318
|
* markup as written when it fails and has already said so in its own words.
|
|
300
319
|
*
|
|
301
|
-
* A
|
|
320
|
+
* A fenced div and a leading caption are the structures this pass handles, and their
|
|
302
321
|
* own scanners decide what is well formed; their findings are reported here so
|
|
303
322
|
* that a front-matter file, a prose file and a note body all get them.
|
|
304
323
|
*
|
|
@@ -325,7 +344,7 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
325
344
|
};
|
|
326
345
|
|
|
327
346
|
const blocks = scanBlocks(source);
|
|
328
|
-
for (const error of blocks.errors)
|
|
347
|
+
for (const error of [...blocks.errors, ...scanAlerts(source).errors])
|
|
329
348
|
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
330
349
|
// A container's body, read for faults of its own — an empty box, an
|
|
331
350
|
// attribute that does not parse — which the scan above passes over, because
|
|
@@ -349,6 +368,8 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
349
368
|
);
|
|
350
369
|
}
|
|
351
370
|
}
|
|
371
|
+
for (const error of [...scanSpans(source).errors, ...markupAnchorFindings(source)])
|
|
372
|
+
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
352
373
|
for (const error of scanFigures(source).errors)
|
|
353
374
|
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
354
375
|
// An image sharing its paragraph with other text, or an address or title
|
|
@@ -527,14 +548,11 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
527
548
|
}
|
|
528
549
|
}
|
|
529
550
|
if (!opts.insideAdmonition) {
|
|
530
|
-
const
|
|
551
|
+
const captionTargets = scanFigures(source).figures;
|
|
531
552
|
const blocks = scanBlocks(source).blocks.filter(
|
|
532
553
|
(block) =>
|
|
533
|
-
!
|
|
534
|
-
(figure) =>
|
|
535
|
-
block.name === "poetry" &&
|
|
536
|
-
block.start >= figure.bodyStart &&
|
|
537
|
-
block.end < figure.bodyEnd,
|
|
554
|
+
!captionTargets.some(
|
|
555
|
+
(figure) => block.start >= figure.bodyStart && block.end < figure.bodyEnd,
|
|
538
556
|
),
|
|
539
557
|
);
|
|
540
558
|
if (blocks.length) {
|
|
@@ -548,21 +566,13 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
548
566
|
insideAdmonition: true,
|
|
549
567
|
}),
|
|
550
568
|
);
|
|
551
|
-
if (block.name === "
|
|
552
|
-
const content =
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
insideAdmonition: true,
|
|
559
|
-
});
|
|
560
|
-
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
561
|
-
})
|
|
562
|
-
.join(" \\\n"),
|
|
563
|
-
)
|
|
564
|
-
.join("\n\n");
|
|
565
|
-
output.push(`\n#block(width: 100%)[${content}]\n`);
|
|
569
|
+
if (block.name === "div") {
|
|
570
|
+
const content = markdownToTypst(block.body, {
|
|
571
|
+
...sharedOptions,
|
|
572
|
+
insideAdmonition: false,
|
|
573
|
+
});
|
|
574
|
+
const anchor = block.id ? ` <${sectionLabel(anchorPrefix, block.id)}>` : "";
|
|
575
|
+
output.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
|
|
566
576
|
cursor = block.end + 1;
|
|
567
577
|
continue;
|
|
568
578
|
}
|
|
@@ -629,7 +639,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
629
639
|
{ ...caption, imagesRemaining: { n: countImages(block) } }
|
|
630
640
|
: caption;
|
|
631
641
|
const segments = [
|
|
632
|
-
caption.kind
|
|
642
|
+
caption.kind !== "table" && caption.kind !== "figure" && caption.kind !== "map" ?
|
|
633
643
|
markdownToTypst(block, { ...sharedOptions, captions: undefined })
|
|
634
644
|
: renderMarkdownSegment(block, md, { ...ctx, caption: captioned }, definitions),
|
|
635
645
|
];
|
|
@@ -732,7 +742,23 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
732
742
|
}
|
|
733
743
|
case "fence":
|
|
734
744
|
case "code_block": {
|
|
735
|
-
|
|
745
|
+
if (token.type === "fence" && /^poetry(?:\s|$)/.test(token.info?.trim() || "")) {
|
|
746
|
+
const content = token.meta.poetryStanzas
|
|
747
|
+
.map((stanza) =>
|
|
748
|
+
stanza
|
|
749
|
+
.map(({ children, level }) => {
|
|
750
|
+
const verse = renderInline({ children }, ctx);
|
|
751
|
+
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
752
|
+
})
|
|
753
|
+
.join(" \\\n"),
|
|
754
|
+
)
|
|
755
|
+
.join("\n\n");
|
|
756
|
+
const poem = scanPoetry(
|
|
757
|
+
`${token.markup}${token.info}\n${token.content}${token.markup}`,
|
|
758
|
+
).blocks[0];
|
|
759
|
+
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
760
|
+
out.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
|
|
761
|
+
} else out.push(rawBlock(token.content, token.info?.trim() || ""));
|
|
736
762
|
return 1;
|
|
737
763
|
}
|
|
738
764
|
case "hr":
|
|
@@ -741,7 +767,20 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
741
767
|
case "blockquote_open": {
|
|
742
768
|
const end = matching(tokens, i, "blockquote_open", "blockquote_close");
|
|
743
769
|
const inner = renderTokens(tokens.slice(i + 1, end), { ...ctx });
|
|
744
|
-
|
|
770
|
+
if (token.meta?.alert) {
|
|
771
|
+
const alert = token.meta.alert;
|
|
772
|
+
const colors = {
|
|
773
|
+
note: "#0969da",
|
|
774
|
+
tip: "#1a7f37",
|
|
775
|
+
important: "#8250df",
|
|
776
|
+
warning: "#9a6700",
|
|
777
|
+
};
|
|
778
|
+
const spec = ALERT_TYPES[alert.type];
|
|
779
|
+
const icon = `#image(bytes("${escapeTypstString(spec.svg.replace("<svg ", '<svg xmlns="http://www.w3.org/2000/svg" ').replaceAll("currentColor", colors[alert.type]))}"), format: "svg", width: 0.9em, height: 0.9em)`;
|
|
780
|
+
const anchor = alert.id ? ` <${sectionLabel(ctx.anchorPrefix, alert.id)}>` : "";
|
|
781
|
+
const box = `\n#block(width: 100%, stroke: (left: 2pt + rgb("${colors[alert.type]}")), inset: 8pt, above: 0.7em, below: 0.7em)[#text(fill: rgb("${colors[alert.type]}"), weight: "bold")[${icon} ${inlineMarkup(alert.title ?? alert.attributes?.title ?? spec.title, ctx)}]\n\n${inner}]${anchor}\n\n`;
|
|
782
|
+
out.push(alert.classes?.includes("border") ? figureBorder(box) : box);
|
|
783
|
+
} else out.push(`\n#quote(block: true)[${inner}]\n\n`);
|
|
745
784
|
return end - i + 1;
|
|
746
785
|
}
|
|
747
786
|
case "bullet_list_open":
|
|
@@ -964,13 +1003,15 @@ function inlineMarkup(text, ctx) {
|
|
|
964
1003
|
*/
|
|
965
1004
|
function captionMarkup(caption, ctx) {
|
|
966
1005
|
const label = escapeTypst(caption.label);
|
|
967
|
-
return caption.hasCaption ?
|
|
1006
|
+
return caption.hasCaption ?
|
|
1007
|
+
`${label ? `${label}: ` : ""}${inlineMarkup(caption.caption, ctx)}`
|
|
1008
|
+
: label;
|
|
968
1009
|
}
|
|
969
1010
|
|
|
970
1011
|
/**
|
|
971
1012
|
* How many markdown images a figure's contents carry.
|
|
972
1013
|
*
|
|
973
|
-
* A grouped figure's
|
|
1014
|
+
* A grouped figure's leading caption describes the whole plate, not any one
|
|
974
1015
|
* picture in it, so {@link renderImage} needs to know which image is the
|
|
975
1016
|
* last — the one that carries the group's single label and anchor — and this
|
|
976
1017
|
* is the count it counts down from.
|
|
@@ -1157,6 +1198,24 @@ function renderInline(token, ctx) {
|
|
|
1157
1198
|
case "code_inline":
|
|
1158
1199
|
out.push(inlineRaw(child.content));
|
|
1159
1200
|
break;
|
|
1201
|
+
case "span_open": {
|
|
1202
|
+
out.push("#box[");
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
case "span_close": {
|
|
1206
|
+
let depth = 1;
|
|
1207
|
+
let opening;
|
|
1208
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
1209
|
+
if (children[j].type === "span_close") depth += 1;
|
|
1210
|
+
if (children[j].type === "span_open" && --depth === 0) {
|
|
1211
|
+
opening = children[j];
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const id = opening?.attrGet?.("id");
|
|
1216
|
+
out.push(`]${id ? ` <${sectionLabel(ctx.anchorPrefix, id)}>` : ""}`);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1160
1219
|
case "strong_open":
|
|
1161
1220
|
out.push("#strong[");
|
|
1162
1221
|
break;
|