@heroiclands/package-build 22.18.0 → 22.19.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Beings**
8
+
9
+ - Beings can be described as Commoners, Entertainers, or Guildspeople, and Commoner stands alone when no other archetype fits.
10
+
11
+ ### Patch Changes
12
+
13
+ - Books place infoboxes after each entry’s authored content, and Foundry journals place each infobox on its own page after the authored pages.
14
+
3
15
  ## 22.18.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -18,7 +18,7 @@ The generated project includes a homepage, website and book targets, npm scripts
18
18
 
19
19
  ## Documentation
20
20
 
21
- The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md).
21
+ The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md). This toolchain does not build its own site or book. A Hugo theme dependency would form a cycle because the theme uses package-build for releases.
22
22
 
23
23
  | Topic | Guide |
24
24
  | ------------------------------------------------- | ------------------------------------------------------------ |
package/docs/api.md CHANGED
@@ -417,7 +417,7 @@ The infobox declarations this toolchain ships, one per system, and the single ca
417
417
 
418
418
  ### `engine.infoboxRender`
419
419
 
420
- Drawing a declared infobox: HTML for a Foundry Journal Page, Typst for the book. The website's boxes travel in front matter and the site theme draws them, so there is no renderer for it here.
420
+ Drawing a declared infobox: HTML for separate Foundry pages appended after authored pages, Typst panels after each note’s authored body in the book. The website's boxes travel in front matter and the site theme draws them, so there is no renderer for it here.
421
421
 
422
422
  | Export | Signature | Returns | Use it when |
423
423
  | ---------------------- | ------------------------------------- | --------- | ------------------------------------------------------------------------ |
@@ -31,10 +31,12 @@ The formatter keeps a collection on one line when the full line is under 100 cha
31
31
 
32
32
  The [note-type reference](../reference/note-types.md) lists every field accepted under `data`, including the shared `id`, `pack`, `packFolder`, `harnworld`, `icon`, and `banner` fields. A type such as `being` also has its own fields, including `culture`, `homes`, and `affiliations`. A field absent from a type's vocabulary is a lint error; an unrelated field is not silently repurposed.
33
33
 
34
+ A being's `data.archetypes` names the roles it can fill in an adventure. Choose every specific role that fits, such as `entertainer` for a performer or `guildsperson` for someone whose professional training or connections matter. Use `[commoner]` when no specific archetype fits. `commoner` cannot share the list with another value. The [archetype reference](../reference/format-details.md#actors) defines the available roles.
35
+
34
36
  A `sohl:` or `hm3:` block holds values for that Foundry system. Its `system:` child contains values sent to the system's DataModel. The system block can override shared routing and art where the declared field permits it. `data.pack` and `data.packFolder` are shared routes; a system-specific route belongs in that system block. The [detailed mapping reference](../reference/format-details.md#mappings-every-type-shares) and [system-block explanation](../reference/format-details.md#frontmatter-has-three-regions) cover precedence and emitted fields.
35
37
 
36
38
  One body describes the subject for both systems. A being's `{#appearance}` and `{#dossier}` sections feed SoHL Actor `system.appearance` and `system.dossier`, and HM3 Actor `system.description` and `system.biography`. An Item's `{#description}` page becomes a shared JournalEntryPage. Its resolved `@UUID[...]` pointer is stored in SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item with no `{#description}` anchor uses its first page. Other H1 pages remain linkable.
37
39
 
38
40
  ## Publication and summaries
39
41
 
40
- A note infobox draws from shared `data`. A being profile shows its top-level `subType` as `Type: Character`, `Type: NPC`, or `Type: Creature`. A system infobox draws from the system's fields, and a mapped system with no document says “Not available.” The same authored body appears in Foundry, on the website, and in the book; presentation adapts to each medium. [Infobox details](../reference/format-details.md#the-infobox) describe field order and empty values.
42
+ A note infobox draws from shared `data`. A being profile shows its top-level `subType` as `Type: Character`, `Type: NPC`, or `Type: Creature`. A system infobox draws from the system's fields, and a mapped system with no document says “Not available.” The same authored body appears in Foundry, on the website, and in the book; the book places boxes after the authored body, Foundry appends one page per box, and the website places boxes in its responsive side rail. [Infobox details](../reference/format-details.md#the-infobox) describe field order and empty values.
@@ -11,10 +11,12 @@ subType: reference
11
11
 
