@heroiclands/package-build 22.14.1 → 22.14.3
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 +26 -0
- package/bin/content-build.mjs +6 -6
- package/bin/package-build.mjs +10 -0
- package/config.mjs +18 -1
- package/content-config.mjs +2 -2
- package/docs/api.md +63 -35
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +157 -181
- package/docs/getting-started.md +42 -11
- package/docs/project-setup.md +5 -6
- package/engine/address.mjs +3 -5
- package/engine/asset-bindings.mjs +66 -0
- package/engine/calendar-notes.mjs +74 -0
- package/engine/changelog-lint.mjs +2 -2
- package/engine/content-index.mjs +63 -36
- package/engine/content-lint.mjs +1 -2
- package/engine/content-package.mjs +2 -2
- package/engine/content-tables.mjs +4 -8
- package/engine/folder-notes.mjs +1 -2
- package/engine/foundry-entries.mjs +21 -6
- package/engine/frontmatter-lint.mjs +3 -4
- package/engine/frontmatter.mjs +5 -8
- package/engine/helpers.mjs +3 -4
- package/engine/index.mjs +3 -0
- package/engine/infobox.mjs +67 -2
- package/engine/item-registry.mjs +2 -2
- package/engine/note-vocabulary.mjs +92 -6
- package/engine/pack-config.mjs +2 -2
- package/engine/pack-router.mjs +2 -2
- package/engine/pdf-render.mjs +1 -1
- package/engine/pdf-toc.mjs +3 -3
- package/engine/retired-fields.mjs +1 -1
- package/engine/scene-levels.mjs +4 -4
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +134 -121
- package/engine/site-preview.mjs +293 -0
- package/engine/site-root.mjs +20 -2
- package/engine/social-tie-terms.mjs +11 -0
- package/engine/social-ties.mjs +84 -0
- package/engine/sql-tables.mjs +4 -9
- package/hm3/actors.mjs +1 -1
- package/package.json +3 -3
- package/sohl/item-fields.mjs +2 -2
- package/types/config.d.mts +8 -0
- package/types/content-config.d.mts +1 -1
- package/types/engine/address.d.mts +3 -5
- package/types/engine/asset-bindings.d.mts +6 -0
- package/types/engine/calendar-notes.d.mts +12 -0
- package/types/engine/content-index.d.mts +11 -0
- package/types/engine/content-package.d.mts +2 -2
- package/types/engine/folder-notes.d.mts +1 -2
- package/types/engine/frontmatter-lint.d.mts +1 -2
- package/types/engine/frontmatter.d.mts +2 -4
- package/types/engine/helpers.d.mts +3 -4
- package/types/engine/index.d.mts +1 -0
- package/types/engine/item-registry.d.mts +2 -2
- package/types/engine/note-vocabulary.d.mts +5 -6
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/pdf-render.d.mts +1 -1
- package/types/engine/pdf-toc.d.mts +1 -1
- package/types/engine/site-build.d.mts +36 -3
- package/types/engine/site-preview.d.mts +34 -0
- package/types/engine/social-tie-terms.d.mts +5 -0
- package/types/engine/social-ties.d.mts +10 -0
package/docs/getting-started.md
CHANGED
|
@@ -35,6 +35,13 @@ and this tutorial links to them rather than repeating them:
|
|
|
35
35
|
| [`project-setup.md`](project-setup.md) | The files a repository carries beyond the configuration, and the script wiring. |
|
|
36
36
|
| [`api.md`](api.md) | The programmatic surface, for a repository with a build script of its own. |
|
|
37
37
|
|
|
38
|
+
An editor can render one note from its current buffer through the
|
|
39
|
+
[`engine.sitePreview` API](api.md#enginesitepreview). The context reads saved
|
|
40
|
+
notes and dependency indexes once, while each render overlays the active note's
|
|
41
|
+
text and frontmatter. It returns generated Markdown and frontmatter for an HTML
|
|
42
|
+
preview, or located findings if the page cannot be rendered. The editor keeps
|
|
43
|
+
its own preview artifact and calls `refresh()` after a save.
|
|
44
|
+
|
|
38
45
|
## The shape of what you are building
|
|
39
46
|
|
|
40
47
|
Nine files, and a directory the build writes:
|
|
@@ -270,9 +277,9 @@ requires it. It compiles to a page and to no Foundry document.
|
|
|
270
277
|
|
|
271
278
|
```markdown
|
|
272
279
|
---
|
|
273
|
-
type: homepage
|
|
274
280
|
shortcode: root
|
|
275
|
-
|
|
281
|
+
name: { full: The Acme Bestiary }
|
|
282
|
+
type: homepage
|
|
276
283
|
description: Creatures of the reed flats, their habits and their hides.
|
|
277
284
|
---
|
|
278
285
|
|
|
@@ -286,11 +293,10 @@ who hunt it.
|
|
|
286
293
|
|
|
287
294
|
```markdown
|
|
288
295
|
---
|
|
296
|
+
shortcode: marshdrake
|
|
297
|
+
name: { full: Marsh Drake }
|
|
289
298
|
type: lore
|
|
290
299
|
subType: bestiary
|
|
291
|
-
name:
|
|
292
|
-
full: Marsh Drake
|
|
293
|
-
shortcode: marshdrake
|
|
294
300
|
description: A wingless drake of the reed flats, hunted for its hide and feared for its patience.
|
|
295
301
|
---
|
|
296
302
|
|
|
@@ -317,20 +323,45 @@ directory has no bearing on which pack it lands in. `subType:` narrows it —
|
|
|
317
323
|
`(type, subType)` is what each game system maps onto its own document type.
|
|
318
324
|
|
|
319
325
|
**`shortcode:` is the note's address.** Cross-references are written
|
|
320
|
-
`[[lore-marshdrake]]` — the type and
|
|
326
|
+
`[[lore-marshdrake|Marsh Drake]]` — the type, shortcode, and readable label — and resolve to whatever
|
|
321
327
|
that note compiles into, in whatever system is being compiled. A shortcode is
|
|
322
328
|
identity: renaming one breaks every link into it.
|
|
323
329
|
|
|
324
330
|
**`name.full` is the document name and the published URL**, derived by one
|
|
325
331
|
shared rule. There is no authored slug anywhere in this toolchain.
|
|
326
332
|
|
|
327
|
-
**Frontmatter has three regions
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
333
|
+
**Frontmatter has three closed regions.** Top-level keys identify the note and
|
|
334
|
+
describe its publication; `data:` holds shared facts; `sohl:` and `hm3:` hold
|
|
335
|
+
system-specific facts. An unrecognised key is an error rather than a site
|
|
336
|
+
parameter. The formatter keeps the allowed top-level keys in their required
|
|
337
|
+
order.
|
|
332
338
|
[`content-format.md`](content-format.md) is the specification.
|
|
333
339
|
|
|
340
|
+
**A setting guide is a `doc` that orients a reader to a place or setting.** It
|
|
341
|
+
gives a usable overview of areas, peoples, institutions, and daily life, then
|
|
342
|
+
links to the detailed notes. Use `subType: settingguide` for that entry point;
|
|
343
|
+
`concept` explains one subject, `reference` serves lookup, and `userguide`
|
|
344
|
+
explains the Foundry controls. For example, a guide at
|
|
345
|
+
`assets/content/Guides/Reed_Flats.md` can begin:
|
|
346
|
+
|
|
347
|
+
```markdown
|
|
348
|
+
---
|
|
349
|
+
shortcode: reedflatsguide
|
|
350
|
+
name: { full: The Reed Flats Guide }
|
|
351
|
+
type: doc
|
|
352
|
+
subType: settingguide
|
|
353
|
+
description: A starting point for adventures in the Reed Flats.
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
The river trade links the settlements of the Reed Flats. Their routes and
|
|
357
|
+
customs give visitors a way into the region. See [[lore-reedfolk|Reed Folk]]
|
|
358
|
+
for the people who guide those journeys.
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The guide can include SQL tables for lists drawn from indexed notes and
|
|
362
|
+
`:::secret` blocks for GM passages. Its address is
|
|
363
|
+
`doc-reedflatsguide`, following the same rule as every `doc` note.
|
|
364
|
+
|
|
334
365
|
An image stands in its own paragraph. Its directive accepts a named size and a
|
|
335
366
|
position together:
|
|
336
367
|
|
package/docs/project-setup.md
CHANGED
|
@@ -27,7 +27,7 @@ acme-bestiary/
|
|
|
27
27
|
│ ├── ISSUE_REPORTING.md # §3 lists the same labels
|
|
28
28
|
│ └── workflows/
|
|
29
29
|
├── assets/
|
|
30
|
-
│ └── content/ # authored notes
|
|
30
|
+
│ └── content/ # authored Markdown notes
|
|
31
31
|
├── lang/
|
|
32
32
|
│ └── en.json
|
|
33
33
|
├── src/ # module JavaScript, if the package has any
|
|
@@ -55,11 +55,10 @@ The two that matter when something is wrong are `build/packs-json/`, which is
|
|
|
55
55
|
the compiled document before Foundry ever sees it, and `build/stage/`, which is
|
|
56
56
|
exactly what ships.
|
|
57
57
|
|
|
58
|
-
**`assets/content/`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
rather than location is what routes a note.
|
|
58
|
+
**`assets/content/` holds the source notes.** Edit the Markdown and YAML
|
|
59
|
+
frontmatter directly; a plain checkout builds without an export step. The
|
|
60
|
+
tree is arranged for readers and authors, while `type:` determines how each
|
|
61
|
+
note compiles.
|
|
63
62
|
|
|
64
63
|
## `package.json`
|
|
65
64
|
|
package/engine/address.mjs
CHANGED
|
@@ -491,11 +491,9 @@ export function readCanonicalKey(key) {
|
|
|
491
491
|
* confidently they mark a value as qualified:
|
|
492
492
|
*
|
|
493
493
|
* - **`type-shortcode`** and its qualified forms — the canonical spelling.
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
* - **`type/shortcode`** — resolved so that a link written before the vault
|
|
498
|
-
* migrated does not silently die. A slash is *unconditionally* a qualifier:
|
|
494
|
+
* The hyphenated form is portable across Markdown editors and renderers.
|
|
495
|
+
* - **`type/shortcode`** — an accepted qualified spelling. A slash is
|
|
496
|
+
* *unconditionally* a qualifier:
|
|
499
497
|
* nothing else uses one, so an unknown type before it is reported rather than
|
|
500
498
|
* guessed at. The split is at the **last** slash.
|
|
501
499
|
*
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { isAddressSegment } from "./address-charset.mjs";
|
|
7
|
+
import { canonicalKey } from "./content-address.mjs";
|
|
8
|
+
import { loadForeignIndexes, metadataRelationships } from "./metadata-index.mjs";
|
|
9
|
+
|
|
10
|
+
/** Files under a configured staging root, without provenance sidecars. */
|
|
11
|
+
function assetFiles(root) {
|
|
12
|
+
if (!fs.existsSync(root)) return [];
|
|
13
|
+
const files = [];
|
|
14
|
+
const visit = (dir) => {
|
|
15
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
const file = path.join(dir, entry.name);
|
|
17
|
+
if (entry.isDirectory()) visit(file);
|
|
18
|
+
else if (entry.isFile() && !entry.name.endsWith(".yaml")) files.push(file);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
visit(root);
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Check configured foreign Address bindings against fetched indexes. */
|
|
26
|
+
export function checkForeignAssetBindings(config, { foreignIndex, assets } = {}) {
|
|
27
|
+
const copies = (assets ?? config.packageBuild?.assets ?? []).filter((asset) => asset.bindsTo);
|
|
28
|
+
if (!copies.length) return [];
|
|
29
|
+
const relationships = metadataRelationships(config);
|
|
30
|
+
const active = copies.filter(({ bindsTo }) =>
|
|
31
|
+
relationships.some((rel) => rel.id === bindsTo.package),
|
|
32
|
+
);
|
|
33
|
+
if (!active.length) return [];
|
|
34
|
+
const fetched = foreignIndex ? null : loadForeignIndexes(config, [config.contentPackage]);
|
|
35
|
+
const foreign = foreignIndex ?? fetched.index;
|
|
36
|
+
for (const { bindsTo } of active) {
|
|
37
|
+
if (fetched && !fetched.packages.has(bindsTo.package))
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
file: "package-build.config.yaml",
|
|
41
|
+
severity: "error",
|
|
42
|
+
message: `asset binding index for ${bindsTo.package} is unavailable; run content-build deps fetch`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const { from, bindsTo } of active) {
|
|
48
|
+
const root = path.resolve(config.rootDir, from);
|
|
49
|
+
if (path.relative(config.rootDir, root).startsWith(".."))
|
|
50
|
+
throw new Error(`packageBuild.assets source ${from} escapes the repository`);
|
|
51
|
+
for (const file of assetFiles(root)) {
|
|
52
|
+
const shortcode = path.parse(file).name;
|
|
53
|
+
const key =
|
|
54
|
+
isAddressSegment(shortcode) ?
|
|
55
|
+
canonicalKey(bindsTo.package, bindsTo.system ?? "note", bindsTo.type, shortcode)
|
|
56
|
+
: null;
|
|
57
|
+
if (!key || !foreign.has(key))
|
|
58
|
+
findings.push({
|
|
59
|
+
file: path.relative(config.rootDir, file),
|
|
60
|
+
severity: "error",
|
|
61
|
+
message: `asset binding ${key ?? JSON.stringify(shortcode)} resolves to no ${bindsTo.type} in ${bindsTo.package}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
@@ -82,6 +82,27 @@ export const CALENDAR_SUBTYPE = "calendar";
|
|
|
82
82
|
*/
|
|
83
83
|
export const INVARIANT_SUBTYPES = Object.freeze(["world", "celestial"]);
|
|
84
84
|
|
|
85
|
+
/** Calendaria's display locations that a calendar note can format. */
|
|
86
|
+
export const CALENDAR_DATE_FORMAT_KEYS = Object.freeze([
|
|
87
|
+
"short",
|
|
88
|
+
"long",
|
|
89
|
+
"full",
|
|
90
|
+
"time",
|
|
91
|
+
"weekHeader",
|
|
92
|
+
"yearHeader",
|
|
93
|
+
"yearLabel",
|
|
94
|
+
"crossCalendar",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** Bare date tokens Calendaria substitutes inside authored format strings. */
|
|
98
|
+
export const CALENDAR_FORMAT_TOKENS = Object.freeze(
|
|
99
|
+
new Set(
|
|
100
|
+
"YYYY YY Y MMMM MMM MM Mo M EEEEE EEEE EEE EE E dddd ddd dd Do DDD DD D d e GGGG GGG GG G QQQQ QQQ QQ Q zzzz z ww w W HH H hh h mm m ss s A a".split(
|
|
101
|
+
" ",
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
|
|
85
106
|
/**
|
|
86
107
|
* The `data:` keys a calendar note may write — the closed list, in authored
|
|
87
108
|
* order.
|
|
@@ -129,6 +150,12 @@ export const CALENDAR_FIELDS = Object.freeze([
|
|
|
129
150
|
"The year-counts kept in this calendar. A date names one by writing " +
|
|
130
151
|
"`<calendar shortcode>.<era shortcode>`.",
|
|
131
152
|
},
|
|
153
|
+
{
|
|
154
|
+
name: "dateFormats",
|
|
155
|
+
shape: "map of short, long, full, time, weekHeader, yearHeader, yearLabel, crossCalendar strings",
|
|
156
|
+
kind: "map",
|
|
157
|
+
describe: "How this calendar writes dates and time in each display context.",
|
|
158
|
+
},
|
|
132
159
|
]);
|
|
133
160
|
|
|
134
161
|
/**
|
|
@@ -336,6 +363,51 @@ function atData(note, keyPath, severity, message) {
|
|
|
336
363
|
};
|
|
337
364
|
}
|
|
338
365
|
|
|
366
|
+
/** Check each authored format slot and every unescaped letter run in it. */
|
|
367
|
+
export function checkCalendarDateFormats(note) {
|
|
368
|
+
const formats = dataOf(note).dateFormats;
|
|
369
|
+
if (formats === undefined || formats === null) return [];
|
|
370
|
+
if (typeof formats !== "object" || Array.isArray(formats)) return [];
|
|
371
|
+
const findings = [];
|
|
372
|
+
for (const [slot, value] of Object.entries(formats)) {
|
|
373
|
+
if (!CALENDAR_DATE_FORMAT_KEYS.includes(slot)) {
|
|
374
|
+
findings.push(
|
|
375
|
+
atData(
|
|
376
|
+
note,
|
|
377
|
+
["dateFormats", slot],
|
|
378
|
+
"error",
|
|
379
|
+
`data.dateFormats.${slot} is not a calendar display format; choose ${CALENDAR_DATE_FORMAT_KEYS.join(", ")}`,
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (typeof value !== "string") {
|
|
385
|
+
findings.push(
|
|
386
|
+
atData(
|
|
387
|
+
note,
|
|
388
|
+
["dateFormats", slot],
|
|
389
|
+
"error",
|
|
390
|
+
`data.dateFormats.${slot} must be a string`,
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const bare = value.replace(/\[[^\]]*\]|\{[^}]*\}/g, " ");
|
|
396
|
+
for (const run of bare.match(/\p{L}+/gu) ?? []) {
|
|
397
|
+
if (CALENDAR_FORMAT_TOKENS.has(run)) continue;
|
|
398
|
+
findings.push(
|
|
399
|
+
atData(
|
|
400
|
+
note,
|
|
401
|
+
["dateFormats", slot],
|
|
402
|
+
"error",
|
|
403
|
+
`data.dateFormats.${slot} has unescaped letter run ${JSON.stringify(run)}; write literal words in brackets, or use a Calendaria date token`,
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return findings;
|
|
409
|
+
}
|
|
410
|
+
|
|
339
411
|
/**
|
|
340
412
|
* Check a `place` note's world facts.
|
|
341
413
|
*
|
|
@@ -488,6 +560,7 @@ export function checkCalendarNote(note, { index } = {}) {
|
|
|
488
560
|
|
|
489
561
|
findings.push(...checkMonthSum(note, index));
|
|
490
562
|
findings.push(...checkEraShortcodes(note));
|
|
563
|
+
findings.push(...checkCalendarDateFormats(note));
|
|
491
564
|
return findings;
|
|
492
565
|
}
|
|
493
566
|
|
|
@@ -747,6 +820,7 @@ export function compileCalendar({ note, invariants, contentPackage }) {
|
|
|
747
820
|
epochDayOffset:
|
|
748
821
|
epoch && months.length ? dayOfYear(months, epoch.month ?? 1, epoch.day ?? 1) - 1 : 0,
|
|
749
822
|
eras: eraEntries(data.eras),
|
|
823
|
+
dateFormats: data.dateFormats,
|
|
750
824
|
moons: moons ?? undefined,
|
|
751
825
|
metadata: written({
|
|
752
826
|
id: shortcode || undefined,
|
|
@@ -56,10 +56,10 @@ const LABEL_LINE_RE = /^\*\*([^*]+)\*\*/;
|
|
|
56
56
|
const COMMIT_HASH_RE = /^-\s+([0-9a-fA-F]{7,40})(?=[:\s]|$)/;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* An issue or pull-request reference
|
|
59
|
+
* An issue or pull-request reference, with or without an owner and repository.
|
|
60
60
|
*
|
|
61
61
|
* No leading `\b` — `#` is not a word character, so a boundary assertion
|
|
62
|
-
* immediately before it never matches
|
|
62
|
+
* immediately before it never matches a bare issue reference
|
|
63
63
|
* preceded by whitespace or punctuation.
|
|
64
64
|
*/
|
|
65
65
|
const ISSUE_REF_RE = /(?:[\w.-]+\/[\w.-]+)?#\d+\b/g;
|
package/engine/content-index.mjs
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
*/
|
|
88
88
|
|
|
89
89
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
90
|
-
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
90
|
+
import { formatDiagnostic, positionOfYamlPath } from "./diagnostics.mjs";
|
|
91
91
|
import {
|
|
92
92
|
decodeNoteAddresses,
|
|
93
93
|
decodeIndexAddresses,
|
|
@@ -101,6 +101,7 @@ import unidecode from "unidecode";
|
|
|
101
101
|
|
|
102
102
|
import { metadataFileName } from "./metadata-index.mjs";
|
|
103
103
|
import { collectAssetRecords } from "./asset-index.mjs";
|
|
104
|
+
import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
104
105
|
import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
105
106
|
import { ownDocumentSystem } from "./address.mjs";
|
|
106
107
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
@@ -360,6 +361,13 @@ function foundryEntries({ frontmatter, address, body, manifest }) {
|
|
|
360
361
|
|
|
361
362
|
function foundryBlock(entry, system) {
|
|
362
363
|
if (!entry) return null;
|
|
364
|
+
if (entry.systemUuids && Object.keys(entry.systemUuids).length)
|
|
365
|
+
return Object.fromEntries(
|
|
366
|
+
Object.entries(entry.systemUuids).map(([targetSystem, uuid]) => [
|
|
367
|
+
targetSystem,
|
|
368
|
+
{ uuid },
|
|
369
|
+
]),
|
|
370
|
+
);
|
|
363
371
|
const block = {};
|
|
364
372
|
if (entry.uuid) block.uuid = entry.uuid;
|
|
365
373
|
if (entry.anchors) block.anchors = entry.anchors;
|
|
@@ -590,6 +598,46 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
|
|
|
590
598
|
);
|
|
591
599
|
}
|
|
592
600
|
|
|
601
|
+
/** Derive every index record belonging to one parsed note. */
|
|
602
|
+
export function indexRecordsForNote({
|
|
603
|
+
frontmatter,
|
|
604
|
+
body,
|
|
605
|
+
bodyLine,
|
|
606
|
+
relPath,
|
|
607
|
+
absPath,
|
|
608
|
+
contentPackage,
|
|
609
|
+
manifest,
|
|
610
|
+
addressContext,
|
|
611
|
+
}) {
|
|
612
|
+
resolveNoteId(frontmatter, { pkg: contentPackage });
|
|
613
|
+
const record = buildIndexRecord({
|
|
614
|
+
frontmatter,
|
|
615
|
+
body,
|
|
616
|
+
bodyLine,
|
|
617
|
+
relPath,
|
|
618
|
+
absPath,
|
|
619
|
+
contentPackage,
|
|
620
|
+
manifest,
|
|
621
|
+
addressContext,
|
|
622
|
+
});
|
|
623
|
+
const records = [decodeIndexAddresses(record, addressContext ?? { package: contentPackage })];
|
|
624
|
+
const address = noteAddress(frontmatter, contentPackage);
|
|
625
|
+
const doc = foundryEntries({ frontmatter, address, body, manifest })?.doc;
|
|
626
|
+
if (doc?.key && address) {
|
|
627
|
+
records.push(
|
|
628
|
+
buildDocRecord({
|
|
629
|
+
frontmatter,
|
|
630
|
+
address,
|
|
631
|
+
entry: doc,
|
|
632
|
+
file: record.file,
|
|
633
|
+
contentPackage,
|
|
634
|
+
anchors: record.anchors ?? [],
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return records;
|
|
639
|
+
}
|
|
640
|
+
|
|
593
641
|
/**
|
|
594
642
|
* Read a content tree into index records, in the order they will be written.
|
|
595
643
|
*
|
|
@@ -648,23 +696,20 @@ export function collectContentIndex(
|
|
|
648
696
|
for (const { frontmatter, body, bodyLine, absPath } of notes) {
|
|
649
697
|
const fm = frontmatter ?? {};
|
|
650
698
|
applyComputedBeingAge(fm, present);
|
|
651
|
-
// The id the note's document is filed under, resolved before
|
|
652
|
-
// the record is built so the index publishes the address *and* the id
|
|
653
|
-
// that address derives.
|
|
654
|
-
resolveNoteId(fm, { pkg: contentPackage });
|
|
655
699
|
const relPath = path.relative(contentBase, absPath);
|
|
656
|
-
let record;
|
|
657
700
|
try {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
701
|
+
records.push(
|
|
702
|
+
...indexRecordsForNote({
|
|
703
|
+
frontmatter: fm,
|
|
704
|
+
relPath,
|
|
705
|
+
absPath,
|
|
706
|
+
contentPackage,
|
|
707
|
+
body,
|
|
708
|
+
bodyLine,
|
|
709
|
+
manifest,
|
|
710
|
+
addressContext,
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
668
713
|
} catch (err) {
|
|
669
714
|
if (fm.shortcode && !isAddressSegment(fm.shortcode) && err.keyPath) {
|
|
670
715
|
err.identity = { type: fm.type, shortcode: fm.shortcode };
|
|
@@ -693,26 +738,6 @@ export function collectContentIndex(
|
|
|
693
738
|
});
|
|
694
739
|
continue;
|
|
695
740
|
}
|
|
696
|
-
records.push(decodeIndexAddresses(record, addressContext ?? { package: contentPackage }));
|
|
697
|
-
|
|
698
|
-
// An item note is two documents, so it is two records. Derived from
|
|
699
|
-
// the address the note *would* hold rather than from the one its
|
|
700
|
-
// record carries, because a stub withholds its own address; the second
|
|
701
|
-
// record says whether the journal beside the item was made.
|
|
702
|
-
const address = noteAddress(fm, contentPackage);
|
|
703
|
-
const doc = foundryEntries({ frontmatter: fm, address, body, manifest })?.doc;
|
|
704
|
-
if (doc?.key && address) {
|
|
705
|
-
records.push(
|
|
706
|
-
buildDocRecord({
|
|
707
|
-
frontmatter: fm,
|
|
708
|
-
address,
|
|
709
|
-
entry: doc,
|
|
710
|
-
file: record.file,
|
|
711
|
-
contentPackage,
|
|
712
|
-
anchors: record.anchors ?? [],
|
|
713
|
-
}),
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
741
|
}
|
|
717
742
|
|
|
718
743
|
if (assetsBase) {
|
|
@@ -869,6 +894,8 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
869
894
|
// the index refuse to build for a configuration that is perfectly able to
|
|
870
895
|
// state one.
|
|
871
896
|
const records = indexRecordsFor({ contentBase: tree, config: resolved });
|
|
897
|
+
const assetBindings = checkForeignAssetBindings(resolved);
|
|
898
|
+
if (assetBindings.length) throw new Error(assetBindings.map(formatDiagnostic).join("\n"));
|
|
872
899
|
if (records.length === 0) {
|
|
873
900
|
throw new Error(
|
|
874
901
|
`${tree} yielded no notes, so the index would state that this ` +
|
package/engine/content-lint.mjs
CHANGED
|
@@ -48,8 +48,7 @@
|
|
|
48
48
|
*
|
|
49
49
|
* **There is deliberately no third rule** requiring every note to repeat its
|
|
50
50
|
* own `type-shortcode` address in a top-level `aliases:` list. It would serve
|
|
51
|
-
*
|
|
52
|
-
* editor — and nothing in the build reads it: both resolvers parse the hyphen
|
|
51
|
+
* no build reader: both resolvers parse the hyphen
|
|
53
52
|
* qualifier themselves. The field is retired, refused from
|
|
54
53
|
* `retired-fields.mjs`.
|
|
55
54
|
*
|
|
@@ -46,7 +46,7 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
46
46
|
* package below would differ.
|
|
47
47
|
*
|
|
48
48
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
49
|
-
* needs no configuration
|
|
49
|
+
* needs no configuration.
|
|
50
50
|
*
|
|
51
51
|
* @returns {string} The configured `contentPackage`.
|
|
52
52
|
*/
|
|
@@ -71,7 +71,7 @@ export function contentPackage() {
|
|
|
71
71
|
* written — fails the build if this value and the manifest's `id` ever drift.
|
|
72
72
|
*
|
|
73
73
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
74
|
-
* needs no configuration
|
|
74
|
+
* needs no configuration.
|
|
75
75
|
*
|
|
76
76
|
* @returns {string} The configured `foundryPackage`.
|
|
77
77
|
*/
|
|
@@ -15,7 +15,7 @@ import { completeAddress, renderAddress, isAddressTuple } from "./address.mjs";
|
|
|
15
15
|
import { contentPackage } from "./content-package.mjs";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Generated content tables
|
|
18
|
+
* Generated content tables from fenced `dataview` `TABLE` queries.
|
|
19
19
|
*
|
|
20
20
|
* A catalog table (every cloth armour, every animal's abilities) is data that
|
|
21
21
|
* already lives in the frontmatter of the notes it describes. Authoring such a
|
|
@@ -30,13 +30,9 @@ import { contentPackage } from "./content-package.mjs";
|
|
|
30
30
|
* SORT name.full ASC
|
|
31
31
|
* ```
|
|
32
32
|
*
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* an author sees in Obsidian what the build ships. Both content builds run it —
|
|
37
|
-
* the pack compilers (Foundry journals/items/actors) and the knowledgebase — so
|
|
38
|
-
* one authored query yields the same table in the vault, in Foundry, and on the
|
|
39
|
-
* knowledgebase.
|
|
33
|
+
* The build evaluates the query against note frontmatter and replaces the
|
|
34
|
+
* block with a Markdown table. Pack journals and site pages read the same
|
|
35
|
+
* generated table from the authored query.
|
|
40
36
|
*
|
|
41
37
|
* The emitted table is **markdown**, not HTML, and it is expanded *before*
|
|
42
38
|
* wikilink resolution: a cell may therefore carry `[[type/shortcode|Text]]`,
|
package/engine/folder-notes.mjs
CHANGED
|
@@ -174,8 +174,7 @@ function folderFields(fm) {
|
|
|
174
174
|
* An authored `parent` with any wikilink brackets and label stripped.
|
|
175
175
|
*
|
|
176
176
|
* The specification types `parent` as an `Address`, written bare — but
|
|
177
|
-
* `[[address]]` is
|
|
178
|
-
* and Obsidian wrote that form for years. Accepting both costs one regex and
|
|
177
|
+
* `[[address]]` is also a recognizable link form. Accepting both costs one regex and
|
|
179
178
|
* removes a failure whose message would have to explain the difference.
|
|
180
179
|
*
|
|
181
180
|
* @param {string|null} value - As authored.
|
|
@@ -163,7 +163,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
163
163
|
// A published address must name the pack the document actually shipped in:
|
|
164
164
|
// a consumer resolves the UUID verbatim, and a repository may ship several
|
|
165
165
|
// packs of one type.
|
|
166
|
-
const uuidFor = (type, id, routeFm) => {
|
|
166
|
+
const uuidFor = (type, id, routeFm, system) => {
|
|
167
167
|
// **A document that would be empty is not created, so nothing names
|
|
168
168
|
// one.** A JournalEntry's content is the note's prose, and an empty
|
|
169
169
|
// body compiles no pages, so the pass emits no entry — and a manifest
|
|
@@ -201,7 +201,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
201
201
|
!id ||
|
|
202
202
|
NEVER_PACKED_TYPES.has(String(type)) ||
|
|
203
203
|
DERIVED_PACKED_TYPES.has(String(type)) ||
|
|
204
|
-
(routeFm && declaresNoPack(routeFm))
|
|
204
|
+
(routeFm && declaresNoPack(routeFm, system))
|
|
205
205
|
) {
|
|
206
206
|
return undefined;
|
|
207
207
|
}
|
|
@@ -209,7 +209,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
209
209
|
const docType = routeFm ? packForType(type).docType : "JournalEntry";
|
|
210
210
|
const pack =
|
|
211
211
|
routeFm ?
|
|
212
|
-
packRouter.resolveOrNull(routeFm, docType)
|
|
212
|
+
packRouter.resolveOrNull(routeFm, docType, system)
|
|
213
213
|
: packRouter.defaultOf("JournalEntry");
|
|
214
214
|
|
|
215
215
|
// **A system block is what makes a game document**, so a note carrying
|
|
@@ -224,14 +224,27 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
224
224
|
// and one declaring none by the fallback, which reads a single block of
|
|
225
225
|
// its own.
|
|
226
226
|
if (SYSTEM_DOCUMENT_CLASSES.has(docType)) {
|
|
227
|
-
const
|
|
228
|
-
(pack && packRouter.systemOf?.(pack)) || DEFAULT_DOCUMENT_SUBTYPES.system;
|
|
229
|
-
if (!carriesSystemBlock(routeFm,
|
|
227
|
+
const targetSystem =
|
|
228
|
+
system || (pack && packRouter.systemOf?.(pack)) || DEFAULT_DOCUMENT_SUBTYPES.system;
|
|
229
|
+
if (!carriesSystemBlock(routeFm, targetSystem)) return undefined;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
return compendiumUuid(foundryPackageId, type, id, pack);
|
|
233
233
|
};
|
|
234
234
|
|
|
235
|
+
const systemUuidsFor = (type, id) => {
|
|
236
|
+
const docType = packForType(type)?.docType;
|
|
237
|
+
if (!SYSTEM_DOCUMENT_CLASSES.has(docType)) return undefined;
|
|
238
|
+
const systems = packRouter.systemsOfType?.(docType) ?? [];
|
|
239
|
+
if (!systems.length) return undefined;
|
|
240
|
+
return Object.fromEntries(
|
|
241
|
+
systems.flatMap((system) => {
|
|
242
|
+
const uuid = uuidFor(type, id, fm, system);
|
|
243
|
+
return uuid ? [[system, uuid]] : [];
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
235
248
|
const carriesDoc =
|
|
236
249
|
ctx.docEntryTypes ?
|
|
237
250
|
ctx.docEntryTypes.has(String(currentType(fm.type)))
|
|
@@ -251,6 +264,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
251
264
|
url,
|
|
252
265
|
id: fm.id,
|
|
253
266
|
uuid: uuidFor(fm.type, fm.id, fm),
|
|
267
|
+
systemUuids: systemUuidsFor(fm.type, fm.id),
|
|
254
268
|
doc: docKey,
|
|
255
269
|
},
|
|
256
270
|
{
|
|
@@ -283,6 +297,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
283
297
|
url,
|
|
284
298
|
id: fm.id,
|
|
285
299
|
uuid: own,
|
|
300
|
+
systemUuids: systemUuidsFor(fm.type, fm.id),
|
|
286
301
|
anchors: own && fm.type === "doc" ? anchorsOf(own, fm.id, body ?? "", name) : undefined,
|
|
287
302
|
},
|
|
288
303
|
];
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* - a **wrong type** threw deep inside a builder, or was coerced;
|
|
25
25
|
* - a **misspelled property** was simply *ignored* — the builders are an
|
|
26
26
|
* allow-list, so an unrecognised `sohl:` key is dropped without a word. That
|
|
27
|
-
* is how 204 kethira mystical abilities shipped with no affiliation
|
|
27
|
+
* is how 204 kethira mystical abilities shipped with no affiliation, and
|
|
28
28
|
* why the silence mattered more than the missing field: an author could not
|
|
29
29
|
* tell a builder that forgot the field apart from a field that does
|
|
30
30
|
* not belong on the type at all;
|
|
@@ -312,8 +312,7 @@ function nearest(key, candidates) {
|
|
|
312
312
|
*
|
|
313
313
|
* Deliberately lenient about the spellings YAML makes ambiguous: `"12"` is a
|
|
314
314
|
* number, because a quoted scalar is how a number arrives from many editors,
|
|
315
|
-
* and a map authored as an empty list
|
|
316
|
-
* editor serialises an emptied map that way (#8). What it rejects is a value
|
|
315
|
+
* and a map authored as an empty list has no entries, just like `{}`. What it rejects is a value
|
|
317
316
|
* that cannot mean what the field is for — `weight: heavy`.
|
|
318
317
|
*
|
|
319
318
|
* @param {unknown} value - The authored value.
|
|
@@ -378,7 +377,7 @@ function mapEntries(value) {
|
|
|
378
377
|
* The `data:` container a note authored.
|
|
379
378
|
*
|
|
380
379
|
* An emptied map arrives from the property editor as `[]` and means the same
|
|
381
|
-
* thing `{}` does — this note authors no entries
|
|
380
|
+
* thing `{}` does — this note authors no entries — so both read as an
|
|
382
381
|
* empty container rather than as a malformed one.
|
|
383
382
|
*
|
|
384
383
|
* @param {object} fm - The note's frontmatter.
|