@heroiclands/package-build 20.5.0 → 20.7.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,39 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 20.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6c10f17: **A `trees` entry's pages stay where their tree mounts them** — declaring
8
+ `subType` on a note inside `site.trees` no longer moves its published address.
9
+ A `subType` classifies a page for the site index; it never routed a tree
10
+ page's URL to begin with for a note that left it unset, and now it does not
11
+ for one that sets it either. A site that already worked around this by
12
+ avoiding `subType` on its tree notes can declare it again: those pages move
13
+ back under the tree's own section on upgrade.
14
+
15
+ ### Patch Changes
16
+
17
+ - 72daae4: **A being that declares no `sohl:` block no longer takes the site build down.**
18
+ `content-build site` failed with `unacceptable kind of an object to dump [object
19
+ Undefined]` for any `type: being` note whose front matter carried no `sohl:` key
20
+ at all, and the throw aborted the entire run rather than the one page. A note
21
+ carrying `sohl: null` had always been fine, so the failure only appeared once a
22
+ tree removed the empty key rather than emptying it. Both shapes now publish the
23
+ same page. Fixes HeroicLands/package-build#478.
24
+
25
+ ## 20.6.0
26
+
27
+ ### Minor Changes
28
+
29
+ - a2742cc: **PDF book outline** — a book's printed table of contents now lists only the
30
+ sections its document tree declares; every note stays reachable from the PDF
31
+ bookmarks panel, titled from its own name, without also cluttering the printed
32
+ contents. A note's own body headings never surface on either surface, so a note
33
+ opening with a heading that repeats its own title no longer shows a duplicate
34
+ entry in the sidebar. Every heading — printed, bookmarked or neither — is still
35
+ a working link target, including one an author never gave an explicit anchor.
36
+
3
37
  ## 20.5.0
4
38
 
5
39
  ### Minor Changes
