@heroiclands/package-build 17.2.0 → 18.1.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 +1741 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -60
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +225 -127
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +55 -83
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +146 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +107 -24
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/manifest.mjs +43 -2
- package/package.json +18 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +13 -4
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +13 -7
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +65 -4
- package/sohl/items.mjs +44 -258
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +54 -49
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +2 -43
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +105 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +41 -5
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
package/engine/content-index.mjs
CHANGED
|
@@ -61,6 +61,28 @@
|
|
|
61
61
|
* Foundry data root, so anything left there ships inside the installed system
|
|
62
62
|
* to every player, and a build artifact has no business there.
|
|
63
63
|
*
|
|
64
|
+
* **What it deliberately does not carry: the note's text, and positions within
|
|
65
|
+
* it.** #243 asks whether the index should record a position for every
|
|
66
|
+
* frontmatter key, so that a pass reading the index could report a field defect
|
|
67
|
+
* without opening the file. It should not, and the numbers are not close: over
|
|
68
|
+
* `sohl`'s 1,685 notes the index is 3.0 MB and holds 50,598 leaf values, so a
|
|
69
|
+
* `{line, column}` on each would add roughly 1.6 MB — **a 54% larger artifact**
|
|
70
|
+
* — to carry data that is only ever read on the *failing* path.
|
|
71
|
+
*
|
|
72
|
+
* The rule that replaces it is the one this module was already built on:
|
|
73
|
+
* **the index carries what is _about_ a note; the file carries the note's text
|
|
74
|
+
* and every position within it.** Any pass needing either opens the file whose
|
|
75
|
+
* path the record already names ({@link noteFile}). That costs nothing it was
|
|
76
|
+
* not already paying — a check reads each note once for its body, and a
|
|
77
|
+
* compiler must read the prose regardless, so while it holds the bytes a
|
|
78
|
+
* position is free. Recording positions would charge every build, and every
|
|
79
|
+
* reader of the artifact, for something the passes that want them get for
|
|
80
|
+
* nothing.
|
|
81
|
+
*
|
|
82
|
+
* The exception proves the rule: an **anchor** carries its `line`, because an
|
|
83
|
+
* anchor is a fact about the note's structure that a consumer addresses
|
|
84
|
+
* directly, not a locator for a diagnostic about a key.
|
|
85
|
+
*
|
|
64
86
|
* @module
|
|
65
87
|
*/
|
|
66
88
|
|
|
@@ -69,111 +91,58 @@ import path from "node:path";
|
|
|
69
91
|
|
|
70
92
|
import unidecode from "unidecode";
|
|
71
93
|
|
|
72
|
-
import {
|
|
73
|
-
import { canonicalKey } from "./
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
import { metadataFileName } from "./metadata-index.mjs";
|
|
95
|
+
import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
96
|
+
// One reader for a note's anchors, shared with the link checker and with the
|
|
97
|
+
// builds that emit a link (#243). Re-exported because this is where callers
|
|
98
|
+
// have always addressed it.
|
|
99
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
100
|
+
import { subtypeRow, NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
|
|
101
|
+
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
|
|
77
102
|
|
|
78
103
|
/**
|
|
79
|
-
* The
|
|
80
|
-
* itself.
|
|
104
|
+
* The `<system>` a note belongs to when it belongs to none.
|
|
81
105
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* reads the same value from either. `file` namespaces the note's place in the
|
|
86
|
-
* tree, again matching the expander's `file.*`.
|
|
106
|
+
* The specification's word, not this module's: the canonical address carries it
|
|
107
|
+
* in the same position — `harnadventures-none-being-grod` — so the index and
|
|
108
|
+
* the address say "no system" the same way (#59).
|
|
87
109
|
*
|
|
88
|
-
*
|
|
89
|
-
* notes, so the neighbouring names are close enough to a real key that a silent
|
|
90
|
-
* overwrite is a plausible future rather than a hypothetical one.
|
|
91
|
-
*
|
|
92
|
-
* @type {ReadonlyArray<string>}
|
|
93
|
-
*/
|
|
94
|
-
export const DERIVED_KEYS = Object.freeze([
|
|
95
|
-
"package",
|
|
96
|
-
"file",
|
|
97
|
-
"address",
|
|
98
|
-
"anchors",
|
|
99
|
-
"nameAscii",
|
|
100
|
-
"aliasesAscii",
|
|
101
|
-
"foundry",
|
|
102
|
-
"documentation",
|
|
103
|
-
"documents",
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* A heading, and the `{#slug}` anchor it declares.
|
|
108
|
-
*
|
|
109
|
-
* Kept identical to the pair {@link splitPages} matches, because the two must
|
|
110
|
-
* agree about what an anchor is: that pass decides which sections become
|
|
111
|
-
* addressable journal pages, and an index naming an anchor it does not produce
|
|
112
|
-
* would advertise a link that resolves nowhere. `tests/content-index.test.ts`
|
|
113
|
-
* asserts the two find the same anchors, so drift fails the suite rather than
|
|
114
|
-
* shipping.
|
|
115
|
-
*/
|
|
116
|
-
const HEADING = /^\s*(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
117
|
-
const ANCHOR = /^(.*?)\s*\{#([^}]+)\}\s*$/;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* The `{#slug}` anchors a note's body declares, with where each one sits.
|
|
121
|
-
*
|
|
122
|
-
* Only headings carrying an explicit anchor are collected. A bare `#` heading
|
|
123
|
-
* also starts a journal page, but it declares no slug, so nothing can address
|
|
124
|
-
* it with `#…` — listing it would offer a link that cannot be written.
|
|
125
|
-
*
|
|
126
|
-
* @param {string} body - The note's markdown body, frontmatter already removed.
|
|
127
|
-
* @param {number} [bodyLine] - The 1-based file line the body starts on, from
|
|
128
|
-
* `parseMarkdownFile`. Anchors are reported at their position in the **file**,
|
|
129
|
-
* so an editor can jump straight to one; passing nothing numbers from the body.
|
|
130
|
-
* @returns {Array<{slug: string, name: string, level: number, line: number}>}
|
|
131
|
-
* In document order.
|
|
110
|
+
* @type {string}
|
|
132
111
|
*/
|
|
133
|
-
export function collectAnchors(body, bodyLine = 1) {
|
|
134
|
-
const anchors = [];
|
|
135
|
-
let inCodeBlock = false;
|
|
136
|
-
const lines = String(body ?? "").split("\n");
|
|
137
112
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
113
|
+
export { collectAnchors };
|
|
114
|
+
import { entriesForNote, foundryIdentities } from "./foundry-entries.mjs";
|
|
115
|
+
import { walkMarkdownTree } from "./helpers.mjs";
|
|
116
|
+
import { resolveNoteId } from "./note-ids.mjs";
|
|
117
|
+
// The retired-field refusal and the key locator, so a note authoring a derived
|
|
118
|
+
// key is reported where it is rather than as a bare abort (#243).
|
|
119
|
+
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
120
|
+
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
121
|
+
import { loadPackConfig } from "./pack-config.mjs";
|
|
122
|
+
// The record accessors, which live apart so that a module the compilers load
|
|
123
|
+
// can read a record without importing this one and closing a cycle (#243).
|
|
124
|
+
// Re-exported because this is where callers have always addressed them.
|
|
125
|
+
import { authoredFrontmatter, DERIVED_KEYS, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
151
126
|
|
|
152
|
-
|
|
153
|
-
if (!slug) continue;
|
|
154
|
-
anchors.push({
|
|
155
|
-
slug,
|
|
156
|
-
name: anchor[1].trim(),
|
|
157
|
-
level: heading[1].length,
|
|
158
|
-
line: bodyLine + i,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return anchors;
|
|
162
|
-
}
|
|
127
|
+
export { authoredFrontmatter, DERIVED_KEYS, isNoteRecord, noteFile };
|
|
163
128
|
|
|
164
129
|
/**
|
|
165
130
|
* The address a wikilink writes to reach a note, or `null` when it has none.
|
|
166
131
|
*
|
|
167
132
|
* A wikilink target is an address: `being-aurochs` locally, or
|
|
168
133
|
* `sohl-being-aurochs` from another package (`readQualifier` also accepts
|
|
169
|
-
* `being/aurochs`, the same two fields with a different separator). Both
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
134
|
+
* `being/aurochs`, the same two fields with a different separator). Both are
|
|
135
|
+
* *partial* addresses — the canonical one this field records is
|
|
136
|
+
* `sohl-sohl-being-aurochs`, and a target that omits the system is matched
|
|
137
|
+
* with that segment wildcarded. Every form is already derivable from `type` and
|
|
138
|
+
* `shortcode`, which every record carries, plus the system the type compiles
|
|
139
|
+
* into — so this field adds no information. What it adds is the *rule*: the
|
|
140
|
+
* lowercasing, the hyphen join and the system lookup live in one place, and a
|
|
141
|
+
* consumer that reimplements them slightly differently gets a lookup that
|
|
142
|
+
* matches nothing and says nothing about why. That is a real failure, not a
|
|
143
|
+
* hypothetical one — it is precisely how a resolver keyed on a bare
|
|
144
|
+
* `type/shortcode` silently misses every canonical
|
|
145
|
+
* `pkg-system-type-shortcode` entry.
|
|
177
146
|
*
|
|
178
147
|
* Derived by the same functions the link manifest and the site build use, so an
|
|
179
148
|
* index cannot disagree with either about where a note lives.
|
|
@@ -181,8 +150,8 @@ export function collectAnchors(body, bodyLine = 1) {
|
|
|
181
150
|
* @param {Record<string, any>} frontmatter - The note's parsed frontmatter.
|
|
182
151
|
* @param {string} contentPackage - The package the tree compiles as.
|
|
183
152
|
* @returns {{slug: string, canonical: string}|null} `slug` is what goes inside
|
|
184
|
-
* `[[…]]` within this package; `canonical` is the
|
|
185
|
-
* manifest files the note under. `null` for a note with no type or no
|
|
153
|
+
* `[[…]]` within this package; `canonical` is the fully qualified key the
|
|
154
|
+
* manifest files the note under, carrying the package and the system as well. `null` for a note with no type or no
|
|
186
155
|
* shortcode, which has no address at all and is stated as such rather than
|
|
187
156
|
* left for every reader to rediscover.
|
|
188
157
|
*/
|
|
@@ -198,7 +167,12 @@ export function noteAddress(frontmatter, contentPackage) {
|
|
|
198
167
|
}
|
|
199
168
|
return {
|
|
200
169
|
slug,
|
|
201
|
-
canonical: canonicalKey(
|
|
170
|
+
canonical: canonicalKey(
|
|
171
|
+
contentPackage,
|
|
172
|
+
systemOf(frontmatter.type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
|
|
173
|
+
frontmatter.type,
|
|
174
|
+
frontmatter.shortcode,
|
|
175
|
+
),
|
|
202
176
|
};
|
|
203
177
|
}
|
|
204
178
|
|
|
@@ -355,35 +329,97 @@ function foundryEntries({ frontmatter, address, body, manifest }) {
|
|
|
355
329
|
}
|
|
356
330
|
|
|
357
331
|
/**
|
|
358
|
-
* The `foundry` block for one manifest entry
|
|
332
|
+
* The `foundry` block for one manifest entry, **keyed by the system that
|
|
333
|
+
* compiles it**.
|
|
334
|
+
*
|
|
335
|
+
* A note may declare more than one system — 2,497 of `harn-ensemble`'s carry
|
|
336
|
+
* both a `sohl:` and an `hm3:` block — and each compiles into its *own* Foundry
|
|
337
|
+
* document, of that system's document type, in that system's pack. One `uuid`
|
|
338
|
+
* on the record cannot name two documents, so it named whichever the single
|
|
339
|
+
* shipped map produced and said nothing about the other.
|
|
340
|
+
*
|
|
341
|
+
* Only `sohl` can appear today, because `KNOWN_DOCUMENT_SUBTYPE_MAPS` holds one
|
|
342
|
+
* map and #139 tracks the missing `hm3/` half. The shape is system-keyed now so
|
|
343
|
+
* that adding it is one more key rather than a second breaking change to an
|
|
344
|
+
* artifact consumers have already started reading.
|
|
345
|
+
*
|
|
346
|
+
* **Derived, and a sibling of the authored block rather than inside it.** The
|
|
347
|
+
* uuid could have been synthesized onto `sohl:`/`hm3:` themselves, but those are
|
|
348
|
+
* regions a note *authors*, and {@link DERIVED_KEYS} — which refuses a note that
|
|
349
|
+
* writes over derived data — reaches only the top level. A note authoring
|
|
350
|
+
* `sohl.uuid` would collide silently, which is the failure this index exists to
|
|
351
|
+
* stop rather than to add.
|
|
359
352
|
*
|
|
360
353
|
* @param {object|null} entry - A manifest entry.
|
|
361
|
-
* @
|
|
354
|
+
* @param {string} system - The system whose document this is.
|
|
355
|
+
* @returns {object|null} `{ [system]: { uuid?, anchors? } }`, or null when it
|
|
356
|
+
* addresses nothing.
|
|
362
357
|
*/
|
|
363
|
-
|
|
358
|
+
|
|
359
|
+
function foundryBlock(entry, system) {
|
|
364
360
|
if (!entry) return null;
|
|
365
361
|
const block = {};
|
|
366
362
|
if (entry.uuid) block.uuid = entry.uuid;
|
|
367
363
|
if (entry.anchors) block.anchors = entry.anchors;
|
|
368
|
-
|
|
364
|
+
if (!Object.keys(block).length) return null;
|
|
365
|
+
return { [system || NO_SYSTEM]: block };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Refuse a note that authors a key the index derives, and say where.
|
|
370
|
+
*
|
|
371
|
+
* **Located, because every reader of the index is now a reporter of this.**
|
|
372
|
+
* Until #243 the only pass that built a record was the emitter, so aborting
|
|
373
|
+
* with a bare message was the whole story. Now the link check and the address
|
|
374
|
+
* diff read the index too, and a bare abort in one of them reports *nothing*
|
|
375
|
+
* about the tree — the one malformed note takes every other finding with it,
|
|
376
|
+
* and the reader is handed a stack instead of a line to open. So the error
|
|
377
|
+
* carries `file` and a `position`, and a pass that collects rather than throws
|
|
378
|
+
* can emit `file:line:column: error: …` like any other finding.
|
|
379
|
+
*
|
|
380
|
+
* **`package:` keeps its own words.** It is not a name collision but a *retired
|
|
381
|
+
* field* (#56), and the correction is to delete it, not to rename it — which is
|
|
382
|
+
* what {@link module:engine/note-package.assertNoDeclaredPackage} has always
|
|
383
|
+
* said, and had no caller to say it to. Deferring to it means one message for
|
|
384
|
+
* one mistake rather than two that contradict each other about the fix.
|
|
385
|
+
*
|
|
386
|
+
* @param {object} frontmatter - The note's parsed frontmatter.
|
|
387
|
+
* @param {string} relPath - The note's path within the tree, for the message.
|
|
388
|
+
* @param {string} [absPath] - The file, read only on the failing path to locate
|
|
389
|
+
* the offending key.
|
|
390
|
+
* @param {string} [contentPackage] - The package this tree compiles as.
|
|
391
|
+
* @returns {void}
|
|
392
|
+
* @throws {Error} When the note authors a derived key. `file` and `position`
|
|
393
|
+
* ride on the error.
|
|
394
|
+
*/
|
|
395
|
+
function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
|
|
396
|
+
for (const key of DERIVED_KEYS) {
|
|
397
|
+
if (!Object.hasOwn(frontmatter ?? {}, key)) continue;
|
|
398
|
+
if (key === "package") {
|
|
399
|
+
// Throws with its own wording, and its own position.
|
|
400
|
+
assertNoDeclaredPackage(frontmatter, { absPath, configured: contentPackage });
|
|
401
|
+
}
|
|
402
|
+
const err = new Error(
|
|
403
|
+
`\`${key}:\` is derived by the content index and cannot be ` +
|
|
404
|
+
`authored — rename the frontmatter field`,
|
|
405
|
+
);
|
|
406
|
+
err.file = relPath;
|
|
407
|
+
const position = absPath ? locateFrontmatterKey(absPath, key) : undefined;
|
|
408
|
+
if (position) err.position = position;
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
369
411
|
}
|
|
370
412
|
|
|
371
413
|
export function buildIndexRecord({
|
|
372
414
|
frontmatter,
|
|
373
415
|
relPath,
|
|
416
|
+
absPath,
|
|
374
417
|
contentPackage,
|
|
375
418
|
body,
|
|
376
419
|
bodyLine,
|
|
377
420
|
manifest,
|
|
378
421
|
}) {
|
|
379
|
-
|
|
380
|
-
if (Object.hasOwn(frontmatter ?? {}, key)) {
|
|
381
|
-
throw new Error(
|
|
382
|
-
`${relPath}: \`${key}:\` is derived by the content index and ` +
|
|
383
|
-
`cannot be authored — rename the frontmatter field`,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
422
|
+
assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage);
|
|
387
423
|
|
|
388
424
|
const posix = relPath.split(path.sep).join("/");
|
|
389
425
|
const folder = posix.includes("/") ? posix.slice(0, posix.lastIndexOf("/")) : "";
|
|
@@ -405,7 +441,10 @@ export function buildIndexRecord({
|
|
|
405
441
|
...a,
|
|
406
442
|
link: address ? `${address.slug}#${a.slug}` : null,
|
|
407
443
|
})),
|
|
408
|
-
foundry: foundryBlock(
|
|
444
|
+
foundry: foundryBlock(
|
|
445
|
+
entries?.own,
|
|
446
|
+
systemOf(frontmatter?.type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
|
|
447
|
+
),
|
|
409
448
|
// Forward link to the note's documentation journal, which is its
|
|
410
449
|
// own record. Named rather than nested, because the journal is a
|
|
411
450
|
// separate document with its own address — see `buildDocRecord`.
|
|
@@ -481,30 +520,58 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
|
|
|
481
520
|
// the forward link on that record, so either end reaches the other.
|
|
482
521
|
documents: address.canonical,
|
|
483
522
|
anchors,
|
|
484
|
-
foundry: foundryBlock(entry),
|
|
523
|
+
foundry: foundryBlock(entry), // a journal: no system key
|
|
485
524
|
file,
|
|
486
525
|
})
|
|
487
526
|
);
|
|
488
527
|
}
|
|
489
528
|
|
|
490
|
-
export function collectContentIndex(
|
|
529
|
+
export function collectContentIndex(
|
|
530
|
+
contentBase,
|
|
531
|
+
{ contentPackage, skipDirectories, manifest, problems },
|
|
532
|
+
) {
|
|
491
533
|
const records = [];
|
|
492
|
-
|
|
534
|
+
// Passed through rather than defaulted away: an absent scope is the
|
|
535
|
+
// caller's omission, and `walkMarkdownTree` says so (#243).
|
|
536
|
+
const walkOpts = { skipDirectories };
|
|
493
537
|
|
|
494
538
|
for (const { frontmatter, body, bodyLine, absPath } of walkMarkdownTree(
|
|
495
539
|
contentBase,
|
|
496
540
|
walkOpts,
|
|
497
541
|
)) {
|
|
498
542
|
const fm = frontmatter ?? {};
|
|
543
|
+
// The id the note's document is filed under (#270), resolved before
|
|
544
|
+
// the record is built so the index publishes the address *and* the id
|
|
545
|
+
// that address derives.
|
|
546
|
+
resolveNoteId(fm, { pkg: contentPackage });
|
|
499
547
|
const relPath = path.relative(contentBase, absPath);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
548
|
+
let record;
|
|
549
|
+
try {
|
|
550
|
+
record = buildIndexRecord({
|
|
551
|
+
frontmatter: fm,
|
|
552
|
+
relPath,
|
|
553
|
+
absPath,
|
|
554
|
+
contentPackage,
|
|
555
|
+
body,
|
|
556
|
+
bodyLine,
|
|
557
|
+
manifest,
|
|
558
|
+
});
|
|
559
|
+
} catch (err) {
|
|
560
|
+
// No `problems` array means the caller wants the old contract: a
|
|
561
|
+
// note that cannot be recorded fails the derivation outright, which
|
|
562
|
+
// is right for the *emitter* — an index quietly missing a note
|
|
563
|
+
// would state that the note does not exist.
|
|
564
|
+
if (!problems) throw err;
|
|
565
|
+
// A reader, by contrast, reports it and carries on: one malformed
|
|
566
|
+
// note must not take every other finding in the tree with it.
|
|
567
|
+
problems.push({
|
|
568
|
+
file: absPath,
|
|
569
|
+
...(err.position ?? {}),
|
|
570
|
+
severity: "error",
|
|
571
|
+
message: String(err.message),
|
|
572
|
+
});
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
508
575
|
records.push(record);
|
|
509
576
|
|
|
510
577
|
// An item note is two documents, so it is two records (#239).
|
|
@@ -562,6 +629,43 @@ export function serializeContentIndex(records) {
|
|
|
562
629
|
return `${records.map((r) => JSON.stringify(r)).join("\n")}\n`;
|
|
563
630
|
}
|
|
564
631
|
|
|
632
|
+
/**
|
|
633
|
+
* The index records for a content tree, without writing anything.
|
|
634
|
+
*
|
|
635
|
+
* The half of {@link emitContentIndex} that derives rather than emits, so a
|
|
636
|
+
* pass that needs the corpus in memory — a SQL content table, the link check,
|
|
637
|
+
* and in time every reader #243 converts — builds it the same way the artifact
|
|
638
|
+
* is built, rather than by walking and parsing again with its own idea of the
|
|
639
|
+
* scope.
|
|
640
|
+
*
|
|
641
|
+
* @param {object} [opts]
|
|
642
|
+
* @param {string} [opts.contentBase] - The tree, defaulting to the configured one.
|
|
643
|
+
* @param {object} [opts.config] - Resolved configuration, defaulting to ambient.
|
|
644
|
+
* @param {readonly string[]} [opts.skipDirectories] - The walk's scope, for a
|
|
645
|
+
* caller that resolved one of its own; defaults to the resolved
|
|
646
|
+
* configuration's. Stated separately from `config` because a caller that was
|
|
647
|
+
* *handed* a scope must be able to pass it on rather than have it silently
|
|
648
|
+
* replaced by the one its configuration happens to carry (#243).
|
|
649
|
+
* @param {object[]} [opts.problems] - Supplied by a **reader**: a note that
|
|
650
|
+
* cannot be recorded is pushed here as a diagnostic and skipped, instead of
|
|
651
|
+
* aborting the derivation. Omitted, the note throws — which is the contract
|
|
652
|
+
* the emitter needs, since an index missing a note asserts that it does not
|
|
653
|
+
* exist.
|
|
654
|
+
* @returns {object[]} One record per note, plus one per documentation entry.
|
|
655
|
+
*/
|
|
656
|
+
export function indexRecordsFor({ contentBase, config, skipDirectories, problems } = {}) {
|
|
657
|
+
const resolved = config ?? loadPackConfig();
|
|
658
|
+
const tree = contentBase ?? resolved.paths.content;
|
|
659
|
+
if (!fs.existsSync(tree)) throw new Error(`no content tree at ${tree}`);
|
|
660
|
+
return collectContentIndex(tree, {
|
|
661
|
+
contentPackage: resolved.contentPackage,
|
|
662
|
+
skipDirectories: skipDirectories ?? resolved.skipDirectories,
|
|
663
|
+
// Only the identities a UUID is a function of — see emitContentIndex.
|
|
664
|
+
manifest: foundryIdentities(resolved),
|
|
665
|
+
problems,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
565
669
|
/**
|
|
566
670
|
* Emit this package's content index.
|
|
567
671
|
*
|
|
@@ -597,13 +701,7 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
597
701
|
// publishes pages is no part of an address, and depending on it would make
|
|
598
702
|
// the index refuse to build for a configuration that is perfectly able to
|
|
599
703
|
// state one.
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
const records = collectContentIndex(tree, {
|
|
603
|
-
contentPackage,
|
|
604
|
-
skipDirectories: resolved.skipDirectories,
|
|
605
|
-
manifest,
|
|
606
|
-
});
|
|
704
|
+
const records = indexRecordsFor({ contentBase: tree, config: resolved });
|
|
607
705
|
if (records.length === 0) {
|
|
608
706
|
throw new Error(
|
|
609
707
|
`${tree} yielded no notes, so the index would state that this ` +
|
|
@@ -612,7 +710,7 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
612
710
|
}
|
|
613
711
|
|
|
614
712
|
const text = serializeContentIndex(records);
|
|
615
|
-
const file = path.join(dir,
|
|
713
|
+
const file = path.join(dir, metadataFileName(contentPackage));
|
|
616
714
|
fs.mkdirSync(dir, { recursive: true });
|
|
617
715
|
fs.writeFileSync(file, text);
|
|
618
716
|
|