@heroiclands/package-build 22.2.0 → 22.3.1
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 +20 -0
- package/CONTENT.md +27 -32
- package/bin/content-build.mjs +25 -6
- package/content-config.mjs +54 -41
- package/docs/api.md +25 -25
- package/docs/commands.md +23 -0
- package/docs/configuration.md +31 -35
- package/docs/content-format.md +36 -6
- package/engine/field-reference.mjs +135 -0
- package/engine/foundry-entries.mjs +12 -1
- package/engine/frontmatter-lint.mjs +2 -3
- package/engine/helpers.mjs +4 -1
- package/engine/ids.mjs +12 -0
- package/engine/note-claims.mjs +21 -0
- package/engine/pack-router.mjs +82 -5
- package/engine/pdf-build.mjs +2 -2
- package/engine/retired-fields.mjs +2 -3
- package/engine/site-build.mjs +59 -218
- package/engine/site-index.mjs +18 -62
- package/engine/web-wikilinks.mjs +2 -16
- package/engine/wikilinks.mjs +35 -14
- package/package.json +1 -1
- package/sohl/kb-passes.mjs +14 -85
- package/types/content-config.d.mts +14 -6
- package/types/engine/field-reference.d.mts +78 -0
- package/types/engine/ids.d.mts +11 -0
- package/types/engine/pack-router.d.mts +20 -2
- package/types/engine/site-build.d.mts +12 -62
- package/types/engine/site-index.d.mts +3 -28
- package/types/engine/web-wikilinks.d.mts +1 -1
- package/types/engine/wikilinks.d.mts +6 -3
- package/types/sohl/kb-passes.d.mts +5 -36
package/docs/configuration.md
CHANGED
|
@@ -727,11 +727,12 @@ How this repository frames the documentation pages it generates.
|
|
|
727
727
|
registry and are the same wherever rendered; everything here is the
|
|
728
728
|
consumer's: heading, orientation, where the page is filed.
|
|
729
729
|
|
|
730
|
-
| Key
|
|
731
|
-
|
|
|
732
|
-
| `docs.itemFields.title`
|
|
733
|
-
| `docs.itemFields.out`
|
|
734
|
-
| `docs.itemFields.preamble`
|
|
730
|
+
| Key | Type | Required | Default |
|
|
731
|
+
| ----------------------------- | -------- | -------- | ---------------------------------------------------------------------- |
|
|
732
|
+
| `docs.itemFields.title` | string | no | none — the page's H1 |
|
|
733
|
+
| `docs.itemFields.out` | string | no | none — without it, the page goes to stdout |
|
|
734
|
+
| `docs.itemFields.preamble` | string[] | no | none — markdown lines between the generated banner and the first table |
|
|
735
|
+
| `docs.itemFields.frontmatter` | object | no | none — extra note frontmatter, deep-merged over the generated envelope |
|
|
735
736
|
|
|
736
737
|
> ``package-build config: `docs.itemFields` must be a mapping.``
|
|
737
738
|
|
|
@@ -744,9 +745,14 @@ consumer's: heading, orientation, where the page is filed.
|
|
|
744
745
|
|
|
745
746
|
> ``package-build config: `docs.itemFields.preamble[<index>]` must be a string.``
|
|
746
747
|
|
|
748
|
+
`frontmatter` is deep-merged over the note envelope written when `out` is
|
|
749
|
+
under the content tree — see [`content-build docs item-fields`](commands.md#content-build-docs-item-fields):
|
|
750
|
+
|
|
751
|
+
> ``package-build config: `docs.itemFields.frontmatter` must be a mapping.``
|
|
752
|
+
|
|
747
753
|
Any other key under `docs.itemFields` is refused:
|
|
748
754
|
|
|
749
|
-
> ``package-build config: `docs.itemFields.<key>` is not a recognized option (expected one of: title, out, preamble).``
|
|
755
|
+
> ``package-build config: `docs.itemFields.<key>` is not a recognized option (expected one of: title, out, preamble, frontmatter).``
|
|
750
756
|
|
|
751
757
|
### `site`
|
|
752
758
|
|
|
@@ -759,9 +765,7 @@ Any other key under `docs.itemFields` is refused:
|
|
|
759
765
|
| `site.description` | string | `""`, but required for `content-build site` |
|
|
760
766
|
| `site.packages` | string[] | `[]` |
|
|
761
767
|
| `site.sections` | object | `{}` |
|
|
762
|
-
| `site.readmeSections` | object | `{}` |
|
|
763
768
|
| `site.landing` | object | `null` |
|
|
764
|
-
| `site.trees` | array | `[]` |
|
|
765
769
|
| `site.pass` | string | `""` |
|
|
766
770
|
| `site.passOptions` | object | `{}` |
|
|
767
771
|
| `site.backfillSections` | boolean | `false` |
|
|
@@ -771,10 +775,19 @@ Any other key under `docs.itemFields` is refused:
|
|
|
771
775
|
|
|
772
776
|
How much of a package reaches the web at all is **not** here — it is
|
|
773
777
|
[`publish.site`](#publish). `site` is framing: what a section is called,
|
|
774
|
-
which
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
+
which named pass bundle supplies the repository's own body rewrites, and the
|
|
779
|
+
residue of the [generated Hugo configuration](#the-generated-hugo-configuration)
|
|
780
|
+
that is genuinely this repository's own.
|
|
781
|
+
|
|
782
|
+
What the site publishes is the content tree, and nothing beside it. A page of
|
|
783
|
+
documentation is a note — `type: doc`, addressed by its shortcode, and
|
|
784
|
+
`pack: none` where it compiles into no Foundry document — so there is no
|
|
785
|
+
second mechanism for mounting a directory of markdown, and a configuration
|
|
786
|
+
that names one is refused with a message saying where the page goes instead:
|
|
787
|
+
|
|
788
|
+
> ``package-build config: `site.trees` is retired — a page is a note in the content tree. Give each page `type: doc`, a `shortcode` and `pack: none`, file it under `assets/content/`, and declare the section that lists it under `site.sections`.``
|
|
789
|
+
|
|
790
|
+
> ``package-build config: `site.readmeSections` is retired — a page is a note in the content tree. Give each page `type: doc`, a `shortcode` and `pack: none`, file it under `assets/content/`, and declare the section that lists it under `site.sections`.``
|
|
778
791
|
|
|
779
792
|
Where the Hugo tree is written is not a choice. `content-build site` writes
|
|
780
793
|
the whole Hugo source tree under `build/hugo/` — the generated `hugo.toml`,
|
|
@@ -786,7 +799,7 @@ published. A `site.out` is refused by name:
|
|
|
786
799
|
|
|
787
800
|
> ``package-build config: `site` must be a mapping.``
|
|
788
801
|
|
|
789
|
-
> ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, sections,
|
|
802
|
+
> ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, sections, landing, pass, passOptions, backfillSections, list, notfound, hugo).``
|
|
790
803
|
|
|
791
804
|
`site.assets` is the host every package's imagery is served from, and it is
|
|
792
805
|
the one address in this file that is not this repository's own. A note names
|
|
@@ -832,25 +845,8 @@ names a registry):
|
|
|
832
845
|
|
|
833
846
|
> ``package-build config: `site.packages[<index>]` must be a non-empty string.``
|
|
834
847
|
|
|
835
|
-
`site.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
| Key (under `site.trees[]`) | Type | Required |
|
|
839
|
-
| -------------------------- | ------ | -------- |
|
|
840
|
-
| `site.trees[].from` | string | yes |
|
|
841
|
-
| `site.trees[].section` | string | yes |
|
|
842
|
-
|
|
843
|
-
> ``package-build config: `site.trees` must be a list.``
|
|
844
|
-
|
|
845
|
-
> ``package-build config: `site.trees[<index>]` must be a mapping.``
|
|
846
|
-
|
|
847
|
-
> ``package-build config: `site.trees[<index>].from` must be a non-empty string.``
|
|
848
|
-
|
|
849
|
-
> ``package-build config: `site.trees[<index>].<key>` is not a recognized option (expected one of: from, section).``
|
|
850
|
-
|
|
851
|
-
`site.sections` (and `site.readmeSections`, the same shape) is a closed
|
|
852
|
-
vocabulary — a section's _only_ place to speak, since it exists solely as
|
|
853
|
-
the generated `_index.md` this build writes for it:
|
|
848
|
+
`site.sections` is a closed vocabulary — a section's _only_ place to speak,
|
|
849
|
+
since it exists solely as the generated `_index.md` this build writes for it:
|
|
854
850
|
|
|
855
851
|
| Key (under `site.sections.<name>`) | Type | Required | Default |
|
|
856
852
|
| ---------------------------------- | -------------------------- | -------- | ------- |
|
|
@@ -1336,9 +1332,9 @@ its content tree's addresses mount inside the package.
|
|
|
1336
1332
|
Every HeroicLands package publishes at least an authored homepage at
|
|
1337
1333
|
`https://www.heroiclands.org/<contentPackage>/` — there is no value meaning
|
|
1338
1334
|
_no web presence at all_. `homepage` is the floor: the authored homepage
|
|
1339
|
-
and nothing else, no content-tree walk, no `site.sections` / `site.
|
|
1340
|
-
|
|
1341
|
-
|
|
1335
|
+
and nothing else, no content-tree walk, no `site.sections` / `site.landing`
|
|
1336
|
+
output. `content` is the homepage plus every page the content tree
|
|
1337
|
+
publishes. `publishesContentPages(config)`, exported from
|
|
1342
1338
|
`content-config.mjs` alongside [`compilesFoundryDocuments`](#packagekind),
|
|
1343
1339
|
answers the one question every reader of the mode actually asks — the site
|
|
1344
1340
|
build, to decide whether to walk the tree at all, and the content index, to
|
package/docs/content-format.md
CHANGED
|
@@ -801,13 +801,43 @@ hm3:
|
|
|
801
801
|
Where no pack of that type is the default, the build refuses rather than
|
|
802
802
|
guessing, and names the candidates.
|
|
803
803
|
|
|
804
|
-
|
|
804
|
+
**`pack: none` compiles the note into no document.** The note is walked,
|
|
805
|
+
published as a page, present in the content index with an address and no
|
|
806
|
+
Foundry UUID, and addressable by wikilink like any other; every pack compiler
|
|
807
|
+
passes over it without a finding, and the check that reports a note nothing
|
|
808
|
+
compiles says nothing about it. Write it on a page that belongs on the website
|
|
809
|
+
and in no compendium — a page of developer documentation, a page about the
|
|
810
|
+
package itself:
|
|
805
811
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
812
|
+
```yaml
|
|
813
|
+
type: doc
|
|
814
|
+
subType: concept
|
|
815
|
+
shortcode: architecture
|
|
816
|
+
pack: none
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
A link to such a note resolves everywhere the note is addressable. On the web
|
|
820
|
+
it is an ordinary link to the page. In a compiled journal there is no document
|
|
821
|
+
to open, so the reader gets the link's text as prose and no `@UUID` — the
|
|
822
|
+
mirror of a link into a package that publishes documents and no pages.
|
|
823
|
+
|
|
824
|
+
It is accepted only on a type whose **sole document is the JournalEntry its
|
|
825
|
+
prose becomes** — `doc`, `place`, `lore` and `scenario`. On a type that
|
|
826
|
+
compiles an Item, an Actor, a Macro, a Scene or an Adventure it is refused by
|
|
827
|
+
name, because there it would drop the document the type exists to produce;
|
|
828
|
+
the message names that document. It is read per system like any other
|
|
829
|
+
`pack:`, so `<system>.pack: none` withholds one system's document while the
|
|
830
|
+
shared declaration names a pack for the rest, and a configured pack may not
|
|
831
|
+
be called `none`.
|
|
832
|
+
|
|
833
|
+
Four declarations are refused, each with the reason:
|
|
834
|
+
|
|
835
|
+
| written | why it is refused |
|
|
836
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
837
|
+
| a **companion** pack | A companion is written by another pack's pass, so no note may be routed into one. |
|
|
838
|
+
| a pack **nothing answers to** | The message lists the configured packs of that document type. |
|
|
839
|
+
| a pack of **another document type** | A note's `pack:` names a pack of its own document type. |
|
|
840
|
+
| **`none`** on a type with a second document | The Item, Actor, Macro, Scene or Adventure the type compiles into would be dropped, by name. |
|
|
811
841
|
|
|
812
842
|
#### Template priority: which template wins
|
|
813
843
|
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
* @module
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
import matter from "gray-matter";
|
|
39
|
+
|
|
37
40
|
import { authoredFields, runtimeOnlyFields } from "./field-spec.mjs";
|
|
38
41
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
39
42
|
|
|
@@ -309,3 +312,135 @@ export function renderItemFieldReference({
|
|
|
309
312
|
// and then reported stale by `--check` forever after.
|
|
310
313
|
return lines.join("\n").replace(/\n+$/, "");
|
|
311
314
|
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {unknown} value - Anything.
|
|
318
|
+
* @returns {boolean} Whether it is a mapping a field may be read out of.
|
|
319
|
+
*/
|
|
320
|
+
function isPlainObject(value) {
|
|
321
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Recursively merge `overlay` onto `base`. Plain objects merge key-by-key;
|
|
326
|
+
* everything else (arrays, primitives, `null`) replaces. Inputs are not
|
|
327
|
+
* mutated.
|
|
328
|
+
*
|
|
329
|
+
* @param {any} base - The generated envelope.
|
|
330
|
+
* @param {any} overlay - The consumer's declared `frontmatter`.
|
|
331
|
+
* @returns {any} The merged value.
|
|
332
|
+
*/
|
|
333
|
+
function deepMerge(base, overlay) {
|
|
334
|
+
if (overlay === undefined) return base;
|
|
335
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) return overlay;
|
|
336
|
+
const out = { ...base };
|
|
337
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
338
|
+
out[key] = key in base ? deepMerge(base[key], value) : value;
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Whether a destination file sits under a content tree.
|
|
345
|
+
*
|
|
346
|
+
* The one question that decides whether `docs item-fields` writes a note
|
|
347
|
+
* envelope: `assets/content/` walks every file under it for its `type:`, so a
|
|
348
|
+
* generated page filed there needs one to publish at all, while a page filed
|
|
349
|
+
* anywhere else — a repository's own `docs/` — is read by nobody but Hugo's
|
|
350
|
+
* `--check` guard and the reader following a link, neither of which wants
|
|
351
|
+
* frontmatter.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} destination - Absolute path of the file being written.
|
|
354
|
+
* @param {string} contentRoot - Absolute path of the content tree root
|
|
355
|
+
* (`config.paths.content`).
|
|
356
|
+
* @returns {boolean} Whether `destination` resolves inside `contentRoot`.
|
|
357
|
+
*/
|
|
358
|
+
export function isUnderContentTree(destination, contentRoot) {
|
|
359
|
+
const relative = path.relative(contentRoot, destination);
|
|
360
|
+
return (
|
|
361
|
+
relative !== "" &&
|
|
362
|
+
relative !== ".." &&
|
|
363
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
364
|
+
!path.isAbsolute(relative)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* A note's `shortcode`, derived from the basename of its destination file.
|
|
370
|
+
*
|
|
371
|
+
* Lowercase alphanumerics only — the address charset every other shortcode in
|
|
372
|
+
* the tree is held to — so `item-frontmatter.md` derives `itemfrontmatter`
|
|
373
|
+
* rather than carrying a hyphen no address segment permits.
|
|
374
|
+
*
|
|
375
|
+
* @param {string} destination - Where the note is written.
|
|
376
|
+
* @returns {string} The derived shortcode.
|
|
377
|
+
*/
|
|
378
|
+
export function shortcodeFromBasename(destination) {
|
|
379
|
+
return path
|
|
380
|
+
.basename(destination, path.extname(destination))
|
|
381
|
+
.toLowerCase()
|
|
382
|
+
.replace(/[^a-z0-9]/g, "");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The note envelope `docs item-fields` writes when its page lives in the
|
|
387
|
+
* content tree.
|
|
388
|
+
*
|
|
389
|
+
* The universal keys every note in the format carries: `type: doc`,
|
|
390
|
+
* `subType: reference` — this page is out-of-world lookup material, the same
|
|
391
|
+
* genre as every other generated reference — a `shortcode`, `name.full` from
|
|
392
|
+
* the page's title, and `pack: none`, since the page publishes to the website
|
|
393
|
+
* and compiles into no compendium document. A consumer's own
|
|
394
|
+
* `docs.itemFields.frontmatter` is deep-merged over it, so it may add keys or
|
|
395
|
+
* override any of the derived ones, `shortcode` included.
|
|
396
|
+
*
|
|
397
|
+
* @param {object} options
|
|
398
|
+
* @param {string} options.title - The page's H1, and `name.full`'s default.
|
|
399
|
+
* @param {string} options.shortcode - The derived shortcode, from
|
|
400
|
+
* {@link shortcodeFromBasename}.
|
|
401
|
+
* @param {Record<string, unknown>} [options.frontmatter] - The consumer's
|
|
402
|
+
* declared `docs.itemFields.frontmatter`.
|
|
403
|
+
* @returns {Record<string, unknown>} The envelope, ready for `matter.stringify`.
|
|
404
|
+
*/
|
|
405
|
+
export function itemFieldsEnvelope({ title, shortcode, frontmatter }) {
|
|
406
|
+
return deepMerge(
|
|
407
|
+
{
|
|
408
|
+
type: "doc",
|
|
409
|
+
subType: "reference",
|
|
410
|
+
shortcode,
|
|
411
|
+
name: { full: title },
|
|
412
|
+
pack: "none",
|
|
413
|
+
},
|
|
414
|
+
frontmatter,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Wrap the rendered item-fields page in the note envelope, when its
|
|
420
|
+
* destination is under the content tree.
|
|
421
|
+
*
|
|
422
|
+
* `--check` compares the **whole** file this returns, envelope included — a
|
|
423
|
+
* page committed with its old envelope by hand, or with none at all, is
|
|
424
|
+
* exactly the staleness the guard exists to catch.
|
|
425
|
+
*
|
|
426
|
+
* @param {string} body - The page {@link renderItemFieldReference} rendered.
|
|
427
|
+
* @param {object} options
|
|
428
|
+
* @param {string} options.title - The page's H1, threaded through to
|
|
429
|
+
* `name.full`.
|
|
430
|
+
* @param {string} options.destination - Absolute path the page is written to.
|
|
431
|
+
* @param {string} options.contentRoot - Absolute path of the content tree
|
|
432
|
+
* root (`config.paths.content`).
|
|
433
|
+
* @param {Record<string, unknown>} [options.frontmatter] - The consumer's
|
|
434
|
+
* declared `docs.itemFields.frontmatter`.
|
|
435
|
+
* @returns {string} `body`, unchanged when `destination` is outside the
|
|
436
|
+
* content tree; otherwise `body` with the note envelope stringified above it.
|
|
437
|
+
*/
|
|
438
|
+
export function renderItemFieldsPage(body, { title, destination, contentRoot, frontmatter }) {
|
|
439
|
+
if (!isUnderContentTree(destination, contentRoot)) return body;
|
|
440
|
+
const envelope = itemFieldsEnvelope({
|
|
441
|
+
title,
|
|
442
|
+
shortcode: shortcodeFromBasename(destination),
|
|
443
|
+
frontmatter,
|
|
444
|
+
});
|
|
445
|
+
return matter.stringify(body, envelope);
|
|
446
|
+
}
|
|
@@ -70,6 +70,7 @@ import { resolveNoteId } from "./note-ids.mjs";
|
|
|
70
70
|
import { compendiumUuid, currentType, packForType, pageUuid } from "./ids.mjs";
|
|
71
71
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
72
72
|
import { isHomepage } from "./homepage.mjs";
|
|
73
|
+
import { declaresNoPack } from "./pack-router.mjs";
|
|
73
74
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
74
75
|
import { assertNoDeclaredFolder } from "./folder-notes.mjs";
|
|
75
76
|
import {
|
|
@@ -179,7 +180,17 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
179
180
|
// and its id is hashed under the `folder` namespace against its own
|
|
180
181
|
// address rather than under `document`. Emitting one would publish an
|
|
181
182
|
// `Item` UUID for a `Folder`, at an id no document carries.
|
|
182
|
-
|
|
183
|
+
//
|
|
184
|
+
// And a note declaring **`pack: none`** is in no pack by its own
|
|
185
|
+
// statement: it publishes a page and compiles into nothing, so it has
|
|
186
|
+
// an address and no UUID. The shared declaration is what is read,
|
|
187
|
+
// because an entry names one document and this pass routes no system.
|
|
188
|
+
(
|
|
189
|
+
id &&
|
|
190
|
+
!NEVER_PACKED_TYPES.has(String(type)) &&
|
|
191
|
+
!DERIVED_PACKED_TYPES.has(String(type)) &&
|
|
192
|
+
!(routeFm && declaresNoPack(routeFm))
|
|
193
|
+
) ?
|
|
183
194
|
compendiumUuid(
|
|
184
195
|
foundryPackageId,
|
|
185
196
|
type,
|
|
@@ -1265,9 +1265,8 @@ export function lintNote(
|
|
|
1265
1265
|
message: draftRetiredMessage(),
|
|
1266
1266
|
});
|
|
1267
1267
|
}
|
|
1268
|
-
// Anchored at column 1 for the same reason `aliases` is:
|
|
1269
|
-
//
|
|
1270
|
-
// some other block is not this field.
|
|
1268
|
+
// Anchored at column 1 for the same reason `aliases` is: a nested
|
|
1269
|
+
// `section` under some other block is not this field.
|
|
1271
1270
|
if (Object.hasOwn(fm, "section")) {
|
|
1272
1271
|
findings.push({
|
|
1273
1272
|
file: note.file,
|
package/engine/helpers.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { foundryAddressProblem, pathnameProblem, resolvePathname } from "./pathn
|
|
|
38
38
|
import log from "loglevel";
|
|
39
39
|
|
|
40
40
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
41
|
-
import { packRouter } from "./pack-router.mjs";
|
|
41
|
+
import { declaresNoPack, packRouter } from "./pack-router.mjs";
|
|
42
42
|
import { contentPackage, foundryPackageId } from "./content-package.mjs";
|
|
43
43
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
44
44
|
import { PACKAGE_BASE } from "./content-address.mjs";
|
|
@@ -741,6 +741,9 @@ export function buildContentLinkIndex(
|
|
|
741
741
|
// its prose compiles into lands — two documents, two packs.
|
|
742
742
|
pack: router.resolveOrNull(fm, packForType(fm.type).docType),
|
|
743
743
|
docPack: router.resolveOrNull(fm, "JournalEntry"),
|
|
744
|
+
// Whether the note declares `pack: none` and so has no document
|
|
745
|
+
// at all: a link to it names a page, never a compendium entry.
|
|
746
|
+
none: declaresNoPack(fm),
|
|
744
747
|
shortcode: fm.shortcode ?? null,
|
|
745
748
|
// What the note *is*, carried so a caller resolving a reference
|
|
746
749
|
// can group by the family its target declares rather than only by
|
package/engine/ids.mjs
CHANGED
|
@@ -86,6 +86,18 @@ export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap
|
|
|
86
86
|
*/
|
|
87
87
|
export const JOURNAL_TYPES = Object.freeze(new Set(["doc", "place", "lore", "scenario"]));
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The `pack:` value that routes a note's document into no compendium.
|
|
91
|
+
*
|
|
92
|
+
* One reading, on the key that already decides which compendium receives a
|
|
93
|
+
* note: the note publishes to the site and compiles into nothing. Declared
|
|
94
|
+
* here, beside the pack table, so the configuration can refuse a pack of this
|
|
95
|
+
* name and the router can read the value from one spelling.
|
|
96
|
+
*
|
|
97
|
+
* @type {string}
|
|
98
|
+
*/
|
|
99
|
+
export const NO_PACK = "none";
|
|
100
|
+
|
|
89
101
|
/**
|
|
90
102
|
* Content type → the pack its documents compile into, and the document type
|
|
91
103
|
* that pack holds.
|
package/engine/note-claims.mjs
CHANGED
|
@@ -91,6 +91,7 @@ import { JOURNAL_TYPES, MAP_TYPES, PACK_BY_TYPE, RETIRED_TYPES, currentType } fr
|
|
|
91
91
|
import { itemTypes } from "./item-registry.mjs";
|
|
92
92
|
import { docEntryTypes } from "./item-docs.mjs";
|
|
93
93
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
94
|
+
import { declaresNoPack } from "./pack-router.mjs";
|
|
94
95
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
95
96
|
import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
|
|
96
97
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
@@ -641,6 +642,12 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
641
642
|
// {@link claimedNoteTypes} states: its JSON is checked in, it has no pass,
|
|
642
643
|
// and no note is routed into it.
|
|
643
644
|
const configured = new Set((config.packs ?? []).filter((p) => !p.prebuilt).map((p) => p.type));
|
|
645
|
+
// Every system a pack declares, plus the shared position: a note that
|
|
646
|
+
// answers `pack: none` for all of them compiles into nothing anywhere.
|
|
647
|
+
const routed = [
|
|
648
|
+
undefined,
|
|
649
|
+
...new Set((config.packs ?? []).map((p) => p.system).filter(Boolean)),
|
|
650
|
+
];
|
|
644
651
|
|
|
645
652
|
// The corpus this compile derived once, required rather than
|
|
646
653
|
// derived here: this module is imported *by* the content index, so it
|
|
@@ -681,6 +688,20 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
681
688
|
// Every document it produces has somewhere to go.
|
|
682
689
|
if (produces.length && !missing.length) continue;
|
|
683
690
|
|
|
691
|
+
// `pack: none`, on a type whose only document is the JournalEntry its
|
|
692
|
+
// prose becomes: the note compiles into nothing **by declaration**, so
|
|
693
|
+
// a missing JournalEntry pack drops nothing it meant to keep. A type
|
|
694
|
+
// that also produces an Item or an Actor is not excused here — the
|
|
695
|
+
// router refuses that declaration by name when a pass asks it, and
|
|
696
|
+
// where no pass does, the finding below still says nothing claims the
|
|
697
|
+
// note.
|
|
698
|
+
if (
|
|
699
|
+
produces.every((docType) => docType === "JournalEntry") &&
|
|
700
|
+
routed.every((system) => declaresNoPack(fm, system))
|
|
701
|
+
) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
684
705
|
// Some do and some do not: the note compiles, and one of its documents
|
|
685
706
|
// is dropped in silence. A type nothing produces at all falls past this
|
|
686
707
|
// to the unclaimed messages below, where `produces` being empty is itself
|
package/engine/pack-router.mjs
CHANGED
|
@@ -65,11 +65,22 @@
|
|
|
65
65
|
* a shared value that cannot describe a system-specific pack, so it does not
|
|
66
66
|
* answer and that system falls through to its own default. Both were silent
|
|
67
67
|
* losses of one system's whole document set.
|
|
68
|
+
* - **`pack: none` routes the document nowhere, on purpose.** The note is
|
|
69
|
+
* walked, indexed, published and linkable, and no compendium receives it:
|
|
70
|
+
* {@link createPackRouter}'s `resolve` answers `undefined`, which no pass's
|
|
71
|
+
* name equals, so every pass passes over the note as one it does not own.
|
|
72
|
+
* It is accepted only on a type whose sole document is the JournalEntry its
|
|
73
|
+
* prose becomes — `doc` and the other journal-only types. On a type that
|
|
74
|
+
* compiles an Item, an Actor, a Macro, a Scene or an Adventure it is refused
|
|
75
|
+
* by name, because there the declaration would drop the document the type
|
|
76
|
+
* exists to produce. Read per system like any other `pack:`, so a block may
|
|
77
|
+
* say `none` for one system and the shared declaration may name a pack for
|
|
78
|
+
* the rest.
|
|
68
79
|
*
|
|
69
80
|
* @module
|
|
70
81
|
*/
|
|
71
82
|
|
|
72
|
-
import { packForType } from "./ids.mjs";
|
|
83
|
+
import { NO_PACK, packForType } from "./ids.mjs";
|
|
73
84
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
74
85
|
import { blockProperty, systemBlock } from "./system-block.mjs";
|
|
75
86
|
|
|
@@ -95,6 +106,35 @@ export class PackRoutingError extends Error {
|
|
|
95
106
|
*/
|
|
96
107
|
export const PACK_FIELD = "pack";
|
|
97
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The `pack:` value that routes a note's document into no compendium — see
|
|
111
|
+
* {@link module:engine/ids.NO_PACK}. Re-exported here because this is the
|
|
112
|
+
* module that reads it; a configured pack may not take the name, so the value
|
|
113
|
+
* can never be mistaken for one.
|
|
114
|
+
*/
|
|
115
|
+
export { NO_PACK };
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether a note declares `pack: none` for one system's document.
|
|
119
|
+
*
|
|
120
|
+
* The same reading {@link createPackRouter}'s `resolve` applies: the system's
|
|
121
|
+
* block wins where it declares a pack, and the shared top-level value stands
|
|
122
|
+
* otherwise. Asked with no system, it reads the shared value alone, which is
|
|
123
|
+
* what every single-system build and the content index read.
|
|
124
|
+
*
|
|
125
|
+
* Pure, so the readers that never route — the Foundry-address pass, the link
|
|
126
|
+
* index, the unclaimed-type check — can ask it without a router.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} fm - The note's frontmatter.
|
|
129
|
+
* @param {string} [system] - The system whose document is asked about.
|
|
130
|
+
* @returns {boolean} True when the answer for that document is `none`.
|
|
131
|
+
*/
|
|
132
|
+
export function declaresNoPack(fm, system) {
|
|
133
|
+
const declared =
|
|
134
|
+
system === undefined ? fm?.[PACK_FIELD] : blockProperty(fm, system, PACK_FIELD);
|
|
135
|
+
return declared === NO_PACK;
|
|
136
|
+
}
|
|
137
|
+
|
|
98
138
|
/**
|
|
99
139
|
* Build the router for one configured pack list.
|
|
100
140
|
*
|
|
@@ -103,7 +143,7 @@ export const PACK_FIELD = "pack";
|
|
|
103
143
|
*
|
|
104
144
|
* @param {readonly object[]} packs - The resolved `packs` list from
|
|
105
145
|
* `defineConfig`.
|
|
106
|
-
* @returns {{resolve: (fm: object, docType: string, system?: string) => string,
|
|
146
|
+
* @returns {{resolve: (fm: object, docType: string, system?: string) => string|undefined,
|
|
107
147
|
* resolveOrNull: (fm: object, docType: string, system?: string) => string|undefined,
|
|
108
148
|
* packsOfType: (docType: string) => string[],
|
|
109
149
|
* defaultOf: (docType: string) => string|undefined}} The router.
|
|
@@ -179,6 +219,14 @@ export function createPackRouter(packs) {
|
|
|
179
219
|
/** @param {object} fm */
|
|
180
220
|
const noteLabel = (fm) => fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "a note";
|
|
181
221
|
|
|
222
|
+
/**
|
|
223
|
+
* `"a"` or `"an"`, so a document class reads as English in a message.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} word - The word the article precedes.
|
|
226
|
+
* @returns {string} The article.
|
|
227
|
+
*/
|
|
228
|
+
const article = (word) => (/^[AEIOUaeiou]/.test(word) ? "an" : "a");
|
|
229
|
+
|
|
182
230
|
/**
|
|
183
231
|
* The pack a note declaring none is routed to.
|
|
184
232
|
*
|
|
@@ -212,8 +260,11 @@ export function createPackRouter(packs) {
|
|
|
212
260
|
* @param {string} [system] - The system whose document is being routed. Its
|
|
213
261
|
* block's `pack:` wins over the shared one; without it only the shared
|
|
214
262
|
* declaration is read, which is every single-system build.
|
|
215
|
-
* @returns {string} The pack name
|
|
216
|
-
*
|
|
263
|
+
* @returns {string|undefined} The pack name — or `undefined` where the
|
|
264
|
+
* note declares `pack: none`, which no pass's name equals, so every pass
|
|
265
|
+
* passes over the note as one it does not own.
|
|
266
|
+
* @throws {PackRoutingError} When the note routes nowhere, or declares
|
|
267
|
+
* `pack: none` on a type that compiles a document beside its prose.
|
|
217
268
|
*/
|
|
218
269
|
function resolve(fm, docType, system) {
|
|
219
270
|
const inBlock = system === undefined ? undefined : systemBlock(fm, system)?.[PACK_FIELD];
|
|
@@ -225,6 +276,29 @@ export function createPackRouter(packs) {
|
|
|
225
276
|
// JournalEntry — is not what the author was addressing.
|
|
226
277
|
const ownDocType = packForType(fm?.type).docType;
|
|
227
278
|
|
|
279
|
+
// `none`: the note compiles into no document. Only a type whose own
|
|
280
|
+
// document *is* the JournalEntry may say so — for every other type
|
|
281
|
+
// the declaration would drop the Item, Actor, Macro, Scene or
|
|
282
|
+
// Adventure the type exists to produce, and its prose with it. The
|
|
283
|
+
// document class comes from the same table every pass routes by, so
|
|
284
|
+
// the refusal names what the compile would have written. Asked only
|
|
285
|
+
// by the pass that writes the note's own document, as every `pack:`
|
|
286
|
+
// is, so the refusal is reported once rather than once per pass.
|
|
287
|
+
if (declared === NO_PACK && docType === ownDocType) {
|
|
288
|
+
if (ownDocType !== "JournalEntry") {
|
|
289
|
+
const spelled = authoredInBlock ? `${system}.pack: ${NO_PACK}` : `pack: ${NO_PACK}`;
|
|
290
|
+
throw new PackRoutingError(
|
|
291
|
+
`${noteLabel(fm)} declares \`${spelled}\`, but a ${fm?.type} note ` +
|
|
292
|
+
`compiles into ${article(ownDocType)} ${ownDocType}, and ` +
|
|
293
|
+
`\`pack: ${NO_PACK}\` would drop it. Only a type whose sole ` +
|
|
294
|
+
`document is the JournalEntry its prose becomes may ` +
|
|
295
|
+
`declare it — name the pack the ${ownDocType} goes to, or ` +
|
|
296
|
+
`leave \`pack:\` unset for the default.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
228
302
|
// **A declaration naming another system's pack is not this system's
|
|
229
303
|
// answer**, and which of two things that means depends on where
|
|
230
304
|
// it was written.
|
|
@@ -328,7 +402,10 @@ export function createPackRouter(packs) {
|
|
|
328
402
|
* @param {object} fm - The note's frontmatter.
|
|
329
403
|
* @param {string} docType - The document type being addressed.
|
|
330
404
|
* @param {string} [system] - The system whose document is addressed.
|
|
331
|
-
* @returns {string|undefined} The pack name, or `undefined
|
|
405
|
+
* @returns {string|undefined} The pack name, or `undefined` — for a
|
|
406
|
+
* note that routes nowhere and for one declaring `pack: none`
|
|
407
|
+
* alike. A caller that must tell the two apart asks
|
|
408
|
+
* {@link declaresNoPack}.
|
|
332
409
|
*/
|
|
333
410
|
resolveOrNull(fm, docType, system) {
|
|
334
411
|
try {
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* ## `publish.site` is the switch, and it is the only switch
|
|
36
36
|
*
|
|
37
37
|
* `homepage` mode fences the content surfaces off: the tree is not walked and
|
|
38
|
-
* `sections
|
|
38
|
+
* `sections` and `landing` emit nothing however they are declared. A
|
|
39
39
|
* PDF of the content tree is a content surface by any reading — arguably the
|
|
40
40
|
* most portable one there is — so it is fenced on exactly the same terms, by
|
|
41
41
|
* asking the same {@link module:content-config.publishesContentPages} the
|
|
@@ -481,7 +481,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
481
481
|
* @returns {string} Typst markup.
|
|
482
482
|
*/
|
|
483
483
|
const renderPage = (page, headingOffset, anchorPrefix) => {
|
|
484
|
-
const src = page.relPath ?? page.
|
|
484
|
+
const src = page.relPath ?? page.base;
|
|
485
485
|
const wikiErrors = [];
|
|
486
486
|
const { markdown, errors } = expandContentTables(page.body, {
|
|
487
487
|
docs: universe.get(page.pkg) ?? [],
|
|
@@ -326,9 +326,8 @@ export function assertNoSectionField(fm, { file, absPath } = {}) {
|
|
|
326
326
|
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "section")) return;
|
|
327
327
|
|
|
328
328
|
const err = new Error(`${sectionRetiredMessage(file)}.`);
|
|
329
|
-
// Anchored at column 1: `
|
|
330
|
-
//
|
|
331
|
-
// this field — a finding about the top-level one must not open on it.
|
|
329
|
+
// Anchored at column 1: a nested `section:` inside some other block is
|
|
330
|
+
// not this field — a finding about the top-level one must not open on it.
|
|
332
331
|
const position = locateFrontmatterKey(absPath, "section", undefined, { topLevel: true });
|
|
333
332
|
if (position) err.position = position;
|
|
334
333
|
throw err;
|