@heroiclands/package-build 15.0.0 → 17.0.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 +185 -0
- package/CONTENT.md +203 -0
- package/bin/content-build.mjs +54 -0
- package/content-config.mjs +10 -0
- package/docs/content-format.md +7 -1
- package/engine/content-address.mjs +21 -36
- package/engine/content-index.mjs +439 -0
- package/engine/frontmatter-lint.mjs +86 -1
- package/engine/index.mjs +3 -0
- package/engine/note-schemas.mjs +43 -0
- package/engine/note-vocabulary.mjs +119 -2
- package/package.json +1 -1
- package/sohl/note-schemas.mjs +17 -0
- package/types/content-config.d.mts +9 -0
- package/types/engine/content-address.d.mts +22 -34
- package/types/engine/content-index.d.mts +194 -0
- package/types/engine/index.d.mts +1 -0
- package/types/engine/note-vocabulary.d.mts +50 -0
|
@@ -395,7 +395,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
395
395
|
{ name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
|
|
396
396
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
397
397
|
{ name: "stations", ...LINKS, describe: "Stations the being holds." },
|
|
398
|
-
{
|
|
398
|
+
{
|
|
399
|
+
name: "lore",
|
|
400
|
+
...LINKS,
|
|
401
|
+
describe:
|
|
402
|
+
"Lore concerning this being — the people it is of, the standing it " +
|
|
403
|
+
"holds, the law it lives under.",
|
|
404
|
+
},
|
|
399
405
|
{ name: "homes", ...LINKS, describe: "Places the being calls home." },
|
|
400
406
|
{
|
|
401
407
|
name: "affiliations",
|
|
@@ -425,6 +431,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
425
431
|
]),
|
|
426
432
|
}),
|
|
427
433
|
|
|
434
|
+
vehicle: Object.freeze({
|
|
435
|
+
subTypes: null,
|
|
436
|
+
data: Object.freeze([
|
|
437
|
+
{ name: "portrait", ...TEXT, describe: "Path to the portrait image." },
|
|
438
|
+
TEMPLATE_PRIORITY,
|
|
439
|
+
]),
|
|
440
|
+
}),
|
|
441
|
+
|
|
428
442
|
/* ----- items ---------------------------------------------------- */
|
|
429
443
|
|
|
430
444
|
affiliation: Object.freeze({
|
|
@@ -448,6 +462,18 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
448
462
|
...TEXT,
|
|
449
463
|
describe: "What one member is called — a Vylarian.",
|
|
450
464
|
},
|
|
465
|
+
{
|
|
466
|
+
name: "epithet",
|
|
467
|
+
...TEXT,
|
|
468
|
+
describe: "The by-name it is known by — a god's, an order's, a company's.",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "symbol",
|
|
472
|
+
...TEXT,
|
|
473
|
+
describe:
|
|
474
|
+
"Its emblem in words: a feather atop a golden scale, a chisel carving " +
|
|
475
|
+
"a star.",
|
|
476
|
+
},
|
|
451
477
|
{
|
|
452
478
|
name: "governance.model",
|
|
453
479
|
...TEXT,
|
|
@@ -481,7 +507,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
481
507
|
...LINKS,
|
|
482
508
|
describe: "What its economic life runs on — currencies, banking bodies, goods.",
|
|
483
509
|
},
|
|
484
|
-
{
|
|
510
|
+
{
|
|
511
|
+
name: "lore",
|
|
512
|
+
...LINKS,
|
|
513
|
+
describe:
|
|
514
|
+
"Lore concerning it — the peoples it draws on, the god a faith " +
|
|
515
|
+
"venerates, its law, its calendar.",
|
|
516
|
+
},
|
|
485
517
|
{ name: "parents", ...LINKS, describe: "Affiliations it is subordinate to." },
|
|
486
518
|
{
|
|
487
519
|
name: "relations",
|
|
@@ -532,6 +564,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
532
564
|
data: Object.freeze([TEMPLATE_PRIORITY, ...GEAR]),
|
|
533
565
|
}),
|
|
534
566
|
|
|
567
|
+
armorlocation: Object.freeze({
|
|
568
|
+
subTypes: null,
|
|
569
|
+
data: Object.freeze([TEMPLATE_PRIORITY]),
|
|
570
|
+
}),
|
|
571
|
+
|
|
535
572
|
attribute: Object.freeze({
|
|
536
573
|
data: Object.freeze([TEMPLATE_PRIORITY]),
|
|
537
574
|
}),
|
|
@@ -674,6 +711,86 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
674
711
|
|
|
675
712
|
macro: Object.freeze({ data: Object.freeze([]) }),
|
|
676
713
|
|
|
714
|
+
lore: Object.freeze({
|
|
715
|
+
subTypes: Object.freeze([
|
|
716
|
+
"cosmology",
|
|
717
|
+
"deity",
|
|
718
|
+
"theology",
|
|
719
|
+
"arcana",
|
|
720
|
+
"spirit",
|
|
721
|
+
"economy",
|
|
722
|
+
"law",
|
|
723
|
+
"calendar",
|
|
724
|
+
"history",
|
|
725
|
+
"material",
|
|
726
|
+
"folk",
|
|
727
|
+
"culture",
|
|
728
|
+
"bestiary",
|
|
729
|
+
]),
|
|
730
|
+
// Nothing of its own: a lore note is prose, and what it *is* about is
|
|
731
|
+
// its subType. The specification declares an empty table for it, and
|
|
732
|
+
// the authored corpus writes no `data:` key on any of the 180.
|
|
733
|
+
data: Object.freeze([]),
|
|
734
|
+
}),
|
|
735
|
+
|
|
736
|
+
place: Object.freeze({
|
|
737
|
+
subTypes: Object.freeze(["world", "region", "settlement", "site", "structure", "feature"]),
|
|
738
|
+
data: Object.freeze([
|
|
739
|
+
{
|
|
740
|
+
name: "demonym",
|
|
741
|
+
...TEXT,
|
|
742
|
+
describe: "What a person from this place is called — a Vylarian.",
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "lore",
|
|
746
|
+
...LINKS,
|
|
747
|
+
describe:
|
|
748
|
+
"Lore concerning this place — its peoples, its law, its calendar, " +
|
|
749
|
+
"its history.",
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "parents",
|
|
753
|
+
...LINKS,
|
|
754
|
+
describe: "Enclosing places this one sits within.",
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: "population",
|
|
758
|
+
...NUM,
|
|
759
|
+
describe: "Approximate population, to two significant digits.",
|
|
760
|
+
},
|
|
761
|
+
]),
|
|
762
|
+
}),
|
|
763
|
+
|
|
764
|
+
scenario: Object.freeze({
|
|
765
|
+
subTypes: Object.freeze(["campaign", "adventure", "encounter"]),
|
|
766
|
+
data: Object.freeze([
|
|
767
|
+
{ name: "parents", ...LINKS, describe: "Scenarios this one sits within." },
|
|
768
|
+
{ name: "locations", ...LINKS, describe: "Places the scenario takes place in." },
|
|
769
|
+
{ name: "cast", ...LINKS, describe: "Beings who appear in it." },
|
|
770
|
+
{ name: "factions", ...LINKS, describe: "Affiliations with a stake in it." },
|
|
771
|
+
{
|
|
772
|
+
name: "follows",
|
|
773
|
+
...LINKS,
|
|
774
|
+
describe: "Scenarios that should be played before this one.",
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "status",
|
|
778
|
+
...TEXT,
|
|
779
|
+
describe: "`draft`, `playtested` or `published`.",
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: "party.size",
|
|
783
|
+
...TEXT,
|
|
784
|
+
describe: "`solo`, `small`, `standard`, `large` or `host`.",
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: "party.archetypes",
|
|
788
|
+
...LIST,
|
|
789
|
+
describe: "Archetypes the scenario is written for.",
|
|
790
|
+
},
|
|
791
|
+
]),
|
|
792
|
+
}),
|
|
793
|
+
|
|
677
794
|
homepage: Object.freeze({ data: Object.freeze([]) }),
|
|
678
795
|
|
|
679
796
|
map: Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.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",
|
package/sohl/note-schemas.mjs
CHANGED
|
@@ -87,6 +87,22 @@ const MACRO_FIELDS = Object.freeze([
|
|
|
87
87
|
*
|
|
88
88
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
89
89
|
*/
|
|
90
|
+
/**
|
|
91
|
+
* A `vehicle` note — a conveyance that carries goods and people.
|
|
92
|
+
*
|
|
93
|
+
* Empty, and the emptiness is the declaration: the specification gives a
|
|
94
|
+
* vehicle two `data` properties and no `sohl:` field of its own, and the
|
|
95
|
+
* closed `data:` container is where `engine/note-vocabulary.mjs` holds them.
|
|
96
|
+
* Declaring the type with no fields distinguishes a type with no vocabulary
|
|
97
|
+
* from a type that is unknown, which are different findings (#231).
|
|
98
|
+
*
|
|
99
|
+
* It is here rather than in the engine because a vehicle is a SoHL actor —
|
|
100
|
+
* the specification maps it to `sohl` and marks it NA for hm3.
|
|
101
|
+
*
|
|
102
|
+
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
103
|
+
*/
|
|
104
|
+
const VEHICLE_FIELDS = Object.freeze([]);
|
|
105
|
+
|
|
90
106
|
const BEING_FIELDS = Object.freeze([
|
|
91
107
|
{
|
|
92
108
|
name: "body",
|
|
@@ -330,5 +346,6 @@ export const NOTE_SCHEMAS = Object.freeze({
|
|
|
330
346
|
doc: DOC_FIELDS,
|
|
331
347
|
macro: MACRO_FIELDS,
|
|
332
348
|
being: Object.freeze([...BEING_FIELDS, ...PRESENTATION_FIELDS.being]),
|
|
349
|
+
vehicle: VEHICLE_FIELDS,
|
|
333
350
|
map: MAP_FIELDS,
|
|
334
351
|
});
|
|
@@ -41,6 +41,7 @@ export namespace DEFAULT_PATHS {
|
|
|
41
41
|
let content: "assets/content";
|
|
42
42
|
let manifests: "assets/manifests";
|
|
43
43
|
let manifestOut: "build/manifests";
|
|
44
|
+
let contentIndex: "build/content-index";
|
|
44
45
|
let packJson: "build/packs-json";
|
|
45
46
|
let stage: "build/stage/packs";
|
|
46
47
|
let unpack: "build/tmp/packs";
|
|
@@ -296,6 +297,13 @@ export type PathsInput = {
|
|
|
296
297
|
* `manifests` directory.
|
|
297
298
|
*/
|
|
298
299
|
manifestOut?: string | undefined;
|
|
300
|
+
/**
|
|
301
|
+
* Where `content-index` writes this
|
|
302
|
+
* package's note index. Outbound, and a
|
|
303
|
+
* derived artifact — never a source, and
|
|
304
|
+
* never inside `stage`.
|
|
305
|
+
*/
|
|
306
|
+
contentIndex?: string | undefined;
|
|
299
307
|
/**
|
|
300
308
|
* Build-only per-entry JSON intermediate.
|
|
301
309
|
*/
|
|
@@ -316,6 +324,7 @@ export type ResolvedPaths = {
|
|
|
316
324
|
content: string;
|
|
317
325
|
manifests: string;
|
|
318
326
|
manifestOut: string;
|
|
327
|
+
contentIndex: string;
|
|
319
328
|
packJson: string;
|
|
320
329
|
stage: string;
|
|
321
330
|
unpack: string;
|
|
@@ -21,44 +21,32 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export function addressSlug(fm: object): string;
|
|
23
23
|
/**
|
|
24
|
-
* A note's address
|
|
24
|
+
* A note's address: `<type>-<shortcode>/`, e.g. `affliction-aconite/`.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* decides nothing about where it publishes.
|
|
30
|
-
*
|
|
31
|
-
* @param {object} fm - Parsed frontmatter.
|
|
32
|
-
* @returns {string} The mount-relative address, with a trailing slash.
|
|
33
|
-
* @throws {Error} When the note has no address.
|
|
34
|
-
*/
|
|
35
|
-
export function contentAddress(fm: object): string;
|
|
36
|
-
/**
|
|
37
|
-
* A note's address relative to its **package**, e.g. `affliction-aconite/`.
|
|
38
|
-
*
|
|
39
|
-
* This is the form the link manifest records and the site build emits pages at,
|
|
40
|
-
* and it is one function because those two must agree — a manifest asserting an
|
|
26
|
+
* This is the one form a note is addressed by. It is what the link manifest
|
|
27
|
+
* records as an entry's `path` and what the site build emits the page at, and
|
|
28
|
+
* it is one function because those two must agree — a manifest asserting an
|
|
41
29
|
* address the site does not publish resolves at build time and 404s for the
|
|
42
30
|
* reader, which is the failure this module exists to prevent.
|
|
43
31
|
*
|
|
44
|
-
* **
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* is
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
32
|
+
* **The address is relative to the package**, and to nothing finer. A consumer
|
|
33
|
+
* composing a URL prepends where the package is served (`/<package>/`); a
|
|
34
|
+
* consumer composing a manifest entry measures against that same base. Nothing
|
|
35
|
+
* else is prepended: `prefix` says where the content tree *mounts inside the
|
|
36
|
+
* package* — the Hugo directory its pages are written under — and an address is
|
|
37
|
+
* `(type, shortcode)`, a package-wide identity that takes no mount, so `sohl`
|
|
38
|
+
* publishes `/sohl/affliction-aconite/` from a file written under `kb/`. The
|
|
39
|
+
* `type-` half is what keeps that flat namespace clear of the package's fixed
|
|
40
|
+
* mounts — `/<package>/` for the landing, `/<package>/api/` for generated API
|
|
41
|
+
* docs, neither of which contains a hyphen or names a type.
|
|
42
|
+
*
|
|
43
|
+
* **It is a pure function of the frontmatter**, and takes no options. Nothing
|
|
44
|
+
* about the file the note was read from reaches it: the `README.md` convention
|
|
45
|
+
* that made one note address a whole section is retired with the section itself
|
|
46
|
+
* (#204), so every note is addressed alike and there is one rule and no branch.
|
|
47
|
+
* It took an address scheme until #215, to validate a `landing` rule it then
|
|
48
|
+
* discarded; with that key retired, `prefix` was the only thing left in the
|
|
49
|
+
* scheme and the paragraph above is the reason it never applied.
|
|
62
50
|
*
|
|
63
51
|
* @param {object} fm - Parsed frontmatter.
|
|
64
52
|
* @returns {string} The package-relative address, with a trailing slash and no
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `{#slug}` anchors a note's body declares, with where each one sits.
|
|
3
|
+
*
|
|
4
|
+
* Only headings carrying an explicit anchor are collected. A bare `#` heading
|
|
5
|
+
* also starts a journal page, but it declares no slug, so nothing can address
|
|
6
|
+
* it with `#…` — listing it would offer a link that cannot be written.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} body - The note's markdown body, frontmatter already removed.
|
|
9
|
+
* @param {number} [bodyLine] - The 1-based file line the body starts on, from
|
|
10
|
+
* `parseMarkdownFile`. Anchors are reported at their position in the **file**,
|
|
11
|
+
* so an editor can jump straight to one; passing nothing numbers from the body.
|
|
12
|
+
* @returns {Array<{slug: string, name: string, level: number, line: number}>}
|
|
13
|
+
* In document order.
|
|
14
|
+
*/
|
|
15
|
+
export function collectAnchors(body: string, bodyLine?: number): Array<{
|
|
16
|
+
slug: string;
|
|
17
|
+
name: string;
|
|
18
|
+
level: number;
|
|
19
|
+
line: number;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* The address a wikilink writes to reach a note, or `null` when it has none.
|
|
23
|
+
*
|
|
24
|
+
* A wikilink target is an address: `being-aurochs` locally, or
|
|
25
|
+
* `sohl-being-aurochs` from another package (`readQualifier` also accepts
|
|
26
|
+
* `being/aurochs`, the same two fields with a different separator). Both forms
|
|
27
|
+
* are already derivable from `type` and `shortcode`, which every record
|
|
28
|
+
* carries — so this field adds no information. What it adds is the *rule*:
|
|
29
|
+
* the lowercasing and the hyphen join live in one place, and a consumer that
|
|
30
|
+
* reimplements them slightly differently gets a lookup that matches nothing and
|
|
31
|
+
* says nothing about why. That is a real failure, not a hypothetical one — it
|
|
32
|
+
* is precisely how a resolver keyed on a bare `type/shortcode` silently misses
|
|
33
|
+
* every canonical `pkg-type-shortcode` entry.
|
|
34
|
+
*
|
|
35
|
+
* Derived by the same functions the link manifest and the site build use, so an
|
|
36
|
+
* index cannot disagree with either about where a note lives.
|
|
37
|
+
*
|
|
38
|
+
* @param {Record<string, any>} frontmatter - The note's parsed frontmatter.
|
|
39
|
+
* @param {string} contentPackage - The package the tree compiles as.
|
|
40
|
+
* @returns {{slug: string, canonical: string}|null} `slug` is what goes inside
|
|
41
|
+
* `[[…]]` within this package; `canonical` is the package-qualified key the
|
|
42
|
+
* manifest files the note under. `null` for a note with no type or no
|
|
43
|
+
* shortcode, which has no address at all and is stated as such rather than
|
|
44
|
+
* left for every reader to rediscover.
|
|
45
|
+
*/
|
|
46
|
+
export function noteAddress(frontmatter: Record<string, any>, contentPackage: string): {
|
|
47
|
+
slug: string;
|
|
48
|
+
canonical: string;
|
|
49
|
+
} | null;
|
|
50
|
+
/**
|
|
51
|
+
* Recursively sort an object's keys, so serialization is order-independent.
|
|
52
|
+
*
|
|
53
|
+
* Arrays keep their order — it is authored — but every object inside one is
|
|
54
|
+
* sorted too. Anything that is not a plain object is returned as it is.
|
|
55
|
+
*
|
|
56
|
+
* @param {unknown} value - The value to normalize.
|
|
57
|
+
* @returns {unknown} The value with every plain object's keys in sorted order.
|
|
58
|
+
*/
|
|
59
|
+
export function sortKeysDeep(value: unknown): unknown;
|
|
60
|
+
/**
|
|
61
|
+
* A note's display name reduced to printable 7-bit ASCII.
|
|
62
|
+
*
|
|
63
|
+
* Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
|
|
64
|
+
* `Kèthîra` — and nobody types them. A reader searching the index, or an editor
|
|
65
|
+
* completing a wikilink, needs a form that matches what a keyboard produces, so
|
|
66
|
+
* the record states one rather than leaving every consumer to invent it (and to
|
|
67
|
+
* invent a *different* one, which is how two searches over the same data come
|
|
68
|
+
* to disagree).
|
|
69
|
+
*
|
|
70
|
+
* **Transliterated, not stripped.** `unidecode` — the same table
|
|
71
|
+
* {@link slugify} already runs, so an ASCII name and a slug can never disagree
|
|
72
|
+
* about a character — carries a letter across rather than deleting it:
|
|
73
|
+
* diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
|
|
74
|
+
* `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
|
|
75
|
+
* even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
|
|
76
|
+
* would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
|
|
77
|
+
*
|
|
78
|
+
* Anything still outside printable ASCII after that becomes a space, and runs
|
|
79
|
+
* of whitespace collapse — a space rather than nothing, so a character that
|
|
80
|
+
* transliterates away cannot silently weld two words together.
|
|
81
|
+
*
|
|
82
|
+
* The value is emitted even when it equals the name, so a consumer matching on
|
|
83
|
+
* it never has to branch on whether the name happened to be ASCII already.
|
|
84
|
+
*
|
|
85
|
+
* @param {unknown} name - The note's `name.full`.
|
|
86
|
+
* @returns {string|null} The ASCII form, or `null` when there is no name, or
|
|
87
|
+
* nothing printable survives.
|
|
88
|
+
*/
|
|
89
|
+
export function asciiName(name: unknown): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
|
|
92
|
+
*
|
|
93
|
+
* An alias is the name a reader is at least as likely to reach for as the
|
|
94
|
+
* canonical one — `Killer Whale` for an orca, `Ice Bear` for a polar bear,
|
|
95
|
+
* `Ix'balam` for a jaguar — so anything searching or completing over the index
|
|
96
|
+
* has to match them too, and needs the same keyboard-typeable form
|
|
97
|
+
* {@link asciiName} gives the primary name.
|
|
98
|
+
*
|
|
99
|
+
* Order is the authored order, so a caller can pair an entry with the alias it
|
|
100
|
+
* came from. An alias that is not a non-empty string, or that leaves nothing
|
|
101
|
+
* printable behind, is dropped rather than left as a hole — the array is a set
|
|
102
|
+
* of names to match, and a null in it is not one.
|
|
103
|
+
*
|
|
104
|
+
* @param {unknown} aliases - The note's `name.aliases`; may be absent or null.
|
|
105
|
+
* @returns {Array<string>} Possibly empty, never null: a note with no aliases
|
|
106
|
+
* has an empty set of them, which is a fact rather than a missing value, and
|
|
107
|
+
* a consumer iterating it should not have to check first.
|
|
108
|
+
*/
|
|
109
|
+
export function asciiAliases(aliases: unknown): Array<string>;
|
|
110
|
+
/**
|
|
111
|
+
* Build one index record from a note's frontmatter and its place in the tree.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} options - Options.
|
|
114
|
+
* @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
|
|
115
|
+
* @param {string} options.relPath - Its path below the content root, POSIX-separated.
|
|
116
|
+
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
117
|
+
* @param {string} [options.body] - The note's markdown body, for its anchors.
|
|
118
|
+
* @param {number} [options.bodyLine] - The 1-based file line the body starts on.
|
|
119
|
+
* @returns {Record<string, any>} The record, keys sorted at every depth.
|
|
120
|
+
* @throws {Error} When the note carries a key this module derives, which would
|
|
121
|
+
* otherwise be overwritten without a word.
|
|
122
|
+
*/
|
|
123
|
+
export function buildIndexRecord({ frontmatter, relPath, contentPackage, body, bodyLine }: {
|
|
124
|
+
frontmatter: Record<string, any>;
|
|
125
|
+
relPath: string;
|
|
126
|
+
contentPackage: string;
|
|
127
|
+
body?: string | undefined;
|
|
128
|
+
bodyLine?: number | undefined;
|
|
129
|
+
}): Record<string, any>;
|
|
130
|
+
/**
|
|
131
|
+
* Read a content tree into index records, in the order they will be written.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} contentBase - The content tree to walk.
|
|
134
|
+
* @param {object} options - Options.
|
|
135
|
+
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
136
|
+
* @param {Array<string>} [options.skipDirectories] - Directory names to skip.
|
|
137
|
+
* @returns {Array<Record<string, any>>} The records, in a total order that does
|
|
138
|
+
* not depend on directory-read order.
|
|
139
|
+
*/
|
|
140
|
+
export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories }: {
|
|
141
|
+
contentPackage: string;
|
|
142
|
+
skipDirectories?: string[] | undefined;
|
|
143
|
+
}): Array<Record<string, any>>;
|
|
144
|
+
/**
|
|
145
|
+
* Serialize records as JSON Lines.
|
|
146
|
+
*
|
|
147
|
+
* @param {Array<Record<string, any>>} records - From {@link collectContentIndex}.
|
|
148
|
+
* @returns {string} One compact JSON object per line, newline-terminated. An
|
|
149
|
+
* empty set serializes to the empty string rather than to a lone newline, so
|
|
150
|
+
* the file is exactly the lines it holds.
|
|
151
|
+
*/
|
|
152
|
+
export function serializeContentIndex(records: Array<Record<string, any>>): string;
|
|
153
|
+
/**
|
|
154
|
+
* Emit this package's content index.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [options] - Options.
|
|
157
|
+
* @param {string} [options.contentBase] - The content tree; defaults to the
|
|
158
|
+
* configured `paths.content`.
|
|
159
|
+
* @param {string} [options.outDir] - Where to write; defaults to the configured
|
|
160
|
+
* `paths.contentIndex`.
|
|
161
|
+
* @param {object} [options.config] - A resolved configuration; loaded when omitted.
|
|
162
|
+
* @returns {{file: string, notes: number, bytes: number}} Where it was written,
|
|
163
|
+
* how many notes it holds, and its size.
|
|
164
|
+
* @throws {Error} When the content tree is absent, or when it yields no note at
|
|
165
|
+
* all — an empty index is indistinguishable from a mis-pointed tree, and a
|
|
166
|
+
* reader would take it as the authoritative statement that this package has
|
|
167
|
+
* no content.
|
|
168
|
+
*/
|
|
169
|
+
export function emitContentIndex({ contentBase, outDir, config }?: {
|
|
170
|
+
contentBase?: string | undefined;
|
|
171
|
+
outDir?: string | undefined;
|
|
172
|
+
config?: object | undefined;
|
|
173
|
+
}): {
|
|
174
|
+
file: string;
|
|
175
|
+
notes: number;
|
|
176
|
+
bytes: number;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The keys this module adds to a record, which a note therefore may not carry
|
|
180
|
+
* itself.
|
|
181
|
+
*
|
|
182
|
+
* `package` is the note's distribution unit — the configured `contentPackage`,
|
|
183
|
+
* since a note declaring its own is a hard error (package-build#56) — and it
|
|
184
|
+
* matches what the content-table expander puts on the same field, so a query
|
|
185
|
+
* reads the same value from either. `file` namespaces the note's place in the
|
|
186
|
+
* tree, again matching the expander's `file.*`.
|
|
187
|
+
*
|
|
188
|
+
* Both are checked rather than assumed: `folder` is real frontmatter on most
|
|
189
|
+
* notes, so the neighbouring names are close enough to a real key that a silent
|
|
190
|
+
* overwrite is a plausible future rather than a hypothetical one.
|
|
191
|
+
*
|
|
192
|
+
* @type {ReadonlyArray<string>}
|
|
193
|
+
*/
|
|
194
|
+
export const DERIVED_KEYS: ReadonlyArray<string>;
|
package/types/engine/index.d.mts
CHANGED
|
@@ -17,6 +17,7 @@ export * as contentAddress from "./content-address.mjs";
|
|
|
17
17
|
export * as foreignManifests from "./foreign-manifests.mjs";
|
|
18
18
|
export * as kbManifest from "./kb-manifest.mjs";
|
|
19
19
|
export * as manifestEmit from "./manifest-emit.mjs";
|
|
20
|
+
export * as contentIndex from "./content-index.mjs";
|
|
20
21
|
export * as siteBuild from "./site-build.mjs";
|
|
21
22
|
export * as contentLint from "./content-lint.mjs";
|
|
22
23
|
export * as contentLinks from "./content-links.mjs";
|
|
@@ -187,6 +187,10 @@ export const NOTE_VOCABULARY: Readonly<{
|
|
|
187
187
|
subTypes: null;
|
|
188
188
|
data: readonly DataFieldSpec[];
|
|
189
189
|
}>;
|
|
190
|
+
vehicle: Readonly<{
|
|
191
|
+
subTypes: null;
|
|
192
|
+
data: readonly DataFieldSpec[];
|
|
193
|
+
}>;
|
|
190
194
|
affiliation: Readonly<{
|
|
191
195
|
subTypes: readonly string[];
|
|
192
196
|
data: readonly DataFieldSpec[];
|
|
@@ -198,6 +202,10 @@ export const NOTE_VOCABULARY: Readonly<{
|
|
|
198
202
|
armorgear: Readonly<{
|
|
199
203
|
data: readonly DataFieldSpec[];
|
|
200
204
|
}>;
|
|
205
|
+
armorlocation: Readonly<{
|
|
206
|
+
subTypes: null;
|
|
207
|
+
data: readonly DataFieldSpec[];
|
|
208
|
+
}>;
|
|
201
209
|
attribute: Readonly<{
|
|
202
210
|
data: readonly DataFieldSpec[];
|
|
203
211
|
}>;
|
|
@@ -256,6 +264,48 @@ export const NOTE_VOCABULARY: Readonly<{
|
|
|
256
264
|
macro: Readonly<{
|
|
257
265
|
data: readonly never[];
|
|
258
266
|
}>;
|
|
267
|
+
lore: Readonly<{
|
|
268
|
+
subTypes: readonly string[];
|
|
269
|
+
data: readonly never[];
|
|
270
|
+
}>;
|
|
271
|
+
place: Readonly<{
|
|
272
|
+
subTypes: readonly string[];
|
|
273
|
+
data: readonly ({
|
|
274
|
+
describe: string;
|
|
275
|
+
shape: "string";
|
|
276
|
+
kind: "string";
|
|
277
|
+
name: string;
|
|
278
|
+
} | {
|
|
279
|
+
describe: string;
|
|
280
|
+
shape: "list of wikilinks";
|
|
281
|
+
kind: "list";
|
|
282
|
+
name: string;
|
|
283
|
+
} | {
|
|
284
|
+
describe: string;
|
|
285
|
+
shape: "number";
|
|
286
|
+
kind: "number";
|
|
287
|
+
name: string;
|
|
288
|
+
})[];
|
|
289
|
+
}>;
|
|
290
|
+
scenario: Readonly<{
|
|
291
|
+
subTypes: readonly string[];
|
|
292
|
+
data: readonly ({
|
|
293
|
+
describe: string;
|
|
294
|
+
shape: "list of wikilinks";
|
|
295
|
+
kind: "list";
|
|
296
|
+
name: string;
|
|
297
|
+
} | {
|
|
298
|
+
describe: string;
|
|
299
|
+
shape: "string";
|
|
300
|
+
kind: "string";
|
|
301
|
+
name: string;
|
|
302
|
+
} | {
|
|
303
|
+
describe: string;
|
|
304
|
+
shape: "list";
|
|
305
|
+
kind: "list";
|
|
306
|
+
name: string;
|
|
307
|
+
})[];
|
|
308
|
+
}>;
|
|
259
309
|
homepage: Readonly<{
|
|
260
310
|
data: readonly never[];
|
|
261
311
|
}>;
|