@heroiclands/package-build 23.1.0 → 23.2.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 +71 -0
- package/bin/content-commands.mjs +68 -149
- package/bin/package-build.mjs +56 -10
- package/ci/check-banners.mjs +42 -6
- package/ci/confirm-npm-version.mjs +135 -0
- package/ci/content-format-reference.mjs +14 -5
- package/config.mjs +1 -0
- package/container.mjs +185 -4
- package/content-config.mjs +333 -210
- package/deploy.mjs +116 -10
- package/docs/api.md +238 -246
- package/docs/authoring/assets.md +11 -5
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +87 -0
- package/docs/commands.md +138 -99
- package/docs/configuration.md +277 -125
- package/docs/content-format.md +2 -0
- package/docs/diagnostics.md +48 -13
- package/docs/guides/book.md +341 -21
- package/docs/guides/e2e.md +13 -2
- package/docs/guides/icons.md +14 -4
- package/docs/guides/module-project.md +18 -8
- package/docs/guides/release-and-deploy.md +54 -6
- package/docs/guides/site.md +5 -2
- package/docs/project-setup.md +30 -10
- package/docs/reference/format-details.md +373 -170
- package/docs/reference/note-types.md +128 -109
- package/docs/reference/theme-pages.md +31 -6
- package/e2e.mjs +140 -28
- package/engine/actor-compiler.mjs +22 -31
- package/engine/address-charset.mjs +4 -17
- package/engine/address-fields.mjs +1 -1
- package/engine/address-render.mjs +12 -1
- package/engine/address-values.mjs +6 -0
- package/engine/address.mjs +46 -33
- package/engine/art-fields.mjs +30 -5
- package/engine/art-slots.mjs +10 -1
- package/engine/asset-bindings.mjs +12 -1
- package/engine/base-compiler.mjs +34 -65
- package/engine/being-age.mjs +2 -7
- package/engine/being-measurements.mjs +12 -1
- package/engine/calendar-artifacts.mjs +6 -0
- package/engine/calendar-choice.mjs +12 -1
- package/engine/calendar-format.mjs +12 -1
- package/engine/calendar-human.mjs +12 -1
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-address.mjs +9 -12
- package/engine/content-alerts.mjs +13 -1
- package/engine/content-blocks.mjs +14 -3
- package/engine/content-embeds.mjs +11 -3
- package/engine/content-figures.mjs +9 -0
- package/engine/content-footnotes.mjs +12 -1
- package/engine/content-format-check.mjs +11 -262
- package/engine/content-format.yaml +5 -2
- package/engine/content-images.mjs +3 -0
- package/engine/content-index.mjs +443 -20
- package/engine/content-links.mjs +29 -35
- package/engine/content-lint.mjs +2 -3
- package/engine/content-poetry.mjs +12 -1
- package/engine/content-slug.mjs +2 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-tables.mjs +12 -1
- package/engine/culture-choice.mjs +210 -13
- package/engine/data-keys.mjs +19 -16
- package/engine/date-conversion.mjs +12 -1
- package/engine/declaration-check.mjs +9 -0
- package/engine/derived-fields.mjs +6 -7
- package/engine/diagnostics.mjs +34 -4
- package/engine/docs-checks.mjs +9 -0
- package/engine/document-subtypes.mjs +47 -34
- package/engine/event-views.mjs +139 -14
- package/engine/exported-scene.mjs +92 -28
- package/engine/extension-attributes.mjs +12 -1
- package/engine/field-spec.mjs +11 -102
- package/engine/folder-notes.mjs +0 -64
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/format-generated.mjs +9 -0
- package/engine/foundry-entries.mjs +10 -28
- package/engine/frontmatter-lint.mjs +83 -392
- package/engine/frontmatter.mjs +20 -70
- package/engine/generate.mjs +10 -15
- package/engine/heading-attributes.mjs +12 -1
- package/engine/helpers.mjs +41 -102
- package/engine/holdings.mjs +1 -1
- package/engine/homepage.mjs +3 -10
- package/engine/ids.mjs +1 -167
- package/engine/index-records.mjs +6 -4
- package/engine/index.mjs +0 -3
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +13 -14
- package/engine/item-compiler.mjs +4 -10
- package/engine/item-docs.mjs +3 -5
- package/engine/item-registry.mjs +2 -10
- package/engine/journals.mjs +1 -5
- package/engine/literature-notes.mjs +4 -12
- package/engine/macros.mjs +1 -2
- package/engine/map-graphviz.mjs +12 -1
- package/engine/markdown-expressions.mjs +12 -1
- package/engine/markdownlint-run.mjs +567 -0
- package/engine/metadata-index.mjs +42 -12
- package/engine/note-addresses.mjs +143 -35
- package/engine/note-claims.mjs +14 -28
- package/engine/note-dates.mjs +3 -3
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-ids.mjs +8 -14
- package/engine/note-package.mjs +8 -25
- package/engine/note-state.mjs +1 -3
- package/engine/note-vocabulary.mjs +148 -84
- package/engine/number-words.mjs +12 -1
- package/engine/pack-config.mjs +15 -25
- package/engine/pack-router.mjs +10 -16
- package/engine/pathnames.mjs +8 -22
- package/engine/pdf-build.mjs +773 -121
- package/engine/pdf-coverage.mjs +188 -0
- package/engine/pdf-index.mjs +54 -7
- package/engine/pdf-pages.mjs +247 -0
- package/engine/pdf-render.mjs +114 -4
- package/engine/pdf-toc.mjs +29 -32
- package/engine/project-init.mjs +46 -6
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +52 -88
- package/engine/readability-lint.mjs +9 -0
- package/engine/reckoning-markers.mjs +12 -1
- package/engine/reference-targets.mjs +7 -0
- package/engine/runtime-only-fields.mjs +5 -8
- package/engine/scene-levels.mjs +2 -2
- package/engine/scenes.mjs +1 -1
- package/engine/shortcode-references.mjs +7 -0
- package/engine/site-build.mjs +30 -16
- package/engine/site-index.mjs +10 -16
- package/engine/site-preview.mjs +6 -0
- package/engine/site-root.mjs +8 -1
- package/engine/social-tie-terms.mjs +12 -1
- package/engine/social-ties.mjs +3 -0
- package/engine/sql-tables.mjs +56 -20
- package/engine/standing-terms.mjs +11 -1
- package/engine/standings.mjs +11 -27
- package/engine/system-block.mjs +39 -141
- package/engine/system-vocabulary.mjs +6 -0
- package/engine/walk-files.mjs +69 -0
- package/engine/web-wikilinks.mjs +23 -3
- package/engine/wikilink-syntax.mjs +32 -21
- package/engine/wikilinks.mjs +27 -16
- package/engine/yaml-lint.mjs +7 -4
- package/githooks/attribution.mjs +8 -0
- package/githooks/commit-message.mjs +9 -0
- package/hm3/actors.mjs +9 -48
- package/hm3/default-item-art.mjs +9 -9
- package/hm3/item-fields.mjs +2 -9
- package/hm3/template-priority.mjs +6 -7
- package/hugo-theme/data/banner-kinds.yaml +78 -0
- package/hugo-theme/data/banners.yaml +2 -3
- package/hugo-theme/layouts/404.html +3 -3
- package/hugo-theme/layouts/partials/banner-url.html +31 -7
- package/hugo-theme/layouts/partials/hero-banner.html +13 -14
- package/hugo-theme/layouts/partials/related.html +1 -1
- package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
- package/hugo-theme/static/css/style.css +1 -1
- package/manifest.mjs +11 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +6 -3
- package/release.mjs +83 -18
- package/sohl/actors.mjs +16 -27
- package/sohl/default-item-art.mjs +3 -3
- package/sohl/document-subtypes.mjs +1 -16
- package/sohl/infobox.mjs +2 -3
- package/sohl/item-builders.mjs +5 -8
- package/sohl/item-fields.mjs +5 -35
- package/sohl/items.mjs +0 -4
- package/sohl/kb-passes.mjs +3 -3
- package/types/container.d.mts +116 -5
- package/types/content-config.d.mts +83 -35
- package/types/deploy.d.mts +42 -6
- package/types/e2e.d.mts +35 -3
- package/types/engine/address-charset.d.mts +4 -17
- package/types/engine/address.d.mts +17 -8
- package/types/engine/art-fields.d.mts +7 -4
- package/types/engine/art-slots.d.mts +6 -0
- package/types/engine/base-compiler.d.mts +15 -17
- package/types/engine/content-address.d.mts +9 -12
- package/types/engine/content-embeds.d.mts +4 -2
- package/types/engine/content-format-check.d.mts +0 -64
- package/types/engine/content-index.d.mts +82 -3
- package/types/engine/content-links.d.mts +2 -4
- package/types/engine/culture-choice.d.mts +51 -8
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/derived-fields.d.mts +1 -2
- package/types/engine/diagnostics.d.mts +27 -4
- package/types/engine/document-subtypes.d.mts +12 -31
- package/types/engine/event-views.d.mts +14 -2
- package/types/engine/exported-scene.d.mts +3 -2
- package/types/engine/field-spec.d.mts +13 -122
- package/types/engine/folder-notes.d.mts +0 -39
- package/types/engine/foreign-catalog.d.mts +2 -1
- package/types/engine/foundry-entries.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +2 -2
- package/types/engine/frontmatter.d.mts +19 -45
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +25 -42
- package/types/engine/homepage.d.mts +3 -10
- package/types/engine/ids.d.mts +0 -114
- package/types/engine/index-records.d.mts +6 -4
- package/types/engine/index.d.mts +0 -1
- package/types/engine/infobox-render.d.mts +7 -1
- package/types/engine/infobox.d.mts +3 -2
- package/types/engine/item-docs.d.mts +1 -1
- package/types/engine/item-registry.d.mts +2 -4
- package/types/engine/literature-notes.d.mts +1 -1
- package/types/engine/markdownlint-run.d.mts +40 -0
- package/types/engine/metadata-index.d.mts +9 -1
- package/types/engine/note-addresses.d.mts +27 -2
- package/types/engine/note-claims.d.mts +3 -5
- package/types/engine/note-ids.d.mts +2 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-state.d.mts +1 -3
- package/types/engine/note-vocabulary.d.mts +28 -21
- package/types/engine/pack-config.d.mts +2 -6
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/pathnames.d.mts +3 -3
- package/types/engine/pdf-build.d.mts +98 -12
- package/types/engine/pdf-coverage.d.mts +60 -0
- package/types/engine/pdf-index.d.mts +34 -5
- package/types/engine/pdf-pages.d.mts +143 -0
- package/types/engine/pdf-render.d.mts +45 -1
- package/types/engine/pdf-toc.d.mts +12 -14
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +11 -10
- package/types/engine/runtime-only-fields.d.mts +2 -2
- package/types/engine/site-build.d.mts +9 -5
- package/types/engine/site-index.d.mts +4 -5
- package/types/engine/site-root.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -2
- package/types/engine/standing-terms.d.mts +2 -1
- package/types/engine/standings.d.mts +7 -9
- package/types/engine/system-block.d.mts +10 -44
- package/types/engine/walk-files.d.mts +15 -0
- package/types/engine/wikilink-syntax.d.mts +18 -19
- package/types/engine/yaml-lint.d.mts +5 -4
- package/types/hm3/actors.d.mts +1 -2
- package/types/hm3/item-fields.d.mts +2 -4
- package/types/hm3/template-priority.d.mts +6 -7
- package/types/manifest.d.mts +3 -2
- package/types/release.d.mts +12 -7
- package/types/sohl/actors.d.mts +2 -1
- package/types/sohl/document-subtypes.d.mts +1 -2
- package/types/sohl/item-fields.d.mts +3 -7
- package/engine/retired-fields.mjs +0 -631
- package/types/engine/retired-fields.d.mts +0 -354
package/engine/pdf-render.mjs
CHANGED
|
@@ -327,7 +327,7 @@ function offsetPosition(source, offset, opts = {}) {
|
|
|
327
327
|
* @param {object} opts - As {@link markdownToTypst} takes.
|
|
328
328
|
* @param {string} original - `source` before its definitions were blanked —
|
|
329
329
|
* {@link unusedFootnoteDefinitions} locates a definition by searching for
|
|
330
|
-
* its own text, which `source`
|
|
330
|
+
* its own text, which `source` does not carry.
|
|
331
331
|
* @returns {void}
|
|
332
332
|
*/
|
|
333
333
|
function reportUnrenderable(source, definitions, opts, original) {
|
|
@@ -475,6 +475,9 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
475
475
|
* at. Every link the book sets as a URL is resolved against it, because a
|
|
476
476
|
* reader holding a PDF has no page to resolve a path against — see
|
|
477
477
|
* {@link renderLink}.
|
|
478
|
+
* @param {{get: (key: string) => ({title: string, page: string|number}|undefined)}} [opts.elsewhere] -
|
|
479
|
+
* The notes another book is home to, by the slug and addresses a link reaches
|
|
480
|
+
* them through — see {@link crossBookReference}.
|
|
478
481
|
* @param {boolean} [opts.generated] - Whether the Markdown is a note's
|
|
479
482
|
* generated sections rather than its author's text — see
|
|
480
483
|
* {@link module:engine/generated-sections}. Their links follow the rules
|
|
@@ -518,6 +521,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
518
521
|
headingOffset,
|
|
519
522
|
anchorPrefix,
|
|
520
523
|
url: opts.url,
|
|
524
|
+
elsewhere: opts.elsewhere,
|
|
521
525
|
indexMentions: opts.indexMentions === true,
|
|
522
526
|
generated: Boolean(opts.generated),
|
|
523
527
|
seen: new Map(),
|
|
@@ -1361,6 +1365,10 @@ function pageOfLabel(label) {
|
|
|
1361
1365
|
* usable; one whose target the book leaves out is its words alone, with no
|
|
1362
1366
|
* link and no finding.
|
|
1363
1367
|
*
|
|
1368
|
+
* **A link to a note another book is home to** is not a link: a PDF link cannot
|
|
1369
|
+
* leave its file. It is set as {@link crossBookReference}, in generated text
|
|
1370
|
+
* and authored prose alike, and adds nothing to the index.
|
|
1371
|
+
*
|
|
1364
1372
|
* The address slug is read from the tail of the URL, which is where every
|
|
1365
1373
|
* address this toolchain publishes puts it — `…/<type>-<shortcode>/`. A
|
|
1366
1374
|
* cross-package URL resolved through the link manifest is on another package's
|
|
@@ -1395,10 +1403,33 @@ function renderLink(href, inner, ctx) {
|
|
|
1395
1403
|
const marker = ctx.indexMentions && slug ? indexMarker(slug) : "";
|
|
1396
1404
|
return `${link}${marker}`;
|
|
1397
1405
|
}
|
|
1406
|
+
const away = ctx.elsewhere?.get(slug);
|
|
1407
|
+
if (away) {
|
|
1408
|
+
// A mention in authored prose is what lists the note, under
|
|
1409
|
+
// `pdf.indexSeeAlso`.
|
|
1410
|
+
const marker = ctx.indexMentions && ctx.elsewhere.seeAlso ? indexMarker(away.slug) : "";
|
|
1411
|
+
return `${crossBookReference(inner, away)}${marker}`;
|
|
1412
|
+
}
|
|
1398
1413
|
if (!url || ctx.generated) return inner;
|
|
1399
1414
|
return `#link("${escapeTypstString(absolutePageUrl(url, ctx.url))}")[${inner}]`;
|
|
1400
1415
|
}
|
|
1401
1416
|
|
|
1417
|
+
/**
|
|
1418
|
+
* A reference to a note another book prints, as Typst: the words the link
|
|
1419
|
+
* carried, then the book and the page, `Name (_Book title_, p. 12)`.
|
|
1420
|
+
*
|
|
1421
|
+
* The space before the page number does not break, so a number never starts a
|
|
1422
|
+
* line of its own.
|
|
1423
|
+
*
|
|
1424
|
+
* @param {string} inner - The already-rendered link text.
|
|
1425
|
+
* @param {{title: string, page: string|number}} away - The home book's title
|
|
1426
|
+
* and the page the entry starts on.
|
|
1427
|
+
* @returns {string} Typst markup.
|
|
1428
|
+
*/
|
|
1429
|
+
export function crossBookReference(inner, away) {
|
|
1430
|
+
return `${inner} (#emph[${escapeTypst(away.title)}], p.~${away.page})`;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1402
1433
|
/**
|
|
1403
1434
|
* A width in inches, as a Typst length literal.
|
|
1404
1435
|
*
|
|
@@ -1606,6 +1637,27 @@ export function bookTypstPreamble() {
|
|
|
1606
1637
|
'#let book-accent = rgb("#7c3b1e")',
|
|
1607
1638
|
'#let book-head = rgb("#5e2b14")',
|
|
1608
1639
|
'#let book-faint = rgb("#6b6357")',
|
|
1640
|
+
// The cover is a page of its own: the picture fills it, cropped to fit as
|
|
1641
|
+
// a section banner is, and the title and subtitle sit in white over a
|
|
1642
|
+
// translucent dark band across the upper third so they read over any
|
|
1643
|
+
// art. It carries no running foot or number, and counts as page 1.
|
|
1644
|
+
"#let book-cover(path, title, subtitle) = page(margin: 0pt, fill: book-ink, footer: none)[\n" +
|
|
1645
|
+
' #place(top + left, image(path, width: 100%, height: 100%, fit: "cover"))\n' +
|
|
1646
|
+
" #place(top + left, rect(width: 100%, height: 33.4%, fill: rgb(10, 8, 6, 170)))\n" +
|
|
1647
|
+
" #place(top + left, block(width: 100%, height: 33.4%)[\n" +
|
|
1648
|
+
" #align(center + horizon)[\n" +
|
|
1649
|
+
" #block(width: 80%)[\n" +
|
|
1650
|
+
" #set par(justify: false, first-line-indent: 0em)\n" +
|
|
1651
|
+
" #set text(fill: white, hyphenate: false)\n" +
|
|
1652
|
+
' #text(size: 34pt, weight: "bold")[#title]\n' +
|
|
1653
|
+
' #if subtitle != "" [\n' +
|
|
1654
|
+
" #v(0.6em)\n" +
|
|
1655
|
+
" #text(size: 16pt)[#subtitle]\n" +
|
|
1656
|
+
" ]\n" +
|
|
1657
|
+
" ]\n" +
|
|
1658
|
+
" ]\n" +
|
|
1659
|
+
" ])\n" +
|
|
1660
|
+
"]",
|
|
1609
1661
|
"#let book-ornament = box(baseline: 1pt, " +
|
|
1610
1662
|
"rotate(45deg, rect(width: 3pt, height: 3pt, fill: book-accent)))",
|
|
1611
1663
|
"#let book-footer(name) = context {\n" +
|
|
@@ -1755,6 +1807,18 @@ export function bookTypstPreamble() {
|
|
|
1755
1807
|
].join("\n");
|
|
1756
1808
|
}
|
|
1757
1809
|
|
|
1810
|
+
/**
|
|
1811
|
+
* One author as the title page prints it: `Name`, or `Name, Role` with the
|
|
1812
|
+
* role in title case.
|
|
1813
|
+
*
|
|
1814
|
+
* @param {{name: string, role?: string}} person - An entry of `authors`.
|
|
1815
|
+
* @returns {string} The line.
|
|
1816
|
+
*/
|
|
1817
|
+
function authorLine({ name, role }) {
|
|
1818
|
+
if (!role) return name;
|
|
1819
|
+
return `${name}, ${role.charAt(0).toUpperCase()}${role.slice(1)}`;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1758
1822
|
/**
|
|
1759
1823
|
* The whole book, as one Typst document.
|
|
1760
1824
|
*
|
|
@@ -1858,6 +1922,17 @@ export function bookTypstPreamble() {
|
|
|
1858
1922
|
* plate draws without a picture.
|
|
1859
1923
|
* @param {Map<string, {path: string, title: string}>} [opts.maps] - An entry
|
|
1860
1924
|
* anchor to its SVG path and page heading. Each map takes a page after its entry.
|
|
1925
|
+
* @param {string} [opts.coverImage] - The staged cover picture, relative to the
|
|
1926
|
+
* `.typ`. When given the book opens on a full-page cover, page 1, with the
|
|
1927
|
+
* title and subtitle over it; without one the book opens on the title page.
|
|
1928
|
+
* @param {Array<{name: string, role?: string}>} [opts.authors] - Printed on the
|
|
1929
|
+
* title page one per line, as `Name` or `Name, Role`.
|
|
1930
|
+
* @param {string} [opts.copyright] - The title page foot's first line.
|
|
1931
|
+
* @param {string} [opts.license] - The title page foot's remaining lines; a
|
|
1932
|
+
* line break in it starts a new line.
|
|
1933
|
+
* @param {object} [opts.index] - As {@link module:engine/pdf-index.indexTerms}:
|
|
1934
|
+
* `owns` limits the index to the notes this book is home to, and `foreign`
|
|
1935
|
+
* (present only under `pdf.indexSeeAlso`) lists notes another book prints.
|
|
1861
1936
|
* @returns {string} A complete `.typ` document.
|
|
1862
1937
|
*/
|
|
1863
1938
|
export function renderBook({
|
|
@@ -1871,6 +1946,11 @@ export function renderBook({
|
|
|
1871
1946
|
preamble = "",
|
|
1872
1947
|
banners = new Map(),
|
|
1873
1948
|
maps = new Map(),
|
|
1949
|
+
coverImage = "",
|
|
1950
|
+
authors = [],
|
|
1951
|
+
copyright = "",
|
|
1952
|
+
license = "",
|
|
1953
|
+
index = {},
|
|
1874
1954
|
} = {}) {
|
|
1875
1955
|
// The two halves of one superfamily, chosen together: matched metrics are
|
|
1876
1956
|
// most of why the sans can carry every heading over a serif body without
|
|
@@ -1883,7 +1963,7 @@ export function renderBook({
|
|
|
1883
1963
|
const out = [];
|
|
1884
1964
|
|
|
1885
1965
|
out.push(bookTypstPreamble());
|
|
1886
|
-
out.push(indexPreamble());
|
|
1966
|
+
out.push(indexPreamble({ seeAlso: index.foreign !== undefined }));
|
|
1887
1967
|
out.push("");
|
|
1888
1968
|
out.push(`#set document(title: "${escapeTypstString(title)}")`);
|
|
1889
1969
|
// Cream stock and dark ink rather than a dark screen theme: 2,000 pages of
|
|
@@ -1930,6 +2010,14 @@ export function renderBook({
|
|
|
1930
2010
|
out.push("");
|
|
1931
2011
|
}
|
|
1932
2012
|
|
|
2013
|
+
// The cover, when the book has one, is page 1.
|
|
2014
|
+
if (coverImage) {
|
|
2015
|
+
out.push(
|
|
2016
|
+
`#book-cover("${escapeTypstString(coverImage)}", ` +
|
|
2017
|
+
`"${escapeTypstString(title)}", "${escapeTypstString(subtitle)}")`,
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1933
2021
|
// Title page.
|
|
1934
2022
|
out.push("#align(center + horizon)[");
|
|
1935
2023
|
out.push(` #text(size: 30pt, weight: "bold")[${escapeTypst(title)}]`);
|
|
@@ -1941,7 +2029,29 @@ export function renderBook({
|
|
|
1941
2029
|
out.push(" #v(2em)");
|
|
1942
2030
|
out.push(` #text(size: 10pt)[${escapeTypst(version)}]`);
|
|
1943
2031
|
}
|
|
2032
|
+
if (authors.length) {
|
|
2033
|
+
out.push(" #v(2.5em)");
|
|
2034
|
+
out.push(" #set par(first-line-indent: 0em, justify: false)");
|
|
2035
|
+
out.push(
|
|
2036
|
+
" #text(size: 12pt)[" +
|
|
2037
|
+
authors.map((person) => escapeTypst(authorLine(person))).join(" \\\n ") +
|
|
2038
|
+
"]",
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
1944
2041
|
out.push("]");
|
|
2042
|
+
const foot = [copyright, ...String(license).split(/\r?\n/)]
|
|
2043
|
+
.map((line) => line.trim())
|
|
2044
|
+
.filter(Boolean);
|
|
2045
|
+
if (foot.length) {
|
|
2046
|
+
out.push("#place(bottom + center)[");
|
|
2047
|
+
out.push(" #set par(first-line-indent: 0em, justify: false)");
|
|
2048
|
+
out.push(
|
|
2049
|
+
" #block(width: 80%)[#text(size: 8.5pt, fill: book-faint)[" +
|
|
2050
|
+
foot.map((line) => escapeTypst(line)).join(" \\\n ") +
|
|
2051
|
+
"]]",
|
|
2052
|
+
);
|
|
2053
|
+
out.push("]");
|
|
2054
|
+
}
|
|
1945
2055
|
out.push("#pagebreak()");
|
|
1946
2056
|
out.push("");
|
|
1947
2057
|
|
|
@@ -2031,8 +2141,8 @@ export function renderBook({
|
|
|
2031
2141
|
}
|
|
2032
2142
|
}
|
|
2033
2143
|
|
|
2034
|
-
const
|
|
2035
|
-
if (
|
|
2144
|
+
const chapter = indexChapter(plan?.entries, title, index);
|
|
2145
|
+
if (chapter) out.push(chapter);
|
|
2036
2146
|
|
|
2037
2147
|
return `${out.join("\n")}\n`;
|
|
2038
2148
|
}
|
package/engine/pdf-toc.mjs
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Three consequences follow, and they are behaviour rather than oversights:
|
|
24
24
|
*
|
|
25
|
-
* - **A note no clause selects is not in the book.**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* - **A note no clause selects is not in the book.** This pass has no standing
|
|
26
|
+
* to call that a defect: whether a note belongs in some other book of the
|
|
27
|
+
* package is a question about every book at once, which the coverage report
|
|
28
|
+
* ({@link module:engine/pdf-coverage}) answers.
|
|
28
29
|
* - **A note several clauses select appears several times.** A reference work
|
|
29
30
|
* may legitimately carry an entry under more than one heading, so each
|
|
30
31
|
* occurrence is its own page, its own outline node and its own anchor — and
|
|
@@ -169,20 +170,25 @@ const QUALIFIED_TABLE = /\bfrom\s+["'`]?[A-Za-z_][\w-]*["'`]?\s*\./i;
|
|
|
169
170
|
* @param {object} [opts] - Options.
|
|
170
171
|
* @param {string} [opts.text] - The file's source, for finding positions. Every
|
|
171
172
|
* finding without one is still reported, just without a locator.
|
|
173
|
+
* @param {Array<string|number>} [opts.at] - Where `raw` sits in `text`: `[]`
|
|
174
|
+
* for a tree file, `["pdf", "books", i]` for a tree written inline in the
|
|
175
|
+
* configuration. Every key path — and so every position, here and in
|
|
176
|
+
* {@link runTreeFilters} — starts with it.
|
|
172
177
|
* @returns {{nodes: object[], findings: object[]}} The flattened tree and what
|
|
173
178
|
* was wrong with it.
|
|
174
179
|
*/
|
|
175
|
-
export function parseDocumentTree(raw, { text } = {}) {
|
|
180
|
+
export function parseDocumentTree(raw, { text, at = [] } = {}) {
|
|
176
181
|
const findings = [];
|
|
177
182
|
const nodes = [];
|
|
178
183
|
if (!raw || typeof raw !== "object" || !Array.isArray(raw.contents)) {
|
|
179
184
|
findings.push({
|
|
180
185
|
severity: "error",
|
|
181
186
|
message: "the document tree must be a mapping with a `contents:` list",
|
|
187
|
+
...(at.length ? position(text, at) : {}),
|
|
182
188
|
});
|
|
183
189
|
return { nodes, findings };
|
|
184
190
|
}
|
|
185
|
-
walkSections(raw.contents, ["contents"], [], {}, { nodes, findings, text });
|
|
191
|
+
walkSections(raw.contents, [...at, "contents"], [], {}, { nodes, findings, text });
|
|
186
192
|
return { nodes, findings };
|
|
187
193
|
}
|
|
188
194
|
|
|
@@ -199,9 +205,11 @@ export function parseDocumentTree(raw, { text } = {}) {
|
|
|
199
205
|
* @param {string[]} trail - Section titles above this level.
|
|
200
206
|
* @param {object} inherited - Presentation from ancestors.
|
|
201
207
|
* @param {object} ctx - `{ nodes, findings, text }`, accumulated.
|
|
208
|
+
* @param {Array<string|number>} [inheritedPageAt] - Where the `page:` mapping
|
|
209
|
+
* the ancestors left in force is declared, when one is.
|
|
202
210
|
* @returns {void}
|
|
203
211
|
*/
|
|
204
|
-
function walkSections(contents, keyPath, trail, inherited, ctx) {
|
|
212
|
+
function walkSections(contents, keyPath, trail, inherited, ctx, inheritedPageAt) {
|
|
205
213
|
contents.forEach((entry, i) => {
|
|
206
214
|
const at = [...keyPath, i];
|
|
207
215
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
@@ -241,6 +249,9 @@ function walkSections(contents, keyPath, trail, inherited, ctx) {
|
|
|
241
249
|
for (const key of PRESENTATION_KEYS) {
|
|
242
250
|
if (entry[key] !== undefined) presentation[key] = entry[key];
|
|
243
251
|
}
|
|
252
|
+
// A section's own `page:` replaces the inherited one whole, so the
|
|
253
|
+
// mapping in force is declared on the nearest section that wrote one.
|
|
254
|
+
const pageAt = entry.page !== undefined ? [...at, "page"] : inheritedPageAt;
|
|
244
255
|
|
|
245
256
|
const here = [...trail, title];
|
|
246
257
|
const list = Array.isArray(entry.contents) ? entry.contents : [];
|
|
@@ -256,10 +267,11 @@ function walkSections(contents, keyPath, trail, inherited, ctx) {
|
|
|
256
267
|
depth: here.length,
|
|
257
268
|
keyPath: at,
|
|
258
269
|
presentation,
|
|
270
|
+
pageAt,
|
|
259
271
|
items: collectItems(list, [...at, "contents"], here, ctx),
|
|
260
272
|
});
|
|
261
273
|
|
|
262
|
-
walkSections(list, [...at, "contents"], here, presentation, ctx);
|
|
274
|
+
walkSections(list, [...at, "contents"], here, presentation, ctx, pageAt);
|
|
263
275
|
});
|
|
264
276
|
}
|
|
265
277
|
|
|
@@ -357,14 +369,10 @@ function finding(ctx, keyPath, message, opts) {
|
|
|
357
369
|
* something that is not a note, and unable to pick up the `doc<type>`
|
|
358
370
|
* documentation rows that ride the same index as the notes they document.
|
|
359
371
|
*
|
|
360
|
-
* **A filter that selects nothing is
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* act, so one matching nothing is either wrong or left over from a structure
|
|
365
|
-
* that has moved on, and in both cases the tree should not carry it. Reported
|
|
366
|
-
* with the section's name and the filter's position, so the choice between
|
|
367
|
-
* fixing it and deleting it is the author's.
|
|
372
|
+
* **A filter that selects nothing is not a finding.** A section whose clauses
|
|
373
|
+
* select nothing is left out of the plan, and a book whose clauses select
|
|
374
|
+
* nothing at all is refused by the build. Notes that a mistyped clause fails to
|
|
375
|
+
* take are reported by the coverage check, which names each one.
|
|
368
376
|
*
|
|
369
377
|
* @param {object[]} nodes - From {@link parseDocumentTree}.
|
|
370
378
|
* @param {{query: (sql: string) => Promise<{rows: object[]}>}} db - An open
|
|
@@ -383,18 +391,7 @@ export async function runTreeFilters(nodes, db, { keep = () => true, text } = {}
|
|
|
383
391
|
const where = `\`${node.trail.join(" › ")}\``;
|
|
384
392
|
try {
|
|
385
393
|
const { rows } = await db.query(`SELECT * FROM notes WHERE ${item.where}`);
|
|
386
|
-
|
|
387
|
-
selections.set(item.id, kept);
|
|
388
|
-
if (!kept.length) {
|
|
389
|
-
findings.push({
|
|
390
|
-
severity: "error",
|
|
391
|
-
message:
|
|
392
|
-
`${where}: the filter selected no notes — ` +
|
|
393
|
-
`\`${item.where}\` matches nothing in this project, so ` +
|
|
394
|
-
`either it is wrong or the section should not be here`,
|
|
395
|
-
...position(text, [...item.keyPath, "filter"]),
|
|
396
|
-
});
|
|
397
|
-
}
|
|
394
|
+
selections.set(item.id, rows.filter(keep));
|
|
398
395
|
} catch (err) {
|
|
399
396
|
findings.push({
|
|
400
397
|
severity: "error",
|
|
@@ -426,11 +423,8 @@ export async function runTreeFilters(nodes, db, { keep = () => true, text } = {}
|
|
|
426
423
|
*
|
|
427
424
|
* **A section with nothing in it does not print**, and emptiness is judged after
|
|
428
425
|
* its descendants are: a section holding only sections that all resolved to
|
|
429
|
-
* nothing is itself empty.
|
|
430
|
-
*
|
|
431
|
-
* this is the graceful half of that failure rather than a feature: a build whose
|
|
432
|
-
* filters are broken still produces a readable document to look at while they
|
|
433
|
-
* are fixed. A section holding only prose is not empty; it is a preface.
|
|
426
|
+
* nothing is itself empty. The omission is silent. A section holding only prose
|
|
427
|
+
* is not empty; it is a preface.
|
|
434
428
|
*
|
|
435
429
|
* @param {object[]} nodes - From {@link parseDocumentTree}.
|
|
436
430
|
* @param {object} [opts] - Options.
|
|
@@ -502,6 +496,7 @@ function emitSection(node, nodes, ctx) {
|
|
|
502
496
|
trail: node.trail,
|
|
503
497
|
depth: node.depth,
|
|
504
498
|
presentation: node.presentation,
|
|
499
|
+
pageAt: node.pageAt,
|
|
505
500
|
anchor: anchorFor(slugify(node.trail.join(" ")) || "section", ctx.seen),
|
|
506
501
|
});
|
|
507
502
|
|
|
@@ -521,6 +516,7 @@ function emitSection(node, nodes, ctx) {
|
|
|
521
516
|
trail: node.trail,
|
|
522
517
|
depth: node.depth,
|
|
523
518
|
presentation: node.presentation,
|
|
519
|
+
pageAt: node.pageAt,
|
|
524
520
|
anchor: anchorFor(slugify(item.file.replace(/\.md$/i, "")) || "prose", ctx.seen),
|
|
525
521
|
});
|
|
526
522
|
contributed++;
|
|
@@ -542,6 +538,7 @@ function emitSection(node, nodes, ctx) {
|
|
|
542
538
|
trail: node.trail,
|
|
543
539
|
depth: node.depth,
|
|
544
540
|
presentation: node.presentation,
|
|
541
|
+
pageAt: node.pageAt,
|
|
545
542
|
anchor,
|
|
546
543
|
});
|
|
547
544
|
contributed++;
|
package/engine/project-init.mjs
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
3
|
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
4
|
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
5
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
12
|
*/
|
|
7
13
|
|
|
@@ -13,6 +19,7 @@ import YAML from "yaml";
|
|
|
13
19
|
import prettier from "prettier";
|
|
14
20
|
import prettierConfig from "../prettier-config.mjs";
|
|
15
21
|
import { CONFIG_FILENAMES, configFromData } from "./pack-config.mjs";
|
|
22
|
+
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
16
23
|
import { checkLabelRegistry } from "../labels.mjs";
|
|
17
24
|
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
18
25
|
|
|
@@ -128,6 +135,7 @@ function projectConfig(answers) {
|
|
|
128
135
|
const config = {
|
|
129
136
|
contentPackage: answers.contentPackage,
|
|
130
137
|
packageKind: answers.kind,
|
|
138
|
+
authors: [{ name: answers.author }],
|
|
131
139
|
};
|
|
132
140
|
if (answers.kind !== "documentation") {
|
|
133
141
|
config.compatibility = {
|
|
@@ -138,7 +146,19 @@ function projectConfig(answers) {
|
|
|
138
146
|
config.packs = [{ name: "journals", label: "Journals", type: "JournalEntry" }];
|
|
139
147
|
}
|
|
140
148
|
config.publish = { address: { prefix: "" } };
|
|
141
|
-
|
|
149
|
+
// The stem is the package's own name, so the one book is
|
|
150
|
+
// `<package>-<version>.pdf` rather than `<package>-<id>-<version>.pdf`.
|
|
151
|
+
config.pdf = {
|
|
152
|
+
out: "build/pdf",
|
|
153
|
+
books: [
|
|
154
|
+
{
|
|
155
|
+
id: "book",
|
|
156
|
+
title: answers.title,
|
|
157
|
+
document: "book.yaml",
|
|
158
|
+
out: answers.contentPackage,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
142
162
|
config.site = {
|
|
143
163
|
...(answers.kind === "documentation" ? { title: answers.title } : {}),
|
|
144
164
|
assets: "https://cdn.heroiclands.org",
|
|
@@ -154,7 +174,6 @@ function projectConfig(answers) {
|
|
|
154
174
|
manifest: {
|
|
155
175
|
title: answers.title,
|
|
156
176
|
descriptionHtml: answers.description,
|
|
157
|
-
authors: [{ name: answers.author }],
|
|
158
177
|
license: "LICENSE.md",
|
|
159
178
|
readme: "README.md",
|
|
160
179
|
languages: [{ lang: "en", name: "English", path: "lang/en.json" }],
|
|
@@ -451,10 +470,12 @@ export function checkProject(rootDir) {
|
|
|
451
470
|
}
|
|
452
471
|
}
|
|
453
472
|
let config;
|
|
473
|
+
let configSource = "";
|
|
454
474
|
if (names.length === 1 && names[0] !== "package-build.config.mjs") {
|
|
455
475
|
const file = names[0];
|
|
456
476
|
try {
|
|
457
477
|
const source = fs.readFileSync(path.join(root, file), "utf8");
|
|
478
|
+
configSource = source;
|
|
458
479
|
config = YAML.parse(source);
|
|
459
480
|
configFromData(config, path.join(root, file));
|
|
460
481
|
} catch (err) {
|
|
@@ -465,10 +486,29 @@ export function checkProject(rootDir) {
|
|
|
465
486
|
if (!nonempty(config.site?.assets) || !nonempty(config.site?.description)) {
|
|
466
487
|
add(names[0], "site.assets and site.description are required for the website");
|
|
467
488
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
489
|
+
const books = config.pdf?.books;
|
|
490
|
+
if (!Array.isArray(books) || books.length === 0) {
|
|
491
|
+
add(names[0], "pdf.books is required for the book");
|
|
492
|
+
} else {
|
|
493
|
+
books.forEach((book, i) => {
|
|
494
|
+
for (const [keyPath, file, what] of [
|
|
495
|
+
[["document"], book?.document, "document tree"],
|
|
496
|
+
[["coverImage"], book?.coverImage, "cover image"],
|
|
497
|
+
...(Array.isArray(book?.front) ?
|
|
498
|
+
book.front.map((entry, j) => [["front", j], entry, "front-matter file"])
|
|
499
|
+
: []),
|
|
500
|
+
]) {
|
|
501
|
+
if (nonempty(file) && !fs.existsSync(path.join(root, file))) {
|
|
502
|
+
findings.push({
|
|
503
|
+
file: names[0],
|
|
504
|
+
...positionOfYamlPath(configSource, ["pdf", "books", i, ...keyPath], {
|
|
505
|
+
key: true,
|
|
506
|
+
}),
|
|
507
|
+
message: `book \`${book.id}\`: the ${what} \`${file}\` does not exist`,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
});
|
|
472
512
|
}
|
|
473
513
|
if (config.packageKind !== "documentation") {
|
|
474
514
|
if (!nonempty(config.packageBuild?.manifest?.title)) {
|
package/engine/prose-config.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* invokes it.
|
|
26
26
|
*
|
|
27
27
|
* **Neither of these is an override.** A consumer that declares its own
|
|
28
|
-
* Prettier config or its own `.markdownlint-cli2
|
|
28
|
+
* Prettier config or its own `.markdownlint-cli2.*` options file wins; what ships here
|
|
29
29
|
* is the default a repository gets for declaring nothing. Repository *layout*
|
|
30
30
|
* knowledge — which paths to skip — stays with the repository that has the
|
|
31
31
|
* layout, in its own ignore files.
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
* shared configuration *as a default a local one overrides*, and reporting
|
|
22
22
|
* findings in the one parseable form every check in this package emits.
|
|
23
23
|
*
|
|
24
|
-
* **
|
|
25
|
-
* it formats and what an ignore file excludes, through `getFileInfo`;
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* **Prettier's own file discovery is not reimplemented.** Prettier decides what
|
|
25
|
+
* it formats and what an ignore file excludes, through `getFileInfo`; a second
|
|
26
|
+
* implementation would drift from the tool it stands in for, and the whole point
|
|
27
|
+
* of the command is that `package-build format --check` and a bare
|
|
28
|
+
* `prettier --check .` report the same thing. Markdown linting walks the tree
|
|
29
|
+
* with `./markdownlint-run.mjs`, which applies
|
|
30
|
+
* `.gitignore` files and the configured globs itself.
|
|
30
31
|
*
|
|
31
32
|
* @module
|
|
32
33
|
*/
|
|
@@ -42,17 +43,8 @@ import {
|
|
|
42
43
|
sharedPrettierDivergence,
|
|
43
44
|
sharedPrettierOptionsFor,
|
|
44
45
|
} from "./prose-config.mjs";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* Directories never walked, whatever the ignore files say.
|
|
48
|
-
*
|
|
49
|
-
* `node_modules` is in every consumer's `.gitignore` and would be excluded
|
|
50
|
-
* anyway; skipping it in the walk rather than per file is the difference
|
|
51
|
-
* between a check that takes a second and one that stats a hundred thousand
|
|
52
|
-
* files to be told each is ignored. `.git` is in nobody's `.gitignore`, so it
|
|
53
|
-
* has to be named.
|
|
54
|
-
*/
|
|
55
|
-
const NEVER_WALK = Object.freeze(new Set([".git", "node_modules"]));
|
|
46
|
+
import { MarkdownConfigError, runMarkdownlint } from "./markdownlint-run.mjs";
|
|
47
|
+
import { walkFiles } from "./walk-files.mjs";
|
|
56
48
|
|
|
57
49
|
/**
|
|
58
50
|
* The ignore files Prettier consults, in its own default order.
|
|
@@ -101,37 +93,6 @@ const CONVENTION_PROBES = Object.freeze({ code: "index.mjs", markdown: "README.m
|
|
|
101
93
|
/** The one line a consumer writes to adopt the shared configuration verbatim. */
|
|
102
94
|
const SHARED_CONFIG_RE_EXPORT = 'export { default } from "@heroiclands/package-build/prettier";';
|
|
103
95
|
|
|
104
|
-
/**
|
|
105
|
-
* Every file under a root, minus the directories nothing should walk.
|
|
106
|
-
*
|
|
107
|
-
* @param {string} root - Absolute path to walk.
|
|
108
|
-
* @returns {string[]} Absolute file paths, in directory order.
|
|
109
|
-
*/
|
|
110
|
-
function walkFiles(root) {
|
|
111
|
-
const out = [];
|
|
112
|
-
/** @param {string} dir - Directory to descend into. */
|
|
113
|
-
const descend = (dir) => {
|
|
114
|
-
let entries;
|
|
115
|
-
try {
|
|
116
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
117
|
-
} catch {
|
|
118
|
-
// A directory that vanished or cannot be read is not a finding
|
|
119
|
-
// about prose; leave it to whatever owns it.
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
const abs = path.join(dir, entry.name);
|
|
124
|
-
if (entry.isDirectory()) {
|
|
125
|
-
if (!NEVER_WALK.has(entry.name)) descend(abs);
|
|
126
|
-
} else if (entry.isFile()) {
|
|
127
|
-
out.push(abs);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
descend(root);
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
96
|
/**
|
|
136
97
|
* Check — or rewrite — every file Prettier claims, under one root.
|
|
137
98
|
*
|
|
@@ -408,10 +369,10 @@ function toDiagnostic(directory, result) {
|
|
|
408
369
|
/**
|
|
409
370
|
* Lint a repository's markdown against the shared rule set.
|
|
410
371
|
*
|
|
411
|
-
* {@link MARKDOWNLINT_CONFIG} is
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
372
|
+
* {@link MARKDOWNLINT_CONFIG} is the default rule set, which is precisely the
|
|
373
|
+
* "shipped default, consumer overrides" behaviour the command promises: a
|
|
374
|
+
* repository with no configuration of its own gets these rules, and a
|
|
375
|
+
* `.markdownlint-cli2.*` options file found in the tree overrides them.
|
|
415
376
|
*
|
|
416
377
|
* The override is **key by key, and each key wholesale** — which is not the same
|
|
417
378
|
* as "replaces it", and the difference is the one worth stating. A consumer file
|
|
@@ -419,55 +380,58 @@ function toDiagnostic(directory, result) {
|
|
|
419
380
|
* and every per-rule option; but its `ignores` *replaces*
|
|
420
381
|
* {@link MARKDOWN_IGNORES} rather than extending it, so such a file must restate
|
|
421
382
|
* every shared entry it still wants. Omitting `CHANGELOG.md` there silently
|
|
422
|
-
* starts linting a generated file. `node_modules` stays excluded
|
|
423
|
-
*
|
|
383
|
+
* starts linting a generated file. `node_modules` stays excluded even when a
|
|
384
|
+
* consumer replaces `ignores`.
|
|
385
|
+
*
|
|
386
|
+
* A configuration file the contract refuses (an unknown option, a key that
|
|
387
|
+
* cannot mean anything where it is written, a syntax error) is returned as a
|
|
388
|
+
* finding located in that file, and nothing is linted.
|
|
424
389
|
*
|
|
425
390
|
* @param {string} root - Repository to lint.
|
|
426
391
|
* @param {object} [opts]
|
|
427
392
|
* @param {readonly string[]} [opts.paths] - Globs to lint instead of every
|
|
428
393
|
* markdown file.
|
|
429
394
|
* @param {boolean} [opts.fix=false] - Apply the fixes markdownlint can make.
|
|
430
|
-
* @param {(params: object) => Promise<number>} [opts.run] - The
|
|
431
|
-
* markdownlint-cli2 entry point, for tests.
|
|
432
395
|
* @returns {Promise<{findings: object[], exitCode: number}>} The findings, and
|
|
433
|
-
*
|
|
396
|
+
* an exit code that is 1 when there are any.
|
|
434
397
|
*/
|
|
435
398
|
export async function lintMarkdown(root, opts = {}) {
|
|
436
399
|
const { paths, fix = false } = opts;
|
|
437
|
-
const run = opts.run ?? (await import("markdownlint-cli2")).main;
|
|
438
400
|
const directory = path.resolve(root);
|
|
439
401
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
402
|
+
let results;
|
|
403
|
+
try {
|
|
404
|
+
({ results } = await runMarkdownlint({
|
|
405
|
+
directory,
|
|
406
|
+
globs: paths,
|
|
407
|
+
fix,
|
|
408
|
+
defaults: {
|
|
409
|
+
config: MARKDOWNLINT_CONFIG,
|
|
410
|
+
// Generated markdown reports on its generator, not its author,
|
|
411
|
+
// and every consumer already lists its generated trees here.
|
|
412
|
+
gitignore: true,
|
|
413
|
+
globs: MARKDOWN_GLOBS,
|
|
414
|
+
ignores: [...MARKDOWN_IGNORES],
|
|
415
|
+
},
|
|
416
|
+
}));
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (!(err instanceof MarkdownConfigError)) throw err;
|
|
419
|
+
return {
|
|
420
|
+
findings: [
|
|
421
|
+
{
|
|
422
|
+
file: err.file,
|
|
423
|
+
...(err.line ? { line: err.line } : {}),
|
|
424
|
+
...(err.line && err.column ? { column: err.column } : {}),
|
|
425
|
+
severity: "error",
|
|
426
|
+
message: err.message,
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
exitCode: 1,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
468
432
|
|
|
469
433
|
return {
|
|
470
|
-
findings:
|
|
471
|
-
exitCode,
|
|
434
|
+
findings: results.map((result) => toDiagnostic(directory, result)),
|
|
435
|
+
exitCode: results.length ? 1 : 0,
|
|
472
436
|
};
|
|
473
437
|
}
|