12
12
  Markdown generates a JournalEntry: content before the first H1 becomes its "Introduction" page, and each H1 begins another page. The document Address is `<package>-note-<note_type>-<shortcode>`.
13
13
 
14
- An **infobox** is prepended to the entry's first page, before the prose. It is
15
- not a page of its own: a page is what a Foundry UUID addresses, and a summary a
16
- reader has to navigate to is a summary they do not see. What the box holds, and
17
- where it sits in every other medium, is [the infobox](#the-infobox) below.
14
+ Each emitted infobox becomes a separate page at the end of the JournalEntry,
15
+ after every authored page. The page names are **Properties Infobox**,
16
+ **SoHL Infobox**, and **HM3 Infobox**, in that order where emitted. Generated
17
+ page ids derive from the entry and box identity, separately from authored
18
+ heading names and anchors. An authored heading may share an infobox page name.
19
+ What the boxes hold is [the infobox](#the-infobox) below.
18
20
 
19
21
  **There are two kinds of infobox.**
20
22
 
@@ -62,16 +64,13 @@ For web pages, the full Markdown body renders as HTML with its infoboxes.
62
64
 
63
65
  ### The infobox
64
66
 
65
- **The infobox is generated content at a known position — prepended, before the
66
- prose.** It is not a floating sidebar and it is not defined in a rendering
67
- template. The toolchain settles what each box holds; each medium lays that out
68
- its own way.
67
+ **The toolchain settles what each box holds; each medium places it.**
69
68
 
70
- | medium | placement | collapsing |
71
- | ------- | ------------------------------------------------------ | ------------------------- |
72
- | book | flows in the column measure, breaking between sections | none, and none needed |
73
- | website | side rail on wide screens, inline on narrow | `<details>`, default open |
74
- | Foundry | inlined, the page being narrow | `<details>`, default open |
69
+ | medium | placement | collapsing |
70
+ | ------- | ------------------------------------------------------------------------------------------------------------- | ------------------------- |
71
+ | book | after the note's authored body, before the next entry; flows in the column measure, breaking between sections | none |
72
+ | website | side rail on wide screens, inline on narrow screens | `<details>`, default open |
73
+ | Foundry | one page per emitted box, after all authored pages | `<details>`, default open |
75
74
 
76
75
  Two columns are print-only. A scrolling page has no fixed viewport, so columns
77
76
  make a reader travel down and back up, and a narrow screen collapses to one
@@ -79,8 +78,8 @@ column regardless.
79
78
 
80
79
  **Six rules hold in every medium.**
81
80
 
82
- 1. **It is content in document order**, prepended before the prose. An image
83
- authored before it appears before it.
81
+ 1. **Placement belongs to the medium**, as described above. Authored images
82
+ remain in the body in their authored order.
84
83
  2. **It contains no image.** A picture is authored in the text with its own
85
84
  directive, and its position governs.
86
85
  3. **A section is the unit that flows.** Sections are whole and unbreakable and
@@ -442,16 +441,16 @@ surface:
442
441
  > **Draft.** This entry is unfinished. What it states may change, and nothing in
443
442
  > it is settled.
444
443
 
445
- It leads the entry — above the infobox and above the prose — because a reader
444
+ It leads the entry, before the authored content, because a reader
446
445
  deciding whether to rely on an entry has to be told before they read it. Where
447
446
  it lands:
448
447
 
449
- | Surface | What carries it |
450
- | ------------ | -------------------------------------------------------------------- |
451
- | JournalEntry | a `blockquote.sohl-draft-notice` at the head of the first page |
452
- | Actor | the dossier the sheet draws — SoHL's `dossier`, HM3's `biography` |
453
- | The book | a drawn mark and a left rule between the entry's plate and its panel |
454
- | The website | the theme's own notice, above the infobox rail and the prose |
448
+ | Surface | What carries it |
449
+ | ------------ | ---------------------------------------------------------------------------- |
450
+ | JournalEntry | a `blockquote.sohl-draft-notice` at the head of the first page |
451
+ | Actor | the dossier the sheet draws — SoHL's `dossier`, HM3's `biography` |
452
+ | The book | a drawn mark and a left rule between the entry's plate and its authored body |
453
+ | The website | the theme's own notice, above the infobox rail and the prose |
455
454
 
456
455
  **An Actor carries it once.** A being's prose reaches two fields, and a document
457
456
  saying the same thing twice teaches a reader to skip it — a printed sheet draws
@@ -2445,8 +2444,13 @@ For JournalEntries, the following rules apply:
2445
2444
  - mariner: Handles boats and blue water.
2446
2445
  - artisan: Builds, repairs, and appraises craft work.
2447
2446
  - trader: Moves goods, values them, and knows markets.
2447
+ - commoner: Fits no more specific archetype; handles ordinary work and daily life.
2448
+ - entertainer: Performs for an audience through acting, music, comedy, or similar arts.
2449
+ - guildsperson: Has professional training, standing, or connections that open doors.
2448
2450
 
2449
2451
  Note that archetypes are descriptive, not proscriptive, and a character may be described by multiple archetypes at once.
2452
+ `commoner` stands alone: a being with another fitting archetype does not also carry `commoner`.
2453
+ Guild membership alone does not require `guildsperson`; the being's training, standing, or connections must matter to the role.
2450
2454
 
2451
2455
  #### Items
2452
2456
 
@@ -1173,6 +1173,19 @@ export function lintNote(
1173
1173
  const type = String(fm.type ?? "");
1174
1174
  const raw = () => note.raw ?? "";
1175
1175
  const at = (key, literal) => positionInFrontmatter(raw(), key, literal ?? undefined);
1176
+ if (
1177
+ type === "being" &&
1178
+ Array.isArray(fm.data?.archetypes) &&
1179
+ fm.data.archetypes.includes("commoner") &&
1180
+ fm.data.archetypes.length > 1
1181
+ ) {
1182
+ findings.push({
1183
+ file: note.file,
1184
+ ...at("archetypes", "commoner"),
1185
+ severity: "error",
1186
+ message: "`data.archetypes` may contain `commoner` only by itself",
1187
+ });
1188
+ }
1176
1189
  /**
1177
1190
  * The in-block keys this note's own type claims for something other than
1178
1191
  * the note-level field of that name, which every note-level check below
@@ -51,9 +51,9 @@
51
51
  *
52
52
  * ## Six rules that hold in every medium
53
53
  *
54
- * 1. **The infobox is generated content in document order** — prepended,
55
- * before the prose. Not a floating sidebar. An image authored before it
56
- * appears before it.
54
+ * 1. **Placement belongs to the medium.** The book puts boxes after the
55
+ * authored body, Foundry appends one page per box, and the website uses
56
+ * a side rail on wide screens and inline boxes on narrow screens.
57
57
  * 2. **It contains no image.** A picture is authored in the text with its own
58
58
  * directive, and its position governs. {@link NOTE_FIELD_PRESENTATION}
59
59
  * withholds the art slots and `overlay` for that reason and no other.
@@ -335,16 +335,13 @@ export function buildPages(rawPages, entryId, noteName, captions) {
335
335
  * entry: a module may ship the same content for two systems, and each pack's
336
336
  * documents record the system version they were built against. A
337
337
  * caller with no pack in hand gets the package-wide block.
338
- * @param {string} [params.infobox] - The note's infobox, already rendered to
339
- * HTML. Prepended to the entry's first page, which is where the format puts
340
- * it: the box is generated content in document order, before the prose, and
341
- * a Foundry page is narrow enough that inlining it is the only arrangement
342
- * that reads. It is **not** a page of its own — a page is what a UUID
343
- * addresses, and a summary a reader has to navigate to is a summary they do
344
- * not see.
338
+ * @param {Array<{id: string, name: string, html: string}>} [params.infoboxes] -
339
+ * Emitted boxes, each rendered to HTML. Each becomes a separate page after
340
+ * the authored pages. Its identity uses the box id in a generated-page
341
+ * namespace, independent of authored headings and anchors.
345
342
  * @param {string} [params.notice] - A statement about the entry rather than
346
343
  * about its subject, already rendered to HTML — see {@link
347
- * module:engine/draft-notice}. It leads the first page, ahead of the infobox,
344
+ * module:engine/draft-notice}. It leads the first authored page,
348
345
  * because a reader deciding whether to rely on the entry has to be told
349
346
  * before they read it rather than after.
350
347
  * @returns {object} The JournalEntry document, keyed for the pack.
@@ -357,13 +354,21 @@ export function buildJournalEntry({
357
354
  folder = null,
358
355
  flags,
359
356
  stats = defaultStats(),
360
- infobox = "",
357
+ infoboxes = [],
361
358
  notice = "",
362
359
  }) {
363
360
  const rawPages = splitPages(markdown, leadName);
364
361
  const pages = buildPages(rawPages, id, name, scanCaptions(markdown).captions);
365
- if (infobox.trim() && pages.length) {
366
- pages[0].text.content = `${infobox}\n${pages[0].text.content}`;
362
+ for (const box of infoboxes) {
363
+ const pageId = makeId("journal-infobox-page", `${id}:${box.id}`);
364
+ pages.push({
365
+ _id: pageId,
366
+ name: box.name,
367
+ type: "text",
368
+ title: { show: true, level: 1 },
369
+ text: { format: 1, content: box.html },
370
+ _key: `!journal.pages!${id}.${pageId}`,
371
+ });
367
372
  }
368
373
  if (notice.trim() && pages.length) {
369
374
  pages[0].text.content = `${notice}\n${pages[0].text.content}`;
@@ -522,7 +527,15 @@ export class Journals extends BasePackCompiler {
522
527
  id,
523
528
  name,
524
529
  markdown,
525
- infobox: infoboxesToHtml(boxes, { link: linkToUuid }),
530
+ infoboxes: boxes.map((box) => ({
531
+ id: box.id,
532
+ name: `${
533
+ box.id === "note" ? "Properties"
534
+ : box.id === "sohl" ? "SoHL"
535
+ : "HM3"
536
+ } Infobox`,
537
+ html: infoboxesToHtml([box], { link: linkToUuid }),
538
+ })),
526
539
  // Whether the entry is settled, asked of the tag through the one
527
540
  // reader of it. This sits here rather than in `buildJournalEntry`
528
541
  // for the reason `skipNote` runs first: a note with no prose
@@ -639,9 +639,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
639
639
  anchorPrefix,
640
640
  captions: numberedCaptions,
641
641
  });
642
- // The infobox is generated content in document order — prepended,
643
- // before the prose. An image the note authored ahead of it still comes
644
- // first, because the image lives in the body and the body follows.
642
+ // Infoboxes follow the authored body within this note's entry.
645
643
  const boxes = noteInfoboxes(page.fm, {
646
644
  resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
647
645
  router: routerFor(resolved),
@@ -651,10 +649,9 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
651
649
  link: (value) => linkToTypst(value, plan.links, labelFor),
652
650
  });
653
651
  // Whether the entry is settled comes before anything it says: the
654
- // notice leads the leaf, ahead of the panel, which is the position it
655
- // holds on every other surface.
652
+ // notice leads the leaf, before the authored body.
656
653
  const notice = isDraftNote(page.fm) ? draftNoticeTypst() : "";
657
- const parts = [notice, panel, prose].filter((part) => part);
654
+ const parts = [notice, prose, panel].filter((part) => part);
658
655
  return parts.join("\n\n");
659
656
  };
660
657
 
@@ -984,7 +984,6 @@ export function renderPages(pages, options) {
984
984
  */
985
985
  const SITE_PASSES = Object.freeze({
986
986
  sohlKb: () => require("../sohl/kb-passes.mjs").sohlKbPass,
987
- packageDocs: () => require("./documentation-links.mjs").documentationLinksPass,
988
987
  });
989
988
 
990
989
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.18.0",
3
+ "version": "22.19.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -174,7 +174,6 @@
174
174
  },
175
175
  "devDependencies": {
176
176
  "@changesets/cli": "^3.0.0",
177
- "@heroiclands/hugo-theme": "^0.9.0",
178
177
  "@types/node": "^26.2.0",
179
178
  "npm-run-all": "^4.1.5",
180
179
  "vitest": "^5.0.0"
@@ -198,16 +197,7 @@
198
197
  "changeset": "changeset",
199
198
  "changeset:version": "changeset version && node bin/package-build.mjs changelog group && npm install --package-lock-only",
200
199
  "prepare": "git config core.hooksPath .githooks || true",
201
- "build:deps": "node bin/package-build.mjs deps fetch",
202
- "build:content-index": "node bin/package-build.mjs content-index",
203
- "build:site-content": "node bin/package-build.mjs site",
204
- "build:site-html": "hugo --source build/hugo --minify --gc --cleanDestinationDir",
205
- "build:site-root": "node bin/package-build.mjs site-root",
206
- "build:site": "run-s build:deps build:content-index build:site-content build:site-html build:site-root",
207
- "build:book": "run-s build:deps build:content-index build:book-content",
208
- "build:book-content": "node bin/package-build.mjs pdf",
209
- "serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
210
- "serve:site-html": "hugo server --source build/hugo"
200
+ "build:deps": "node bin/package-build.mjs deps fetch"
211
201
  },
212
202
  "engines": {
213
203
  "node": ">=24.0.0"
@@ -139,21 +139,18 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
139
139
  * entry: a module may ship the same content for two systems, and each pack's
140
140
  * documents record the system version they were built against. A
141
141
  * caller with no pack in hand gets the package-wide block.
142
- * @param {string} [params.infobox] - The note's infobox, already rendered to
143
- * HTML. Prepended to the entry's first page, which is where the format puts
144
- * it: the box is generated content in document order, before the prose, and
145
- * a Foundry page is narrow enough that inlining it is the only arrangement
146
- * that reads. It is **not** a page of its own — a page is what a UUID
147
- * addresses, and a summary a reader has to navigate to is a summary they do
148
- * not see.
142
+ * @param {Array<{id: string, name: string, html: string}>} [params.infoboxes] -
143
+ * Emitted boxes, each rendered to HTML. Each becomes a separate page after
144
+ * the authored pages. Its identity uses the box id in a generated-page
145
+ * namespace, independent of authored headings and anchors.
149
146
  * @param {string} [params.notice] - A statement about the entry rather than
150
147
  * about its subject, already rendered to HTML — see {@link
151
- * module:engine/draft-notice}. It leads the first page, ahead of the infobox,
148
+ * module:engine/draft-notice}. It leads the first authored page,
152
149
  * because a reader deciding whether to rely on the entry has to be told
153
150
  * before they read it rather than after.
154
151
  * @returns {object} The JournalEntry document, keyed for the pack.
155
152
  */
156
- export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infobox, notice, }: {
153
+ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infoboxes, notice, }: {
157
154
  id: string;
158
155
  name: string;
159
156
  markdown: string;
@@ -161,7 +158,11 @@ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags,
161
158
  folder?: string | null | undefined;
162
159
  flags?: object | undefined;
163
160
  stats?: object | undefined;
164
- infobox?: string | undefined;
161
+ infoboxes?: {
162
+ id: string;
163
+ name: string;
164
+ html: string;
165
+ }[] | undefined;
165
166
  notice?: string | undefined;
166
167
  }): object;
