@heroiclands/package-build 23.1.1 → 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 +63 -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 +32 -0
- 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/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 +21 -14
- 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/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 +8 -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/content-config.mjs
CHANGED
|
@@ -197,36 +197,6 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
|
|
|
197
197
|
"Scene",
|
|
198
198
|
]);
|
|
199
199
|
|
|
200
|
-
/**
|
|
201
|
-
* Address-scheme keys a configuration may no longer declare.
|
|
202
|
-
*
|
|
203
|
-
* A retired key has exactly two possible fates, and only one of them is honest
|
|
204
|
-
* — the same reasoning `engine/retired-fields.mjs` applies to a retired
|
|
205
|
-
* frontmatter field. Left honoured, it keeps doing whatever it did, which is
|
|
206
|
-
* why it was retired. Left *ignored*, it reads to its author as though it still
|
|
207
|
-
* works: the configuration says one thing and the build does another, and
|
|
208
|
-
* nothing says so. This module has no third option, because it has no warning
|
|
209
|
-
* channel — every finding goes through `fail()`, which throws. So a retired
|
|
210
|
-
* key is **refused**, at the line it was written on, with a message that says
|
|
211
|
-
* the mechanism is gone rather than naming a value to correct.
|
|
212
|
-
*
|
|
213
|
-
* **`landing` is one such key.** It named which note addressed a whole section
|
|
214
|
-
* rather than a page within one — a *landing page*, which therefore had no slug
|
|
215
|
-
* of its own. There are no sections to address: a section is a Hugo content
|
|
216
|
-
* directory the note format does not carry, a page's address names no
|
|
217
|
-
* directory, and so no note lands anything.
|
|
218
|
-
*
|
|
219
|
-
* @type {Readonly<Record<string, string>>}
|
|
220
|
-
*/
|
|
221
|
-
export const RETIRED_ADDRESS_KEYS = Object.freeze({
|
|
222
|
-
landing:
|
|
223
|
-
"is a retired option — delete it. It named which note addressed a " +
|
|
224
|
-
"whole section rather than a page within one, and there are no " +
|
|
225
|
-
"sections to address: a section is a Hugo content directory the note " +
|
|
226
|
-
"format does not carry, so no note lands one and every page is " +
|
|
227
|
-
"addressed `<type>-<shortcode>`. Nothing replaces it",
|
|
228
|
-
});
|
|
229
|
-
|
|
230
200
|
/**
|
|
231
201
|
* A repository's address scheme, with the defaults an unconfigured one gets.
|
|
232
202
|
*
|
|
@@ -237,9 +207,7 @@ export const RETIRED_ADDRESS_KEYS = Object.freeze({
|
|
|
237
207
|
* knowledge, held in `PACKAGE_BASE` (`engine/content-address.mjs`) and prefixed at
|
|
238
208
|
* resolve time, so it is never recorded here.
|
|
239
209
|
*
|
|
240
|
-
* It is the whole scheme
|
|
241
|
-
* whole section, is retired with the sections themselves — see
|
|
242
|
-
* {@link RETIRED_ADDRESS_KEYS}.
|
|
210
|
+
* It is the whole scheme.
|
|
243
211
|
*/
|
|
244
212
|
export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
245
213
|
prefix: "",
|
|
@@ -581,6 +549,18 @@ export function publishesContentPages(config) {
|
|
|
581
549
|
* @property {Record<string, ItemBuilderEntry>} builders The registry itself.
|
|
582
550
|
*/
|
|
583
551
|
|
|
552
|
+
/**
|
|
553
|
+
* One person in the top-level `authors` list.
|
|
554
|
+
*
|
|
555
|
+
* @typedef {object} AuthorSpec
|
|
556
|
+
* @property {string} name As printed.
|
|
557
|
+
* @property {string} [email]
|
|
558
|
+
* @property {string} [url]
|
|
559
|
+
* @property {string} [discord] A Discord handle, as Foundry's author schema
|
|
560
|
+
* carries one.
|
|
561
|
+
* @property {string} [role] One of {@link AUTHOR_ROLES}.
|
|
562
|
+
*/
|
|
563
|
+
|
|
584
564
|
/**
|
|
585
565
|
* The configuration a consumer writes.
|
|
586
566
|
*
|
|
@@ -601,9 +581,10 @@ export function publishesContentPages(config) {
|
|
|
601
581
|
* the site build's `baseURL`. Checked
|
|
602
582
|
* by `checkHomepage` in
|
|
603
583
|
* `config.mjs`.
|
|
604
|
-
* @property {
|
|
605
|
-
*
|
|
606
|
-
*
|
|
584
|
+
* @property {AuthorSpec[]} [authors] The people who made the
|
|
585
|
+
* package. The Foundry manifest's
|
|
586
|
+
* `authors` and the site's byline
|
|
587
|
+
* are generated from it.
|
|
607
588
|
* @property {PackageKind} packageKind Whether the package is a system, a
|
|
608
589
|
* module, or documentation — the kind
|
|
609
590
|
* that publishes a site and a book
|
|
@@ -669,10 +650,13 @@ export function publishesContentPages(config) {
|
|
|
669
650
|
* @property {string|null} homepage `package.json`'s own `homepage`,
|
|
670
651
|
* checked by `checkHomepage` in
|
|
671
652
|
* `config.mjs`.
|
|
653
|
+
* @property {readonly Readonly<AuthorSpec>[]} authors The people who made
|
|
654
|
+
* the package, in the order declared; `[]`
|
|
655
|
+
* when none are.
|
|
672
656
|
* @property {Readonly<{name: string, email?: string, url?: string}>|null} author
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
657
|
+
* Derived: the first of `authors`, in npm's
|
|
658
|
+
* `author` shape — the site's byline.
|
|
659
|
+
* `null` when no author is declared.
|
|
676
660
|
* @property {PackageKind} packageKind
|
|
677
661
|
* @property {string|null} assetRoot Derived, and **conditional**: the served
|
|
678
662
|
* Foundry asset root,
|
|
@@ -744,7 +728,7 @@ const CONFIG_KEYS = [
|
|
|
744
728
|
"contentPackage",
|
|
745
729
|
"foundryPackage",
|
|
746
730
|
"homepage",
|
|
747
|
-
"
|
|
731
|
+
"authors",
|
|
748
732
|
"packageKind",
|
|
749
733
|
"stats",
|
|
750
734
|
"itemBuilders",
|
|
@@ -783,7 +767,42 @@ const SITE_KEYS = [
|
|
|
783
767
|
];
|
|
784
768
|
const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
|
|
785
769
|
const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
|
|
786
|
-
const PDF_KEYS = [
|
|
770
|
+
const PDF_KEYS = [
|
|
771
|
+
"books",
|
|
772
|
+
"out",
|
|
773
|
+
"fonts",
|
|
774
|
+
"iconFonts",
|
|
775
|
+
"binary",
|
|
776
|
+
"unprinted",
|
|
777
|
+
"coverage",
|
|
778
|
+
"indexSeeAlso",
|
|
779
|
+
"copyright",
|
|
780
|
+
"license",
|
|
781
|
+
];
|
|
782
|
+
const PDF_BOOK_KEYS = [
|
|
783
|
+
"id",
|
|
784
|
+
"title",
|
|
785
|
+
"subtitle",
|
|
786
|
+
"coverImage",
|
|
787
|
+
"front",
|
|
788
|
+
"out",
|
|
789
|
+
"contents",
|
|
790
|
+
"document",
|
|
791
|
+
];
|
|
792
|
+
/**
|
|
793
|
+
* How a printable note no book selects is reported: `pdf.coverage`.
|
|
794
|
+
*
|
|
795
|
+
* @type {readonly string[]}
|
|
796
|
+
*/
|
|
797
|
+
const PDF_COVERAGE = Object.freeze(["warning", "error", "off"]);
|
|
798
|
+
/**
|
|
799
|
+
* What a book `id` may be. It is written on the command line as `--book <id>`
|
|
800
|
+
* and is the tail of the book's default file stem, so it takes the charset a
|
|
801
|
+
* file name and an address segment share.
|
|
802
|
+
*/
|
|
803
|
+
const PDF_BOOK_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
804
|
+
/** What a book's file stem may be: one path segment, no directory. */
|
|
805
|
+
const PDF_BOOK_STEM = /^[\w.-]+$/;
|
|
787
806
|
const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
|
|
788
807
|
const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
|
|
789
808
|
const DOC_PAGE_KEYS = ["title", "out", "preamble", "frontmatter"];
|
|
@@ -808,7 +827,28 @@ export const RELATIONSHIP_KEYS = [
|
|
|
808
827
|
"contentIndex",
|
|
809
828
|
"assetReplacement",
|
|
810
829
|
];
|
|
811
|
-
const AUTHOR_KEYS = ["name", "email", "url"];
|
|
830
|
+
const AUTHOR_KEYS = ["name", "email", "url", "discord", "role"];
|
|
831
|
+
/**
|
|
832
|
+
* The closed list a person's `role` in `authors` is drawn from.
|
|
833
|
+
*
|
|
834
|
+
* One role per entry: a person with two roles is listed twice. Printed in
|
|
835
|
+
* title case after the name on a book's title page; the Foundry manifest,
|
|
836
|
+
* whose author schema has no role, never receives it.
|
|
837
|
+
*
|
|
838
|
+
* @type {readonly string[]}
|
|
839
|
+
*/
|
|
840
|
+
export const AUTHOR_ROLES = Object.freeze([
|
|
841
|
+
"author",
|
|
842
|
+
"designer",
|
|
843
|
+
"developer",
|
|
844
|
+
"editor",
|
|
845
|
+
"proofreader",
|
|
846
|
+
"artist",
|
|
847
|
+
"cartographer",
|
|
848
|
+
"layout",
|
|
849
|
+
"playtester",
|
|
850
|
+
"translator",
|
|
851
|
+
]);
|
|
812
852
|
const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
|
|
813
853
|
const ITEM_REGISTRY_KEYS = ["system", "builders"];
|
|
814
854
|
const PACK_KEYS = [
|
|
@@ -1029,52 +1069,60 @@ function optionalString(value, field) {
|
|
|
1029
1069
|
}
|
|
1030
1070
|
|
|
1031
1071
|
/**
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
|
|
1041
|
-
|
|
1072
|
+
* The people who made the package: the top-level `authors` list.
|
|
1073
|
+
*
|
|
1074
|
+
* The one place a package names its people. The Foundry manifest's `authors`
|
|
1075
|
+
* is generated from it with `role` dropped, because Foundry's author schema
|
|
1076
|
+
* has none; the byline the site build reads is its first entry's name, email
|
|
1077
|
+
* and URL. Each entry is `{name, email?, url?, discord?, role?}`, and `role`
|
|
1078
|
+
* is drawn from {@link AUTHOR_ROLES}.
|
|
1079
|
+
*
|
|
1080
|
+
* @param {unknown} value - The declared `authors`, or `undefined`.
|
|
1081
|
+
* @returns {readonly Readonly<AuthorSpec>[]} The entries, frozen; `[]` when
|
|
1082
|
+
* none are declared.
|
|
1083
|
+
*/
|
|
1084
|
+
function normalizeAuthors(value) {
|
|
1085
|
+
if (value === undefined) return Object.freeze([]);
|
|
1086
|
+
if (!Array.isArray(value)) {
|
|
1087
|
+
fail("authors", "must be a list of people, each a mapping with at least a `name`");
|
|
1088
|
+
}
|
|
1089
|
+
return Object.freeze(
|
|
1090
|
+
value.map((entry, i) => {
|
|
1091
|
+
const where = `authors[${i}]`;
|
|
1092
|
+
if (!isPlainObject(entry)) fail(where, "must be a mapping with at least a `name`");
|
|
1093
|
+
const person = /** @type {Record<string, unknown>} */ (entry);
|
|
1094
|
+
rejectUnknownKeys(person, AUTHOR_KEYS, `${where}.`);
|
|
1095
|
+
const out = { name: requireNonEmptyString(person.name, `${where}.name`) };
|
|
1096
|
+
for (const key of ["email", "url", "discord"]) {
|
|
1097
|
+
if (person[key] !== undefined) {
|
|
1098
|
+
out[key] = requireNonEmptyString(person[key], `${where}.${key}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (person.role !== undefined) {
|
|
1102
|
+
if (typeof person.role !== "string" || !AUTHOR_ROLES.includes(person.role)) {
|
|
1103
|
+
fail(`${where}.role`, `must be one of: ${AUTHOR_ROLES.join(", ")}`);
|
|
1104
|
+
}
|
|
1105
|
+
out.role = person.role;
|
|
1106
|
+
}
|
|
1107
|
+
return Object.freeze(out);
|
|
1108
|
+
}),
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1042
1111
|
|
|
1043
1112
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1113
|
+
* The package's byline: the first of `authors`, as npm's `author` shape.
|
|
1114
|
+
*
|
|
1115
|
+
* @param {readonly Readonly<AuthorSpec>[]} authors - From {@link normalizeAuthors}.
|
|
1116
|
+
* @returns {Readonly<{name: string, email?: string, url?: string}>|null} `null`
|
|
1117
|
+
* when no author is declared.
|
|
1047
1118
|
*/
|
|
1048
|
-
function
|
|
1049
|
-
|
|
1050
|
-
if (
|
|
1051
|
-
const match = AUTHOR_STRING.exec(value.trim());
|
|
1052
|
-
const name = match?.[1]?.trim();
|
|
1053
|
-
if (!match || !name) {
|
|
1054
|
-
fail(
|
|
1055
|
-
"author",
|
|
1056
|
-
'must be `"Name"`, `"Name <email>"`, `"Name (url)"` or ' +
|
|
1057
|
-
'`"Name <email> (url)"` — npm\'s own `author` forms',
|
|
1058
|
-
);
|
|
1059
|
-
}
|
|
1060
|
-
return Object.freeze({
|
|
1061
|
-
name: /** @type {string} */ (name),
|
|
1062
|
-
...(match[2] ? { email: match[2] } : {}),
|
|
1063
|
-
...(match[3] ? { url: match[3] } : {}),
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
if (!isPlainObject(value)) {
|
|
1067
|
-
fail("author", "must be a string or an object with `name`, `email` and `url`");
|
|
1068
|
-
}
|
|
1069
|
-
const author = /** @type {Record<string, unknown>} */ (value);
|
|
1070
|
-
rejectUnknownKeys(author, AUTHOR_KEYS, "author.");
|
|
1071
|
-
const name = requireNonEmptyString(author.name, "author.name");
|
|
1119
|
+
function bylineOf(authors) {
|
|
1120
|
+
const [first] = authors;
|
|
1121
|
+
if (!first) return null;
|
|
1072
1122
|
return Object.freeze({
|
|
1073
|
-
name,
|
|
1074
|
-
...(
|
|
1075
|
-
|
|
1076
|
-
: {}),
|
|
1077
|
-
...(author.url !== undefined ? { url: optionalString(author.url, "author.url") } : {}),
|
|
1123
|
+
name: first.name,
|
|
1124
|
+
...(first.email !== undefined ? { email: first.email } : {}),
|
|
1125
|
+
...(first.url !== undefined ? { url: first.url } : {}),
|
|
1078
1126
|
});
|
|
1079
1127
|
}
|
|
1080
1128
|
|
|
@@ -1088,19 +1136,6 @@ function normalizeAuthor(value) {
|
|
|
1088
1136
|
function normalizePack(value, where, nested = false) {
|
|
1089
1137
|
if (!isPlainObject(value)) fail(where, "must be an object");
|
|
1090
1138
|
const pack = /** @type {Record<string, unknown>} */ (value);
|
|
1091
|
-
// Retired with the YAML it named. Refused explicitly rather than
|
|
1092
|
-
// left to the unknown-key check, because the useful thing to say is not
|
|
1093
|
-
// "no such key" but where the folders went: they are notes, and a pack
|
|
1094
|
-
// materialises the ones its documents reference.
|
|
1095
|
-
if (pack.folders !== undefined) {
|
|
1096
|
-
fail(
|
|
1097
|
-
`${where}.folders`,
|
|
1098
|
-
"is retired — delete it. A folder is a note (`type: folder`) now, " +
|
|
1099
|
-
"and a pack materialises the folders its documents reference " +
|
|
1100
|
-
"through `packFolder`, so there is no per-pack hierarchy file " +
|
|
1101
|
-
"to name",
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
1139
|
rejectUnknownKeys(pack, PACK_KEYS, `${where}.`);
|
|
1105
1140
|
|
|
1106
1141
|
const name = requireNonEmptyString(pack.name, `${where}.name`);
|
|
@@ -1609,7 +1644,7 @@ export const DERIVED_HUGO_KEYS = Object.freeze({
|
|
|
1609
1644
|
"the toolchain, which renders a site as its homepage and its pages: no " +
|
|
1610
1645
|
"output format is declared, because every listing kind is disabled",
|
|
1611
1646
|
"params.description": "`site.description`",
|
|
1612
|
-
"params.author": "
|
|
1647
|
+
"params.author": "the first entry of `authors`",
|
|
1613
1648
|
"params.cdnBaseURL": "`site.assets`",
|
|
1614
1649
|
"params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
|
|
1615
1650
|
"params.notfound": "`site.notfound`",
|
|
@@ -1695,38 +1730,6 @@ function normalizeSiteHugo(value) {
|
|
|
1695
1730
|
return Object.freeze(structuredClone(input));
|
|
1696
1731
|
}
|
|
1697
1732
|
|
|
1698
|
-
/**
|
|
1699
|
-
* The keys that asked the site build to write an index between the homepage
|
|
1700
|
-
* and the pages, each refused with the one message.
|
|
1701
|
-
*
|
|
1702
|
-
* A site is its homepage and its pages. Each of these keys asks for a
|
|
1703
|
-
* generated listing between them — `sections` (with `listType` and
|
|
1704
|
-
* `listSubType` inside an entry) a type-wide alphabetical one per entry,
|
|
1705
|
-
* `backfillSections` one for every other directory under the mount, `landing`
|
|
1706
|
-
* the mount's own, `list` how such a listing renders. None of those is a page
|
|
1707
|
-
* anyone chose the contents of, and every one of them is authored instead: a
|
|
1708
|
-
* `doc` note, addressed by its shortcode, carrying a content table over the
|
|
1709
|
-
* notes it introduces, with the columns it chooses.
|
|
1710
|
-
*
|
|
1711
|
-
* Refused rather than ignored, for the reason {@link RETIRED_ADDRESS_KEYS}
|
|
1712
|
-
* gives: a key left ignored reads to its author as though it still works.
|
|
1713
|
-
*
|
|
1714
|
-
* @type {readonly string[]}
|
|
1715
|
-
*/
|
|
1716
|
-
const SITE_INDEX_KEYS = Object.freeze(["sections", "landing", "backfillSections", "list"]);
|
|
1717
|
-
|
|
1718
|
-
/**
|
|
1719
|
-
* The message every key in {@link SITE_INDEX_KEYS} is refused with.
|
|
1720
|
-
*
|
|
1721
|
-
* @type {string}
|
|
1722
|
-
*/
|
|
1723
|
-
const SITE_INDEX_MESSAGE =
|
|
1724
|
-
"is retired — a site is its homepage and its pages, and any index between " +
|
|
1725
|
-
"them is a `doc` note: write one with `type: doc`, a `shortcode` and " +
|
|
1726
|
-
"`pack: none`, carrying a content table over the notes it lists, and link " +
|
|
1727
|
-
"it from the homepage. Nothing is generated between the homepage and the " +
|
|
1728
|
-
"pages, so delete the key";
|
|
1729
|
-
|
|
1730
1733
|
/**
|
|
1731
1734
|
* The `site` section — how this repository frames the website it publishes.
|
|
1732
1735
|
*
|
|
@@ -1734,7 +1737,7 @@ const SITE_INDEX_MESSAGE =
|
|
|
1734
1737
|
* repository's own body rewrites, and the residue of the generated Hugo
|
|
1735
1738
|
* configuration that is genuinely this repository's own. Where the Hugo tree
|
|
1736
1739
|
* is written is not a choice: `package-build site` writes it under
|
|
1737
|
-
* `build/hugo
|
|
1740
|
+
* `build/hugo/`. How a page gets its
|
|
1738
1741
|
* **address** is deliberately not here either — that is `publish.address`,
|
|
1739
1742
|
* shared with the link manifest so the two cannot disagree about where a
|
|
1740
1743
|
* page is.
|
|
@@ -1748,12 +1751,8 @@ const SITE_INDEX_MESSAGE =
|
|
|
1748
1751
|
* **What the site publishes is the homepage and the content tree, and nothing
|
|
1749
1752
|
* beside them.** A page of documentation is a note — `type: doc`, addressed
|
|
1750
1753
|
* by its shortcode, compiling into no document where it says `pack: none` —
|
|
1751
|
-
* so there is no second mechanism for mounting a directory of markdown
|
|
1752
|
-
*
|
|
1753
|
-
* that titled such a tree's landing) is refused with a message saying where
|
|
1754
|
-
* the page goes instead. An index between the homepage and the pages is a
|
|
1755
|
-
* `doc` note too, so the keys that asked the build to generate one
|
|
1756
|
-
* ({@link SITE_INDEX_KEYS}) are refused the same way.
|
|
1754
|
+
* so there is no second mechanism for mounting a directory of markdown. An
|
|
1755
|
+
* index between the homepage and the pages is a `doc` note too.
|
|
1757
1756
|
*
|
|
1758
1757
|
* @param {unknown} value - The `site` block, or `undefined`.
|
|
1759
1758
|
* @returns {Readonly<object>} It, frozen, with every default filled.
|
|
@@ -1775,35 +1774,6 @@ function normalizeSite(value) {
|
|
|
1775
1774
|
if (value === undefined) return empty;
|
|
1776
1775
|
if (!isPlainObject(value)) fail("site", "must be a mapping");
|
|
1777
1776
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1778
|
-
// Refused by name, ahead of the vocabulary check: the useful thing to say
|
|
1779
|
-
// is not "no such key" but that the location is fixed.
|
|
1780
|
-
if (input.out !== undefined) {
|
|
1781
|
-
fail(
|
|
1782
|
-
"site.out",
|
|
1783
|
-
"is retired — the site build writes its content mount at " +
|
|
1784
|
-
"`build/hugo/content`, beside the generated `hugo.toml`, and " +
|
|
1785
|
-
"the location is not configurable. Remove the key",
|
|
1786
|
-
);
|
|
1787
|
-
}
|
|
1788
|
-
// Refused by name too, and ahead of the vocabulary check for the same
|
|
1789
|
-
// reason: a page is a note in the content tree, and the useful thing to
|
|
1790
|
-
// say is where it goes rather than that the key is unknown.
|
|
1791
|
-
for (const key of ["trees", "readmeSections"]) {
|
|
1792
|
-
if (input[key] === undefined) continue;
|
|
1793
|
-
fail(
|
|
1794
|
-
`site.${key}`,
|
|
1795
|
-
"is retired — a page is a note in the content tree. Give each page " +
|
|
1796
|
-
"`type: doc`, a `shortcode` and `pack: none`, file it under " +
|
|
1797
|
-
"`assets/content/`, and link it from the homepage or from a " +
|
|
1798
|
-
"`doc` note that indexes it",
|
|
1799
|
-
);
|
|
1800
|
-
}
|
|
1801
|
-
// And by name, for the same reason: an index between the homepage and
|
|
1802
|
-
// the pages is a `doc` note, and the useful thing to say is that.
|
|
1803
|
-
for (const key of SITE_INDEX_KEYS) {
|
|
1804
|
-
if (input[key] === undefined) continue;
|
|
1805
|
-
fail(`site.${key}`, SITE_INDEX_MESSAGE);
|
|
1806
|
-
}
|
|
1807
1777
|
rejectUnknownKeys(input, SITE_KEYS, "site.");
|
|
1808
1778
|
|
|
1809
1779
|
let packages = [];
|
|
@@ -1833,46 +1803,65 @@ function normalizeSite(value) {
|
|
|
1833
1803
|
}
|
|
1834
1804
|
|
|
1835
1805
|
/**
|
|
1836
|
-
*
|
|
1806
|
+
* One declared book, resolved.
|
|
1837
1807
|
*
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1808
|
+
* @typedef {object} ResolvedPdfBook
|
|
1809
|
+
* @property {string} id Its name on the command line, `--book <id>`.
|
|
1810
|
+
* @property {string} title
|
|
1811
|
+
* @property {string} subtitle `""` when none is declared.
|
|
1812
|
+
* @property {string} coverImage Absolute; `""` when none is declared.
|
|
1813
|
+
* Whether the file exists is checked when the
|
|
1814
|
+
* book is built, not here.
|
|
1815
|
+
* @property {readonly string[]} front Absolute paths of the front matter.
|
|
1816
|
+
* @property {string} out The file stem; the version is appended.
|
|
1817
|
+
* @property {readonly object[]|null} contents The tree written inline, or
|
|
1818
|
+
* `null` when `document` names a file.
|
|
1819
|
+
* @property {string} document Absolute path of the tree's file, or `""`
|
|
1820
|
+
* when the tree is written inline.
|
|
1821
|
+
*/
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* The `pdf` section — the books the content tree is published as.
|
|
1844
1825
|
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1826
|
+
* A third surface beside the packs and the website, and the one that is a
|
|
1827
|
+
* **selection** rather than a rendering of everything: each book's tree says
|
|
1828
|
+
* which notes the volume carries and in what order, because a book is an
|
|
1829
|
+
* editorial act where a site is an index. A tree is written inline as
|
|
1830
|
+
* `contents:` or kept in the file `document:` names, and is parsed by
|
|
1831
|
+
* {@link module:engine/pdf-toc.parseDocumentTree} when the book is built;
|
|
1832
|
+
* nothing about its shape is validated here.
|
|
1833
|
+
*
|
|
1834
|
+
* `books` is the one required key. The rest of the block — faces, icon fonts,
|
|
1835
|
+
* the compiler, the output directory, what no book prints, how an unprinted
|
|
1836
|
+
* note is reported, and the copyright and licence statements — is shared by
|
|
1837
|
+
* every book. Each book's `out` is a file stem (default
|
|
1838
|
+
* `<contentPackage>-<id>`), and two books resolving to one stem is refused,
|
|
1839
|
+
* since the second would overwrite the first.
|
|
1840
|
+
*
|
|
1841
|
+
* Paths resolve against `rootDir` here; whether `coverImage`, `document` and
|
|
1842
|
+
* each `front` file exist is checked when a book is built, so a command that
|
|
1843
|
+
* builds no book never fails for a missing cover.
|
|
1844
|
+
*
|
|
1845
|
+
* **Nothing here is an address or a brand.** The titles, the front matter and
|
|
1846
|
+
* the faces are every one of them the publishing repository's to choose, which
|
|
1847
|
+
* is the whole reason they are configuration: the engine that sets the book
|
|
1848
|
+
* must be able to set somebody else's book.
|
|
1849
1849
|
*
|
|
1850
1850
|
* A PDF is built when the authored tree contains content pages and the `pdf`
|
|
1851
|
-
* block
|
|
1851
|
+
* block is present. See {@link publishesContentPages}.
|
|
1852
1852
|
*
|
|
1853
1853
|
* @param {unknown} value - The `pdf` block, or `undefined`.
|
|
1854
1854
|
* @param {string} rootDir - The repository root configured paths resolve against.
|
|
1855
|
+
* @param {string} contentPackage - The package name a default stem starts with.
|
|
1855
1856
|
* @returns {Readonly<object>|null} It, frozen; `null` when the block is absent.
|
|
1856
1857
|
*/
|
|
1857
|
-
function normalizePdf(value, rootDir) {
|
|
1858
|
+
function normalizePdf(value, rootDir, contentPackage) {
|
|
1858
1859
|
if (value === undefined) return null;
|
|
1859
1860
|
if (!isPlainObject(value)) fail("pdf", "must be a mapping");
|
|
1860
1861
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1861
1862
|
rejectUnknownKeys(input, PDF_KEYS, "pdf.");
|
|
1862
1863
|
|
|
1863
|
-
|
|
1864
|
-
// to print, and a tree with no title produces a file whose name and cover
|
|
1865
|
-
// say nothing about what a reader downloaded.
|
|
1866
|
-
const title = requireNonEmptyString(input.title, "pdf.title");
|
|
1867
|
-
const document = requireNonEmptyString(input.document, "pdf.document");
|
|
1868
|
-
|
|
1869
|
-
const front = [];
|
|
1870
|
-
if (input.front !== undefined) {
|
|
1871
|
-
if (!Array.isArray(input.front)) fail("pdf.front", "must be a list of markdown files");
|
|
1872
|
-
input.front.forEach((entry, i) => {
|
|
1873
|
-
front.push(requireNonEmptyString(entry, `pdf.front[${i}]`));
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1864
|
+
const books = normalizePdfBooks(input.books, rootDir, contentPackage);
|
|
1876
1865
|
|
|
1877
1866
|
let fonts = EMPTY_PDF_FONTS;
|
|
1878
1867
|
if (input.fonts !== undefined) {
|
|
@@ -1919,15 +1908,21 @@ function normalizePdf(value, rootDir) {
|
|
|
1919
1908
|
}
|
|
1920
1909
|
}
|
|
1921
1910
|
|
|
1911
|
+
const coverage = input.coverage === undefined ? "warning" : input.coverage;
|
|
1912
|
+
if (typeof coverage !== "string" || !PDF_COVERAGE.includes(coverage)) {
|
|
1913
|
+
fail("pdf.coverage", `must be one of: ${PDF_COVERAGE.join(", ")}`);
|
|
1914
|
+
}
|
|
1915
|
+
const copyright =
|
|
1916
|
+
input.copyright === undefined ?
|
|
1917
|
+
""
|
|
1918
|
+
: requireNonEmptyString(input.copyright, "pdf.copyright");
|
|
1919
|
+
if (/[\r\n]/.test(copyright.trim())) {
|
|
1920
|
+
fail("pdf.copyright", "must be one line — the licence statement is the one that runs on");
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1922
1923
|
return Object.freeze({
|
|
1923
|
-
|
|
1924
|
-
subtitle:
|
|
1925
|
-
input.subtitle === undefined ?
|
|
1926
|
-
""
|
|
1927
|
-
: requireNonEmptyString(input.subtitle, "pdf.subtitle"),
|
|
1928
|
-
document: path.resolve(rootDir, document),
|
|
1924
|
+
books,
|
|
1929
1925
|
out: input.out === undefined ? "" : requireNonEmptyString(input.out, "pdf.out"),
|
|
1930
|
-
front: Object.freeze(front.map((f) => path.resolve(rootDir, f))),
|
|
1931
1926
|
fonts,
|
|
1932
1927
|
iconFonts: Object.freeze(iconFonts),
|
|
1933
1928
|
// Where the Typst binary is, when it is not simply `typst` on PATH.
|
|
@@ -1935,9 +1930,142 @@ function normalizePdf(value, rootDir) {
|
|
|
1935
1930
|
// platform-specific binary in the dependency tree of three repositories
|
|
1936
1931
|
// that mostly do not build books.
|
|
1937
1932
|
binary: input.binary === undefined ? "" : requireNonEmptyString(input.binary, "pdf.binary"),
|
|
1933
|
+
// A `WHERE` clause over `notes` naming what belongs in no book.
|
|
1934
|
+
unprinted:
|
|
1935
|
+
input.unprinted === undefined ?
|
|
1936
|
+
""
|
|
1937
|
+
: requireNonEmptyString(input.unprinted, "pdf.unprinted"),
|
|
1938
|
+
coverage,
|
|
1939
|
+
// Whether an index names a note another book prints, as
|
|
1940
|
+
// `Name: Book title, p. N`.
|
|
1941
|
+
indexSeeAlso: optionalBoolean(input.indexSeeAlso, "pdf.indexSeeAlso", false),
|
|
1942
|
+
copyright: copyright.trim(),
|
|
1943
|
+
license:
|
|
1944
|
+
input.license === undefined ?
|
|
1945
|
+
""
|
|
1946
|
+
: requireNonEmptyString(input.license, "pdf.license").trim(),
|
|
1938
1947
|
});
|
|
1939
1948
|
}
|
|
1940
1949
|
|
|
1950
|
+
/**
|
|
1951
|
+
* The `pdf.books` list, each entry resolved.
|
|
1952
|
+
*
|
|
1953
|
+
* @param {unknown} value - The declared `books`.
|
|
1954
|
+
* @param {string} rootDir - What a path resolves against.
|
|
1955
|
+
* @param {string} contentPackage - The head of a default file stem.
|
|
1956
|
+
* @returns {readonly Readonly<ResolvedPdfBook>[]} The books, frozen.
|
|
1957
|
+
*/
|
|
1958
|
+
function normalizePdfBooks(value, rootDir, contentPackage) {
|
|
1959
|
+
if (value === undefined) {
|
|
1960
|
+
fail("pdf.books", "is required — list the books this package publishes, one entry each");
|
|
1961
|
+
}
|
|
1962
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1963
|
+
fail("pdf.books", "must be a non-empty list of books");
|
|
1964
|
+
}
|
|
1965
|
+
const ids = new Set();
|
|
1966
|
+
/** @type {Map<string, number>} Stem → the book that took it. */
|
|
1967
|
+
const stems = new Map();
|
|
1968
|
+
return Object.freeze(
|
|
1969
|
+
value.map((entry, i) => {
|
|
1970
|
+
const where = `pdf.books[${i}]`;
|
|
1971
|
+
if (!isPlainObject(entry)) fail(where, "must be a mapping");
|
|
1972
|
+
const book = /** @type {Record<string, unknown>} */ (entry);
|
|
1973
|
+
rejectUnknownKeys(book, PDF_BOOK_KEYS, `${where}.`);
|
|
1974
|
+
|
|
1975
|
+
const id = requireNonEmptyString(book.id, `${where}.id`);
|
|
1976
|
+
if (!PDF_BOOK_ID.test(id)) {
|
|
1977
|
+
fail(
|
|
1978
|
+
`${where}.id`,
|
|
1979
|
+
`is \`${id}\`, which is not lowercase letters, digits and hyphens ` +
|
|
1980
|
+
`starting with a letter or digit (${PDF_BOOK_ID.source}) — it is ` +
|
|
1981
|
+
"written as `--book <id>` and ends the book's default file name",
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
if (ids.has(id)) fail(`${where}.id`, `is \`${id}\`, which names a book more than once`);
|
|
1985
|
+
ids.add(id);
|
|
1986
|
+
const title = requireNonEmptyString(book.title, `${where}.title`);
|
|
1987
|
+
|
|
1988
|
+
// One tree, from exactly one place: two would leave the reader to
|
|
1989
|
+
// guess which is printed, and none prints nothing.
|
|
1990
|
+
if ((book.contents === undefined) === (book.document === undefined)) {
|
|
1991
|
+
fail(
|
|
1992
|
+
where,
|
|
1993
|
+
"must declare exactly one of `contents` and `document` — the tree " +
|
|
1994
|
+
"written inline, or the file holding it",
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
let contents = null;
|
|
1998
|
+
if (book.contents !== undefined) {
|
|
1999
|
+
if (!Array.isArray(book.contents)) {
|
|
2000
|
+
fail(`${where}.contents`, "must be a list of sections");
|
|
2001
|
+
}
|
|
2002
|
+
contents = Object.freeze(structuredClone(book.contents));
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
const front = [];
|
|
2006
|
+
if (book.front !== undefined) {
|
|
2007
|
+
if (!Array.isArray(book.front)) {
|
|
2008
|
+
fail(`${where}.front`, "must be a list of markdown files");
|
|
2009
|
+
}
|
|
2010
|
+
book.front.forEach((file, j) => {
|
|
2011
|
+
front.push(
|
|
2012
|
+
path.resolve(rootDir, requireNonEmptyString(file, `${where}.front[${j}]`)),
|
|
2013
|
+
);
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
let out = `${contentPackage}-${id}`;
|
|
2018
|
+
let stemAt = `${where}.id`;
|
|
2019
|
+
if (book.out !== undefined) {
|
|
2020
|
+
out = requireNonEmptyString(book.out, `${where}.out`);
|
|
2021
|
+
stemAt = `${where}.out`;
|
|
2022
|
+
if (!PDF_BOOK_STEM.test(out)) {
|
|
2023
|
+
fail(
|
|
2024
|
+
`${where}.out`,
|
|
2025
|
+
`is \`${out}\`, which is not a file stem — letters, digits, ` +
|
|
2026
|
+
"`.`, `_` and `-`, with no directory; the directory is `pdf.out`",
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
const taken = stems.get(out);
|
|
2031
|
+
if (taken !== undefined) {
|
|
2032
|
+
fail(
|
|
2033
|
+
stemAt,
|
|
2034
|
+
`gives the file stem \`${out}\`, which \`pdf.books[${taken}]\` already ` +
|
|
2035
|
+
"uses — the second book would overwrite the first",
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
stems.set(out, i);
|
|
2039
|
+
|
|
2040
|
+
return Object.freeze({
|
|
2041
|
+
id,
|
|
2042
|
+
title,
|
|
2043
|
+
subtitle:
|
|
2044
|
+
book.subtitle === undefined ?
|
|
2045
|
+
""
|
|
2046
|
+
: requireNonEmptyString(book.subtitle, `${where}.subtitle`),
|
|
2047
|
+
coverImage:
|
|
2048
|
+
book.coverImage === undefined ?
|
|
2049
|
+
""
|
|
2050
|
+
: path.resolve(
|
|
2051
|
+
rootDir,
|
|
2052
|
+
requireNonEmptyString(book.coverImage, `${where}.coverImage`),
|
|
2053
|
+
),
|
|
2054
|
+
front: Object.freeze(front),
|
|
2055
|
+
out,
|
|
2056
|
+
contents,
|
|
2057
|
+
document:
|
|
2058
|
+
book.document === undefined ?
|
|
2059
|
+
""
|
|
2060
|
+
: path.resolve(
|
|
2061
|
+
rootDir,
|
|
2062
|
+
requireNonEmptyString(book.document, `${where}.document`),
|
|
2063
|
+
),
|
|
2064
|
+
});
|
|
2065
|
+
}),
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1941
2069
|
/**
|
|
1942
2070
|
* A dotted version string as the three-element tuple Foundry compares.
|
|
1943
2071
|
*
|
|
@@ -2551,15 +2679,6 @@ function normalizePublish(value) {
|
|
|
2551
2679
|
fail("publish.address", "must be an object");
|
|
2552
2680
|
}
|
|
2553
2681
|
const address = /** @type {Record<string, unknown>} */ (addressInput ?? {});
|
|
2554
|
-
// A retired key is refused by name, ahead of the vocabulary check: reported
|
|
2555
|
-
// as merely unrecognized it would read as a misspelling of the one key that
|
|
2556
|
-
// survives, and the author would correct the spelling rather than learn
|
|
2557
|
-
// that the mechanism is gone.
|
|
2558
|
-
for (const key of Object.keys(address)) {
|
|
2559
|
-
if (Object.hasOwn(RETIRED_ADDRESS_KEYS, key)) {
|
|
2560
|
-
fail(`publish.address.${key}`, RETIRED_ADDRESS_KEYS[key]);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
2682
|
rejectUnknownKeys(address, ADDRESS_KEYS, "publish.address.");
|
|
2564
2683
|
|
|
2565
2684
|
const prefix =
|
|
@@ -2799,6 +2918,7 @@ export function defineConfig(config) {
|
|
|
2799
2918
|
new Set([...itemTypes, ...ACTOR_TYPES, "macro", ...MAP_TYPES]),
|
|
2800
2919
|
);
|
|
2801
2920
|
const compatibility = normalizeCompatibility(input.compatibility, "compatibility");
|
|
2921
|
+
const authors = normalizeAuthors(input.authors);
|
|
2802
2922
|
if (!documentation && compatibility === null)
|
|
2803
2923
|
fail("compatibility.minimum", "is required for a Foundry package");
|
|
2804
2924
|
|
|
@@ -2813,7 +2933,8 @@ export function defineConfig(config) {
|
|
|
2813
2933
|
// `checkHomepage` in `config.mjs`.
|
|
2814
2934
|
homepage:
|
|
2815
2935
|
input.homepage === undefined ? null : requireNonEmptyString(input.homepage, "homepage"),
|
|
2816
|
-
|
|
2936
|
+
authors,
|
|
2937
|
+
author: bylineOf(authors),
|
|
2817
2938
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
2818
2939
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
2819
2940
|
// one place `systems/sohl` (or `modules/harn-ensemble`) is spelled.
|
|
@@ -2883,7 +3004,9 @@ export function defineConfig(config) {
|
|
|
2883
3004
|
packDirectories: Object.freeze(packDirectories),
|
|
2884
3005
|
docs: normalizeDocs(input.docs),
|
|
2885
3006
|
site: normalizeSite(input.site),
|
|
2886
|
-
|
|
3007
|
+
// Validated above, in `contentPackage`: a book's default file stem is
|
|
3008
|
+
// `<contentPackage>-<id>`.
|
|
3009
|
+
pdf: normalizePdf(input.pdf, rootDir, /** @type {string} */ (input.contentPackage)),
|
|
2887
3010
|
compatibility,
|
|
2888
3011
|
relationships: normalizeRelationships(input.relationships),
|
|
2889
3012
|
systems,
|