package/docs/api.md CHANGED
@@ -714,16 +714,16 @@ The document tree a PDF is built from, and the plan it resolves to (#316). The p
714
714
 
715
715
  A note's markdown, and a document plan, rendered as Typst source. **This module emits text and reads nothing.** It takes markdown and a plan and returns a `.typ` document; the filesystem, the note bodies and the compiler that turns the result into a PDF all live in {@link module:engine/pdf-build}. That split is what lets the outline, the table of contents, every anchor and every link destination be asserted in a unit test with no renderer installed — which is most of what a book has to get right, and all of what a test can check without eyes.
716
716
 
717
- | Export | Signature | Returns | Use it when |
718
- | ----------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ |
719
- | `escapeTypst` | `function escapeTypst(text)` | {string} The same text, inert. | Escape literal text for Typst markup. |
720
- | `escapeTypstString` | `function escapeTypstString(text)` | {string} The same value, quotable. | Escape a string going inside Typst string quotes, as a `#link` URL does. |
721
- | `labelFor` | `function labelFor(anchor)` | {string} A Typst label name. | A Typst label, from a plan anchor. |
722
- | `createParser` | `function createParser(registry)` | {object} A markdown-it instance. | A markdown-it configured to parse, not to render. |
723
- | `markdownToTypst` | `function markdownToTypst(markdown, opts` | {string} Typst markup. | Render markdown as Typst content. |
724
- | `renderBook` | `renderBook({ plan, bodies, title, subtitle, front, fonts, tocDepth, version })` | {string} A complete `.typ` document. | The whole book, as one Typst document. |
725
- | `resolveDanglingLabels` | `function resolveDanglingLabels(source, findings` | {string} The same document, with no reference left dangling. | Point every internal link at a label the document actually declares. |
726
- | `iconNamesIn` | `function iconNamesIn(markdown)` | {string[]} The names, in order of appearance, with repeats. | Every icon name a body uses, so a build can resolve them once. |
717
+ | Export | Signature | Returns | Use it when |
718
+ | ----------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ |
719
+ | `escapeTypst` | `function escapeTypst(text)` | {string} The same text, inert. | Escape literal text for Typst markup. |
720
+ | `escapeTypstString` | `function escapeTypstString(text)` | {string} The same value, quotable. | Escape a string going inside Typst string quotes, as a `#link` URL does. |
721
+ | `labelFor` | `function labelFor(anchor)` | {string} A Typst label name. | A Typst label, from a plan anchor. |
722
+ | `createParser` | `function createParser(registry)` | {object} A markdown-it instance. | A markdown-it configured to parse, not to render. |
723
+ | `markdownToTypst` | `function markdownToTypst(markdown, opts` | {string} Typst markup. | Render markdown as Typst content. |
724
+ | `renderBook` | `renderBook({ plan, bodies, title, subtitle, front, fonts, version })` | {string} A complete `.typ` document. | The whole book, as one Typst document. |
725
+ | `resolveDanglingLabels` | `function resolveDanglingLabels(source, findings` | {string} The same document, with no reference left dangling. | Point every internal link at a label the document actually declares. |
726
+ | `iconNamesIn` | `function iconNamesIn(markdown)` | {string[]} The names, in order of appearance, with repeats. | Every icon name a body uses, so a build can resolve them once. |
727
727
 
728
728
  ### `engine.pdfFonts`
729
729
 
@@ -64,6 +64,7 @@
64
64
  import MarkdownIt from "markdown-it";
65
65
 
66
66
  import { iconPlugin, ICON_PATTERN } from "./content-icons.mjs";
67
+ import { slugify } from "./content-slug.mjs";
67
68
 
68
69
  /**
69
70
  * Characters that mean something to Typst's markup parser.
@@ -170,7 +171,11 @@ export function markdownToTypst(markdown, opts = {}) {
170
171
  anchorPrefix = "",
171
172
  } = opts;
172
173
  const tokens = md.parse(String(markdown ?? ""), {});
173
- return renderTokens(tokens, { links, glyphs, headingOffset, anchorPrefix });
174
+ // One map for the whole body, not one per block: a heading inside a
175
+ // blockquote or a list item shares the entry's anchor namespace with every
176
+ // other heading in the same body, because `sectionLabel` scopes by entry
177
+ // rather than by container.
178
+ return renderTokens(tokens, { links, glyphs, headingOffset, anchorPrefix, seen: new Map() });
174
179
  }
175
180
 
176
181
  /**
@@ -209,15 +214,26 @@ function renderBlock(tokens, i, out, ctx) {
209
214
  // Typst caps headings at a depth no book reaches by accident; going
210
215
  // past it would be a compile error in the middle of a 2,500-entry
211
216
  // run, so it clamps and keeps setting.
212
- const level = Math.min(6, Number(token.tag.slice(1)) + ctx.headingOffset);
217
+ const level = Math.max(1, Math.min(6, Number(token.tag.slice(1)) + ctx.headingOffset));
213
218
  const inline = tokens[i + 1];
214
219
  // `## Appearance {#appearance}` declares an addressable section. The
215
220
  // journals compiler strips the suffix and surfaces it as an anchor;
216
221
  // so does this, because a book that printed the braces would show
217
222
  // every reader the markup that makes a link work.
218
223
  const { text, anchor } = splitHeadingAnchor(inline, ctx);
219
- const label = anchor ? ` <${sectionLabel(ctx.anchorPrefix, anchor)}>` : "";
220
- out.push(`\n${"=".repeat(Math.max(1, level))} ${text}${label}\n\n`);
224
+ // A heading with no authored anchor still needs a link target, so one
225
+ // is derived from its own text. `anchorFor` keeps it from colliding
226
+ // with an authored anchor, or with another derived one, that lands on
227
+ // the same words later in the same entry.
228
+ const base = anchor || slugify(plainHeadingText(inline)) || "heading";
229
+ const unique = anchorFor(base, ctx.seen);
230
+ const label = ` <${sectionLabel(ctx.anchorPrefix, unique)}>`;
231
+ // A body heading is never printed and never bookmarked — it is
232
+ // structure a reader reaches only by following a link, not a
233
+ // destination either outline offers on its own.
234
+ out.push(
235
+ `\n#heading(level: ${level}, outlined: false, bookmarked: false)[${text}]${label}\n\n`,
236
+ );
221
237
  return 3;
222
238
  }
223
239
  case "paragraph_open": {
@@ -285,6 +301,44 @@ function splitHeadingAnchor(inline, ctx) {
285
301
  return { text: renderInline({ ...inline, children }, ctx), anchor: match[2] };
286
302
  }
287
303
 
304
+ /**
305
+ * A heading's text, unescaped and with any `{#anchor}` suffix still attached.
306
+ *
307
+ * Used only to derive an anchor when the author wrote none, so it wants the
308
+ * words as typed rather than the Typst-escaped, suffix-stripped text
309
+ * {@link splitHeadingAnchor} renders — {@link module:engine/content-slug.slugify}
310
+ * normalises punctuation and case itself and has no use for an escape
311
+ * backslash.
312
+ *
313
+ * @param {object} inline - The heading's `inline` token.
314
+ * @returns {string} The heading's raw text.
315
+ */
316
+ function plainHeadingText(inline) {
317
+ const children = inline?.children ?? [];
318
+ return children.map((child) => child.content ?? "").join("");
319
+ }
320
+
321
+ /**
322
+ * A unique anchor within one render pass, suffixed when the base repeats.
323
+ *
324
+ * A derived anchor is only as good as its uniqueness: two headings reading
325
+ * "Notes" in one entry, or a derived "description" landing on an author's own
326
+ * `{#description}`, would otherwise give one label two meanings. First use of
327
+ * a base anchor keeps it exactly as written or slugified; every later use in
328
+ * the same body is suffixed, in the order headings are walked — which is
329
+ * stable across rebuilds because the body's markdown is.
330
+ *
331
+ * @param {string} base - The preferred anchor.
332
+ * @param {Map<string, number>} seen - How many times each base has been used,
333
+ * scoped to one call to {@link markdownToTypst}.
334
+ * @returns {string} The anchor.
335
+ */
336
+ function anchorFor(base, seen) {
337
+ const n = (seen.get(base) ?? 0) + 1;
338
+ seen.set(base, n);
339
+ return n === 1 ? base : `${base}-${n}`;
340
+ }
341
+
288
342
  /**
289
343
  * A label for an anchor declared inside a note.
290
344
  *
@@ -612,25 +666,39 @@ function renderIcon(token, ctx) {
612
666
  * with no filesystem and no compiler. {@link module:engine/pdf-build} supplies
613
667
  * both and runs Typst over the result.
614
668
  *
615
- * ## Two outlines, and why they are not the same outline
669
+ * ## Three surfaces, one heading tree
616
670
  *
617
671
  * A roster of 2,500 entries wants every entry reachable from a viewer's
618
672
  * sidebar, and emphatically does not want all 2,500 printed in the front
619
- * matter: that is forty pages of contents before the book starts.
620
- *
621
- * Typst separates the two for us. **The PDF bookmark outline is built from
622
- * every heading**, so each entry gets its own node at its own depth for free
623
- * and the sidebar is the navigational interface the issue asks for.
624
- * **`#outline()` prints only to `tocDepth`**, so the paper table of contents
625
- * stays the sections. Both are page-numbered and both are links.
673
+ * matter: that is forty pages of contents before the book starts. It also
674
+ * wants every heading in a note's own body to keep working as a link target,
675
+ * without appearing on either surface the anchor an author writes for
676
+ * `[[note#appearance]]` is structure, not a destination either outline offers
677
+ * on its own.
678
+ *
679
+ * `heading` carries `outlined` and `bookmarked` independently, so the three
680
+ * wants are three settings rather than three passes:
681
+ *
682
+ * - A **section** — `outlined: true, bookmarked: true` — prints in the paper
683
+ * contents and the PDF sidebar alike.
684
+ * - A **note leaf**, titled from `name.full`, is `outlined: false,
685
+ * bookmarked: true`: reachable from the sidebar, absent from the printed
686
+ * contents.
687
+ * - A **body heading**, inside a note's own markdown, is `outlined: false,
688
+ * bookmarked: false`: a real heading with a label, so it still supplies a
689
+ * link target, a running head and a page break, but neither outline lists
690
+ * it. {@link markdownToTypst} emits these.
691
+ *
692
+ * `#outline()` needs no depth limit under this model: what prints is decided
693
+ * per heading, not by how deep the tree happens to go.
626
694
  *
627
695
  * ## Headings carry the structure, so nothing else has to
628
696
  *
629
697
  * Every section, every prose file and every entry is a real Typst heading at
630
- * its plan depth. That single decision supplies the bookmarks, the printed
631
- * contents, the running heads and the page breaks at once — where drawing
632
- * titles as styled text would have meant building all four by hand and keeping
633
- * them agreeing with each other.
698
+ * its plan depth. That single decision supplies both outlines, the running
699
+ * heads and the page breaks at once — where drawing titles as styled text
700
+ * would have meant building all four by hand and keeping them agreeing with
701
+ * each other.
634
702
  *
635
703
  * @param {object} opts - Options.
636
704
  * @param {object} opts.plan - From {@link module:engine/pdf-toc.planDocument}.
@@ -640,7 +708,6 @@ function renderIcon(token, ctx) {
640
708
  * @param {string} [opts.subtitle] - Shown under it on the title page.
641
709
  * @param {string[]} [opts.front] - Rendered Typst for each front-matter file.
642
710
  * @param {object} [opts.fonts] - `{ serif, sans, mono }` family names.
643
- * @param {number} [opts.tocDepth] - How deep the *printed* contents go.
644
711
  * @param {string} [opts.version] - Stamped on the title page when given.
645
712
  * @returns {string} A complete `.typ` document.
646
713
  */
@@ -651,7 +718,6 @@ export function renderBook({
651
718
  subtitle = "",
652
719
  front = [],
653
720
  fonts = {},
654
- tocDepth = 2,
655
721
  version = "",
656
722
  } = {}) {
657
723
  const serif = fonts.serif || "Libertinus Serif";
@@ -700,7 +766,9 @@ export function renderBook({
700
766
  out.push("");
701
767
  }
702
768
 
703
- out.push(`#outline(title: [Contents], depth: ${Math.max(1, Number(tocDepth) || 2)})`);
769
+ // No `depth:` limit: what prints is decided per heading by `outlined`,
770
+ // below, not by how deep the plan's tree happens to go.
771
+ out.push("#outline(title: [Contents])");
704
772
  out.push("#pagebreak()");
705
773
  out.push("");
706
774
 
@@ -708,7 +776,12 @@ export function renderBook({
708
776
  const label = labelFor(entry.anchor);
709
777
  const depth = Math.min(6, Math.max(1, Number(entry.depth) || 1));
710
778
  if (entry.kind === "section") {
711
- out.push(`${"=".repeat(depth)} ${escapeTypst(entry.title)} <${label}>`);
779
+ // A declared `sectionName:` — the structure the printed contents
780
+ // shows and the bookmarks panel shows alongside it.
781
+ out.push(
782
+ `#heading(level: ${depth}, outlined: true, bookmarked: true)` +
783
+ `[${escapeTypst(entry.title)}] <${label}>`,
784
+ );
712
785
  out.push("");
713
786
  continue;
714
787
  }
@@ -721,8 +794,13 @@ export function renderBook({
721
794
  out.push("");
722
795
  continue;
723
796
  }
797
+ // A note leaf: reachable from the bookmarks panel, titled from
798
+ // `name.full`, and never printed in the paper contents.
724
799
  const name = entry.record?.name?.full ?? entry.record?.address?.slug ?? "(untitled)";
725
- out.push(`${"=".repeat(Math.min(6, depth + 1))} ${escapeTypst(name)} <${label}>`);
800
+ out.push(
801
+ `#heading(level: ${Math.min(6, depth + 1)}, outlined: false, bookmarked: true)` +
802
+ `[${escapeTypst(name)}] <${label}>`,
803
+ );
726
804
  out.push("");
727
805
  const body = bodies.get(entry.anchor);
728
806
  if (body) {
@@ -267,6 +267,14 @@ export function collectContentPages(contentBase, ctx) {
267
267
  * addressed by type and slug: they are a book with chapters, and a reader
268
268
  * follows their paths. A `README` is its directory's landing.
269
269
  *
270
+ * **The section is the tree's, never the note's.** `tree.section` is the
271
+ * mount point a `trees` entry configures — fixed, physical, and the same
272
+ * value `site-index.mjs` indexes a tree page's address under. A note's own
273
+ * `subType` is a genre and reaches no address, the same contract
274
+ * `packageAddress()` holds for a content page: reading it here would move a
275
+ * page's URL, its file destination (`pageDestination`) and the address a
276
+ * wikilink cites it by, every time an author classified it.
277
+ *
270
278
  * @param {object} tree - `{ from, rel, section, route }`.
271
279
  * @param {object} ctx - `{ mount }`.
272
280
  * @returns {{pages: object[], fmLinkFindings: object[]}}
@@ -287,7 +295,7 @@ export function collectTreePages(tree, ctx) {
287
295
  const rel = path.relative(tree.from, file).replace(/\\/g, "/");
288
296
  const base = path.basename(rel);
289
297
  const isReadme = base.toLowerCase() === "readme.md";
290
- const sec = fm.subType ?? tree.section;
298
+ const sec = tree.section;
291
299
  const h1 = /^#\s+(.+?)\s*$/m.exec(body);
292
300
  const h1Title = h1 ? h1[1].replace(/\{@link\s+[^}]*\}/g, "").trim() : null;
293
301
  const name = fm.name?.full ?? fm.title ?? h1Title ?? path.basename(base, ".md");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "20.5.0",
3
+ "version": "20.7.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",
@@ -154,6 +154,7 @@
154
154
  "devDependencies": {
155
155
  "@changesets/cli": "^3.0.0",
156
156
  "@types/node": "^26.2.0",
157
+ "npm-run-all": "^4.1.5",
157
158
  "vitest": "^5.0.0"
158
159
  },
159
160
  "scripts": {
@@ -163,7 +164,7 @@
163
164
  "build:types": "tsc -p tsconfig.dts.json",
164
165
  "format": "prettier --write .",
165
166
  "format:check": "prettier --check .",
166
- "lint": "npm run format:check && npm run lint:markdown && npm run lint:yaml && npm run lint:labels && npm run lint:content-format",
167
+ "lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:content-format",
167
168
  "lint:markdown": "node bin/content-build.mjs markdown",
168
169
  "lint:yaml": "node bin/package-build.mjs yaml",
169
170
  "lint:labels": "node bin/package-build.mjs labels check",
@@ -108,11 +108,17 @@ const nonEmpty = (v) => Array.isArray(v) && v.length > 0;
108
108
  * @param {object|null|undefined} sohl - The note's `sohl` frontmatter block.
109
109
  * @param {Map<string, {name?: string, url?: string}>} index - Content index,
110
110
  * `"<type>:<shortcode>"` → the item's page.
111
- * @returns {object|null|undefined} The block with its info-block fields filled
112
- * in, or the input unchanged when there is nothing to derive from.
111
+ * @returns {object|null} The block with its info-block fields filled in, or the
112
+ * input unchanged when there is nothing to derive from — with an absent block
113
+ * reported as `null`, the value an empty one already carries.
113
114
  */
114
115
  export function deriveBeingInfo(sohl, index) {
115
- if (!isMap(sohl)) return sohl;
116
+ // A note declaring no `sohl:` key at all arrives as `undefined`, and the
117
+ // site emitter assigns this result straight into a page's front matter.
118
+ // js-yaml refuses to dump a property whose value is `undefined`, and the
119
+ // throw aborts the whole build rather than the one page — so "no block"
120
+ // is answered with the same `null` an empty block gets.
121
+ if (!isMap(sohl)) return sohl ?? null;
116
122
  const out = { ...sohl };
117
123
  const items = Array.isArray(out.items) ? out.items : [];
118
124
  if (items.length === 0) return out;
@@ -70,25 +70,39 @@ export function markdownToTypst(markdown: string, opts?: {
70
70
  * with no filesystem and no compiler. {@link module:engine/pdf-build} supplies
71
71
  * both and runs Typst over the result.
72
72
  *
73
- * ## Two outlines, and why they are not the same outline
73
+ * ## Three surfaces, one heading tree
74
74
  *
75
75
  * A roster of 2,500 entries wants every entry reachable from a viewer's
76
76
  * sidebar, and emphatically does not want all 2,500 printed in the front
77
- * matter: that is forty pages of contents before the book starts.
78
- *
79
- * Typst separates the two for us. **The PDF bookmark outline is built from
80
- * every heading**, so each entry gets its own node at its own depth for free
81
- * and the sidebar is the navigational interface the issue asks for.
82
- * **`#outline()` prints only to `tocDepth`**, so the paper table of contents
83
- * stays the sections. Both are page-numbered and both are links.
77
+ * matter: that is forty pages of contents before the book starts. It also
78
+ * wants every heading in a note's own body to keep working as a link target,
79
+ * without appearing on either surface the anchor an author writes for
80
+ * `[[note#appearance]]` is structure, not a destination either outline offers
81
+ * on its own.
82
+ *
83
+ * `heading` carries `outlined` and `bookmarked` independently, so the three
84
+ * wants are three settings rather than three passes:
85
+ *
86
+ * - A **section** — `outlined: true, bookmarked: true` — prints in the paper
87
+ * contents and the PDF sidebar alike.
88
+ * - A **note leaf**, titled from `name.full`, is `outlined: false,
89
+ * bookmarked: true`: reachable from the sidebar, absent from the printed
90
+ * contents.
91
+ * - A **body heading**, inside a note's own markdown, is `outlined: false,
92
+ * bookmarked: false`: a real heading with a label, so it still supplies a
93
+ * link target, a running head and a page break, but neither outline lists
94
+ * it. {@link markdownToTypst} emits these.
95
+ *
96
+ * `#outline()` needs no depth limit under this model: what prints is decided
97
+ * per heading, not by how deep the tree happens to go.
84
98
  *
85
99
  * ## Headings carry the structure, so nothing else has to
86
100
  *
87
101
  * Every section, every prose file and every entry is a real Typst heading at
88
- * its plan depth. That single decision supplies the bookmarks, the printed
89
- * contents, the running heads and the page breaks at once — where drawing
90
- * titles as styled text would have meant building all four by hand and keeping
91
- * them agreeing with each other.
102
+ * its plan depth. That single decision supplies both outlines, the running
103
+ * heads and the page breaks at once — where drawing titles as styled text
104
+ * would have meant building all four by hand and keeping them agreeing with
105
+ * each other.
92
106
  *
93
107
  * @param {object} opts - Options.
94
108
  * @param {object} opts.plan - From {@link module:engine/pdf-toc.planDocument}.
@@ -98,18 +112,16 @@ export function markdownToTypst(markdown: string, opts?: {
98
112
  * @param {string} [opts.subtitle] - Shown under it on the title page.
99
113
  * @param {string[]} [opts.front] - Rendered Typst for each front-matter file.
100
114
  * @param {object} [opts.fonts] - `{ serif, sans, mono }` family names.
101
- * @param {number} [opts.tocDepth] - How deep the *printed* contents go.
102
115
  * @param {string} [opts.version] - Stamped on the title page when given.
103
116
  * @returns {string} A complete `.typ` document.
104
117
  */
105
- export function renderBook({ plan, bodies, title, subtitle, front, fonts, tocDepth, version, }?: {
118
+ export function renderBook({ plan, bodies, title, subtitle, front, fonts, version, }?: {
106
119
  plan: object;
107
120
  bodies: Map<string, string>;
108
121
  title: string;
109
122
  subtitle?: string | undefined;
110
123
  front?: string[] | undefined;
111
124
  fonts?: object | undefined;
112
- tocDepth?: number | undefined;
113
125
  version?: string | undefined;
114
126
  }): string;
115
127
  /**
@@ -36,6 +36,14 @@ export function collectContentPages(contentBase: string, ctx: object): {
36
36
  * addressed by type and slug: they are a book with chapters, and a reader
37
37
  * follows their paths. A `README` is its directory's landing.
38
38
  *
39
+ * **The section is the tree's, never the note's.** `tree.section` is the
40
+ * mount point a `trees` entry configures — fixed, physical, and the same
41
+ * value `site-index.mjs` indexes a tree page's address under. A note's own
42
+ * `subType` is a genre and reaches no address, the same contract
43
+ * `packageAddress()` holds for a content page: reading it here would move a
44
+ * page's URL, its file destination (`pageDestination`) and the address a
45
+ * wikilink cites it by, every time an author classified it.
46
+ *
39
47
  * @param {object} tree - `{ from, rel, section, route }`.
40
48
  * @param {object} ctx - `{ mount }`.
41
49
  * @returns {{pages: object[], fmLinkFindings: object[]}}
@@ -26,13 +26,14 @@ export function isBeing(fm: {
26
26
  * @param {object|null|undefined} sohl - The note's `sohl` frontmatter block.
27
27
  * @param {Map<string, {name?: string, url?: string}>} index - Content index,
28
28
  * `"<type>:<shortcode>"` → the item's page.
29
- * @returns {object|null|undefined} The block with its info-block fields filled
30
- * in, or the input unchanged when there is nothing to derive from.
29
+ * @returns {object|null} The block with its info-block fields filled in, or the
30
+ * input unchanged when there is nothing to derive from — with an absent block
31
+ * reported as `null`, the value an empty one already carries.
31
32
  */
32
33
  export function deriveBeingInfo(sohl: object | null | undefined, index: Map<string, {
33
34
  name?: string;
34
35
  url?: string;
35
- }>): object | null | undefined;
36
+ }>): object | null;
36
37
  /**
37
38
  * The note `type` whose pages carry a being info block.
38
39
  *