167
168
  /**
@@ -1,31 +0,0 @@
1
- /* SPDX-License-Identifier: GPL-3.0-or-later */
2
-
3
- import fs from "node:fs";
4
- import path from "node:path";
5
-
6
- import { parseMarkdownFile } from "./helpers.mjs";
7
-
8
- const LOCAL_MARKDOWN_LINK = /\[([^\]\n]+)\]\(([^\s)]+\.md(?:#[^\s)]*)?)\)/g;
9
-
10
- /** Keep repository-relative documentation links usable on addressed pages. */
11
- export function documentationLinksPass({ config, book = false }) {
12
- const root = config.paths.content;
13
- const cache = new Map();
14
- return {
15
- beforeLinks(text, page) {
16
- return text.replace(LOCAL_MARKDOWN_LINK, (whole, label, target) => {
17
- const [fileName, fragment] = target.split("#", 2);
18
- const file = path.resolve(path.dirname(page.file), fileName);
19
- if (!file.startsWith(`${root}${path.sep}`) || !fs.existsSync(file)) return whole;
20
- let address = cache.get(file);
21
- if (!address) {
22
- const { frontmatter } = parseMarkdownFile(file);
23
- if (!frontmatter?.type || !frontmatter?.shortcode) return whole;
24
- address = `/${config.contentPackage}/${frontmatter.type}-${frontmatter.shortcode}/`;
25
- cache.set(file, address);
26
- }
27
- return `[${label}](${address}${fragment && !book ? `#${fragment}` : ""})`;
28
- });
29
- },
30
- };
31
- }
@@ -1,7 +0,0 @@
1
- /** Keep repository-relative documentation links usable on addressed pages. */
2
- export function documentationLinksPass({ config, book }: {
3
- config: any;
4
- book?: boolean | undefined;
5
- }): {
6
- beforeLinks(text: any, page: any): any;
7
- };