@heroiclands/package-build 21.0.0 → 21.2.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 +228 -0
- package/CONTENT.md +109 -7
- package/assets/fonts/libertinus/LibertinusMono-Regular.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Bold.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Italic.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Regular.otf +0 -0
- package/assets/fonts/libertinus/OFL.txt +94 -0
- package/assets/fonts/libertinus/provenance.yaml +11 -0
- package/assets/images/banners/afflictionbnr.webp +0 -0
- package/assets/images/banners/armorclothingbnr.webp +0 -0
- package/assets/images/banners/attributebnr.webp +0 -0
- package/assets/images/banners/containerbnr.webp +0 -0
- package/assets/images/banners/devdocsbnr.webp +0 -0
- package/assets/images/banners/miscgearbnr.webp +0 -0
- package/assets/images/banners/mysticalabilitybnr.webp +0 -0
- package/assets/images/banners/projectilebnr.webp +0 -0
- package/assets/images/banners/rulesbnr.webp +0 -0
- package/assets/images/banners/skillbnr.webp +0 -0
- package/assets/images/banners/traumabnr.webp +0 -0
- package/assets/images/banners/userguidebnr.webp +0 -0
- package/assets/images/banners/weapongearbnr.webp +0 -0
- package/assets/images/provenance.yaml +4 -0
- package/bin/content-build.mjs +19 -5
- package/ci/emit-asset-index.mjs +32 -0
- package/content-config.mjs +30 -1
- package/docs/api.md +100 -26
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +512 -109
- package/engine/actor-compiler.mjs +58 -9
- package/engine/art-fields.mjs +292 -0
- package/engine/asset-index.mjs +397 -0
- package/engine/asset-types.mjs +192 -0
- package/engine/base-compiler.mjs +69 -1
- package/engine/bundles.mjs +5 -5
- package/engine/compile-corpus.mjs +4 -0
- package/engine/content-address.mjs +14 -2
- package/engine/content-charset.mjs +5 -1
- package/engine/content-embeds.mjs +314 -0
- package/engine/content-html.mjs +5 -1
- package/engine/content-icons.mjs +5 -1
- package/engine/content-images.mjs +26 -5
- package/engine/content-index.mjs +68 -33
- package/engine/content-links.mjs +164 -24
- package/engine/frontmatter-lint.mjs +36 -23
- package/engine/helpers.mjs +83 -31
- package/engine/index-records.mjs +63 -4
- package/engine/index.mjs +15 -0
- package/engine/infobox-registry.mjs +50 -3
- package/engine/infobox-render.mjs +4 -3
- package/engine/infobox.mjs +12 -8
- package/engine/item-compiler.mjs +10 -10
- package/engine/journals.mjs +3 -0
- package/engine/macros.mjs +16 -13
- package/engine/map-notes.mjs +29 -22
- package/engine/metadata-index.mjs +44 -36
- package/engine/note-vocabulary.mjs +71 -29
- package/engine/pack-router.mjs +14 -0
- package/engine/packagebuild-index.mjs +124 -0
- package/engine/packages.mjs +83 -0
- package/engine/pathnames.mjs +74 -0
- package/engine/pdf-build.mjs +133 -10
- package/engine/pdf-render.mjs +41 -41
- package/engine/scenes.mjs +12 -9
- package/engine/site-build.mjs +43 -16
- package/engine/site-index.mjs +10 -1
- package/engine/subtype-registry.mjs +21 -0
- package/engine/web-wikilinks.mjs +46 -12
- package/engine/wikilink-syntax.mjs +20 -2
- package/engine/wikilinks.mjs +15 -2
- package/hm3/actors.mjs +11 -38
- package/hm3/infobox.mjs +1 -1
- package/package.json +5 -2
- package/packagebuild-metadata.jsonl +13 -0
- package/sohl/actors.mjs +12 -86
- package/sohl/infobox.mjs +1 -1
- package/sohl/note-schemas.mjs +4 -16
- package/types/content-config.d.mts +8 -0
- package/types/engine/actor-compiler.d.mts +29 -0
- package/types/engine/art-fields.d.mts +192 -0
- package/types/engine/asset-index.d.mts +92 -0
- package/types/engine/asset-types.d.mts +110 -0
- package/types/engine/base-compiler.d.mts +42 -1
- package/types/engine/compile-corpus.d.mts +4 -1
- package/types/engine/content-charset.d.mts +4 -0
- package/types/engine/content-embeds.d.mts +218 -0
- package/types/engine/content-html.d.mts +4 -0
- package/types/engine/content-icons.d.mts +4 -0
- package/types/engine/content-images.d.mts +15 -2
- package/types/engine/content-index.d.mts +27 -15
- package/types/engine/frontmatter-lint.d.mts +11 -14
- package/types/engine/helpers.d.mts +7 -2
- package/types/engine/index-records.d.mts +44 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +36 -1
- package/types/engine/infobox.d.mts +6 -4
- package/types/engine/macros.d.mts +9 -5
- package/types/engine/map-notes.d.mts +10 -6
- package/types/engine/metadata-index.d.mts +2 -17
- package/types/engine/note-vocabulary.d.mts +30 -0
- package/types/engine/packagebuild-index.d.mts +56 -0
- package/types/engine/packages.d.mts +62 -0
- package/types/engine/pathnames.d.mts +41 -81
- package/types/engine/pdf-build.d.mts +58 -1
- package/types/engine/pdf-render.d.mts +3 -3
- package/types/engine/scenes.d.mts +2 -1
- package/types/engine/site-build.d.mts +5 -1
- package/types/engine/site-index.d.mts +4 -1
- package/types/engine/subtype-registry.d.mts +20 -0
- package/types/engine/web-wikilinks.d.mts +14 -4
- package/types/engine/wikilink-syntax.d.mts +13 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/sohl/actors.d.mts +0 -1
package/engine/helpers.mjs
CHANGED
|
@@ -32,8 +32,9 @@ import yaml from "yaml";
|
|
|
32
32
|
import unidecode from "unidecode";
|
|
33
33
|
import markdownit from "markdown-it";
|
|
34
34
|
import { iconPlugin } from "./content-icons.mjs";
|
|
35
|
-
import { imagePlugin } from "./content-images.mjs";
|
|
36
|
-
import {
|
|
35
|
+
import { imagePlugin, imagesIn } from "./content-images.mjs";
|
|
36
|
+
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
37
|
+
import { foundryAddressProblem, pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
37
38
|
import log from "loglevel";
|
|
38
39
|
|
|
39
40
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -46,7 +47,7 @@ import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
|
46
47
|
// The record accessors only — deriving records reaches the pack router and the
|
|
47
48
|
// manifest emitter, which reach the compilers, which load this module. Reading
|
|
48
49
|
// a record needs none of that.
|
|
49
|
-
import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
50
|
+
import { authoredFrontmatter, isAssetRecord, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
50
51
|
import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
|
|
51
52
|
// One vocabulary of link findings, and one message per class, so the three
|
|
52
53
|
// resolvers cannot word the same defect differently.
|
|
@@ -100,13 +101,16 @@ export const md = markdownit({ html: true })
|
|
|
100
101
|
// translated by the same rule `img:` follows.
|
|
101
102
|
.use(
|
|
102
103
|
imagePlugin((src) => {
|
|
103
|
-
// **Reported
|
|
104
|
+
// **Reported upstream, never here.** A renderer has no channel to
|
|
104
105
|
// report through, and this one runs inside the very passes whose
|
|
105
106
|
// job is to collect findings — a throw would take the whole lint
|
|
106
|
-
// down and lose every other finding in the tree.
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
107
|
+
// down and lose every other finding in the tree. So nothing reaches
|
|
108
|
+
// this point unreported: {@link convertNoteWikilinks} refuses a body
|
|
109
|
+
// image with no address inside the install before a compiler renders
|
|
110
|
+
// one, and the address passes report the same pathname with a line
|
|
111
|
+
// and a column. What is left here is a fallback for a caller with no
|
|
112
|
+
// configuration to resolve against, where the authored pathname is
|
|
113
|
+
// the most honest thing to emit.
|
|
110
114
|
try {
|
|
111
115
|
return resolveImg(src, loadPackConfig()) ?? src;
|
|
112
116
|
} catch {
|
|
@@ -494,19 +498,9 @@ export function resolveImg(raw, config = loadPackConfig()) {
|
|
|
494
498
|
const forms = resolvePathname(raw, config);
|
|
495
499
|
if (forms === null) return null;
|
|
496
500
|
if (forms.foundry !== null) return forms.foundry;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
`and a \`documentation\` package has no asset root to serve it from — ` +
|
|
501
|
-
`Foundry installs no such package. Address a \`/\`-rooted path or a URL.`,
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
throw new Error(
|
|
505
|
-
`package-build: \`${forms.authored}\` names a file the \`${forms.package}\` ` +
|
|
506
|
-
`package ships, and this build declares no relationship with a package of ` +
|
|
507
|
-
`that name, so there is no Foundry address to derive. Declare it under ` +
|
|
508
|
-
`\`relationships\`, or address the file by a \`/\`-rooted path.`,
|
|
509
|
-
);
|
|
501
|
+
// One sentence per class, stated where the rule is, so an art field that
|
|
502
|
+
// refuses and a body image that refuses tell an author the same thing.
|
|
503
|
+
throw new Error(`package-build: ${foundryAddressProblem(raw, config)}.`);
|
|
510
504
|
}
|
|
511
505
|
|
|
512
506
|
/**
|
|
@@ -704,9 +698,18 @@ export function buildContentLinkIndex(
|
|
|
704
698
|
{ skipDirectories, config, records, problems } = {},
|
|
705
699
|
) {
|
|
706
700
|
const docs = [];
|
|
701
|
+
/** The files this package ships, by canonical address. */
|
|
702
|
+
const assets = new Map();
|
|
707
703
|
const resolved = config ?? loadPackConfig();
|
|
708
704
|
assertSuppliedCorpus(records, "buildContentLinkIndex");
|
|
709
705
|
for (const record of records) {
|
|
706
|
+
// An asset's record addresses a file rather than a note, so it becomes
|
|
707
|
+
// no `doc` and takes no part in link resolution — it is keyed for the
|
|
708
|
+
// art fields, which name a file and never a document.
|
|
709
|
+
if (isAssetRecord(record)) {
|
|
710
|
+
if (record.address?.canonical) assets.set(record.address.canonical, record);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
710
713
|
// A documentation journal is a document this tree emits, not a note in
|
|
711
714
|
// it; the note it documents is indexed here and carries its address.
|
|
712
715
|
if (!isNoteRecord(record)) continue;
|
|
@@ -782,7 +785,9 @@ export function buildContentLinkIndex(
|
|
|
782
785
|
`Wikilink index: ${docs.length} local document(s), ` +
|
|
783
786
|
`${foreign.size} foreign address(es)`,
|
|
784
787
|
);
|
|
785
|
-
return buildWikilinkIndex(docs, resolved.foundryPackage, foreign, resolved.contentPackage
|
|
788
|
+
return buildWikilinkIndex(docs, resolved.foundryPackage, foreign, resolved.contentPackage, {
|
|
789
|
+
assets,
|
|
790
|
+
});
|
|
786
791
|
}
|
|
787
792
|
|
|
788
793
|
/**
|
|
@@ -807,6 +812,9 @@ export function buildContentLinkIndex(
|
|
|
807
812
|
* in, addressing a self-link the same way.
|
|
808
813
|
* @param {object} ctx.index - The address index every link resolves through.
|
|
809
814
|
* @param {string} ctx.name - The note, for the message.
|
|
815
|
+
* @param {object} [ctx.config] - The resolved build configuration, which is
|
|
816
|
+
* what says which packages this build can address a file in. Loaded when
|
|
817
|
+
* omitted, exactly as an art field's resolution loads it.
|
|
810
818
|
* @param {string} [ctx.file] - The note's file, so a report names it.
|
|
811
819
|
* @param {number} [ctx.bodyLine] - 1-based file line of the body's first line.
|
|
812
820
|
* @param {number} [ctx.bodyColumn] - 1-based file column of the same character.
|
|
@@ -814,15 +822,24 @@ export function buildContentLinkIndex(
|
|
|
814
822
|
* authored line each body line came from, from {@link expandNoteTables}.
|
|
815
823
|
* @returns {{markdown: string, unresolved: Array<object>}}
|
|
816
824
|
* @throws {Error} On any link that does not resolve — an unlabelled one, a
|
|
817
|
-
* target that is not an address, or an address nothing publishes
|
|
825
|
+
* target that is not an address, or an address nothing publishes — and on a
|
|
826
|
+
* body image whose pathname names no file a Foundry install serves. The error
|
|
818
827
|
* carries `file` and `position`, so a caller reports it in the same form
|
|
819
828
|
* rather than re-deriving one.
|
|
820
829
|
*/
|
|
821
830
|
export function convertNoteWikilinks(
|
|
822
831
|
body,
|
|
823
|
-
{ type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap },
|
|
832
|
+
{ type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap, config },
|
|
824
833
|
) {
|
|
825
|
-
const
|
|
834
|
+
const source = body ?? "";
|
|
835
|
+
// **Embeds first, and on the authored text.** An embed's interior is a
|
|
836
|
+
// wikilink, so resolving it here is what stops the link pass reading
|
|
837
|
+
// `![[wildboar|Wild Boar]]` as a link to a note called `wildboar`. The
|
|
838
|
+
// rewrite it produces carries no newline, so the line every later offset
|
|
839
|
+
// lands on — and the `lineMap` a generated table supplied — survive it, and
|
|
840
|
+
// only the embed's own findings need the authored text to locate against.
|
|
841
|
+
const embedded = resolveEmbeds(source, { index });
|
|
842
|
+
const result = convertWikilinks(embedded.markdown, {
|
|
826
843
|
type,
|
|
827
844
|
id,
|
|
828
845
|
pack,
|
|
@@ -832,14 +849,15 @@ export function convertNoteWikilinks(
|
|
|
832
849
|
/**
|
|
833
850
|
* Where one unresolved link sits, in file coordinates.
|
|
834
851
|
*
|
|
835
|
-
* @param {object} u - An entry of `
|
|
852
|
+
* @param {object} u - An entry of an `unresolved` list.
|
|
853
|
+
* @param {string} text - The body its `offset` indexes into.
|
|
836
854
|
* @returns {{line?: number, column?: number, generated?: boolean}} Empty
|
|
837
855
|
* when the caller supplied no position to resolve against.
|
|
838
856
|
*/
|
|
839
|
-
const locate = (u) =>
|
|
857
|
+
const locate = (u, text) =>
|
|
840
858
|
bodyLine === undefined || u.offset === undefined ?
|
|
841
859
|
{}
|
|
842
|
-
: positionInBody(
|
|
860
|
+
: positionInBody(text, u.offset, {
|
|
843
861
|
bodyLine,
|
|
844
862
|
bodyColumn,
|
|
845
863
|
lineMap,
|
|
@@ -850,10 +868,11 @@ export function convertNoteWikilinks(
|
|
|
850
868
|
*
|
|
851
869
|
* @param {object} u - The offending link.
|
|
852
870
|
* @param {string} message - What is wrong.
|
|
871
|
+
* @param {string} text - The body its `offset` indexes into.
|
|
853
872
|
* @returns {never}
|
|
854
873
|
*/
|
|
855
|
-
const fail = (u, message) => {
|
|
856
|
-
const at = locate(u);
|
|
874
|
+
const fail = (u, message, text) => {
|
|
875
|
+
const at = locate(u, text);
|
|
857
876
|
// A link this build wrote is not at any authored position, so say
|
|
858
877
|
// where it came from rather than implying an edit site.
|
|
859
878
|
const err = new Error(
|
|
@@ -864,6 +883,39 @@ export function convertNoteWikilinks(
|
|
|
864
883
|
throw err;
|
|
865
884
|
};
|
|
866
885
|
|
|
886
|
+
for (const u of embedded.unresolved) {
|
|
887
|
+
fail(u, `${linkFindingMessage(u)} — in "${name}".`, source);
|
|
888
|
+
}
|
|
889
|
+
// A directive that cannot be honoured is refused for the reason an image's
|
|
890
|
+
// is: rendering as the ordinary width looks exactly like a directive that
|
|
891
|
+
// worked, so it fails here rather than publishing a page nobody asked for.
|
|
892
|
+
for (const problem of embedded.problems) {
|
|
893
|
+
fail(problem, `${problem.message} — in "${name}".`, source);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// **One scan, both spellings.** By here an `![[…]]` embed has been
|
|
897
|
+
// rewritten into the ordinary image every surface renders, so a single walk
|
|
898
|
+
// of the rewritten body holds an authored `` and an embed to the
|
|
899
|
+
// same rule — there is no second image path to keep in step.
|
|
900
|
+
//
|
|
901
|
+
// A pathname the rule refuses is **refused here too, not emitted**. The
|
|
902
|
+
// renderer below has no channel to report through and hands the journal the
|
|
903
|
+
// pathname as authored — which for one class resolves against nothing
|
|
904
|
+
// Foundry serves, and for the other resolves here and on neither of the
|
|
905
|
+
// remaining two surfaces. Every surface refuses such a value rather than
|
|
906
|
+
// deriving an address from it, and this is that refusal on the Foundry one:
|
|
907
|
+
// the same one an art field already makes through {@link resolveImg}.
|
|
908
|
+
const images = imagesIn(embedded.markdown);
|
|
909
|
+
if (images.length) {
|
|
910
|
+
const resolved = config ?? loadPackConfig();
|
|
911
|
+
for (const image of images) {
|
|
912
|
+
const problem =
|
|
913
|
+
pathnameProblem(image.src) || foundryAddressProblem(image.src, resolved);
|
|
914
|
+
if (!problem) continue;
|
|
915
|
+
fail({ offset: image.index }, `${problem} — in "${name}".`, embedded.markdown);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
867
919
|
for (const u of result.unresolved) {
|
|
868
920
|
// Every class fails, and every class is worded by the shared table.
|
|
869
921
|
// The three resolvers read one authored link, so an author who
|
|
@@ -873,7 +925,7 @@ export function convertNoteWikilinks(
|
|
|
873
925
|
//
|
|
874
926
|
// The note's name is appended rather than woven in: the message is the
|
|
875
927
|
// defect, the name is the context this build can add.
|
|
876
|
-
fail(u, `${linkFindingMessage(u)} — in "${name}"
|
|
928
|
+
fail(u, `${linkFindingMessage(u)} — in "${name}".`, embedded.markdown);
|
|
877
929
|
}
|
|
878
930
|
return result;
|
|
879
931
|
}
|
package/engine/index-records.mjs
CHANGED
|
@@ -59,8 +59,30 @@ export const DERIVED_KEYS = Object.freeze([
|
|
|
59
59
|
"foundry",
|
|
60
60
|
"documentation",
|
|
61
61
|
"documents",
|
|
62
|
+
"asset",
|
|
62
63
|
]);
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Recursively sort an object's keys, so serialization is order-independent.
|
|
67
|
+
*
|
|
68
|
+
* Arrays keep their order — it is authored — but every object inside one is
|
|
69
|
+
* sorted too. Anything that is not a plain object is returned as it is.
|
|
70
|
+
*
|
|
71
|
+
* @param {unknown} value - The value to normalize.
|
|
72
|
+
* @returns {unknown} The value with every plain object's keys in sorted order.
|
|
73
|
+
*/
|
|
74
|
+
export function sortKeysDeep(value) {
|
|
75
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
76
|
+
if (value === null || typeof value !== "object") return value;
|
|
77
|
+
// A Date or any other exotic object would lose itself in a rebuild from
|
|
78
|
+
// entries, and YAML frontmatter can produce one.
|
|
79
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return value;
|
|
80
|
+
/** @type {Record<string, unknown>} */
|
|
81
|
+
const out = {};
|
|
82
|
+
for (const key of Object.keys(value).sort()) out[key] = sortKeysDeep(value[key]);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
/**
|
|
65
87
|
* The file a record was read from, as an absolute path.
|
|
66
88
|
*
|
|
@@ -109,18 +131,55 @@ export function authoredFrontmatter(record) {
|
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
/**
|
|
112
|
-
* Whether a record
|
|
134
|
+
* Whether a record addresses a **file** rather than a note.
|
|
135
|
+
*
|
|
136
|
+
* The index holds two record shapes in one file, and this is how a reader tells
|
|
137
|
+
* them apart. An asset record carries no frontmatter, no anchors and no
|
|
138
|
+
* `foundry` block — a `.webp` declares nothing about itself — so every pass that
|
|
139
|
+
* reads a note's fields has to skip it, and the `asset` block is what marks it.
|
|
140
|
+
*
|
|
141
|
+
* Asked of the block rather than of `type`, so a fourth asset type needs no
|
|
142
|
+
* edit here: what makes a record an asset's is that it describes a file, and the
|
|
143
|
+
* block is the description.
|
|
144
|
+
*
|
|
145
|
+
* @param {Record<string, any>} record - An index record.
|
|
146
|
+
* @returns {boolean} True for an asset's record.
|
|
147
|
+
*/
|
|
148
|
+
export function isAssetRecord(record) {
|
|
149
|
+
return Boolean(record?.asset);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether a record is a note's, rather than a documentation journal's or an
|
|
154
|
+
* asset's.
|
|
113
155
|
*
|
|
114
156
|
* An item note yields two records — itself and the JournalEntry its prose
|
|
115
157
|
* compiles into — and the second is a document, not a note: it has no file of
|
|
116
158
|
* its own to read, no frontmatter an author wrote, and its `type` is the
|
|
117
159
|
* virtual `doc<type>` that `readQualifier` resolves rather than a type any tree
|
|
118
|
-
* declares.
|
|
119
|
-
*
|
|
160
|
+
* declares. An asset's record is not a note either, for the stronger reason that
|
|
161
|
+
* nobody authored it at all. A reader enumerating the corpus wants the notes;
|
|
162
|
+
* one resolving an address wants all three.
|
|
120
163
|
*
|
|
121
164
|
* @param {Record<string, any>} record - An index record.
|
|
122
165
|
* @returns {boolean} True for a note's own record.
|
|
123
166
|
*/
|
|
124
167
|
export function isNoteRecord(record) {
|
|
125
|
-
return !record?.documents;
|
|
168
|
+
return !record?.documents && !isAssetRecord(record);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The path a record names inside its package, whichever shape it is.
|
|
173
|
+
*
|
|
174
|
+
* The two shapes state it differently and honestly: a note names the `.md` it
|
|
175
|
+
* was parsed from, relative to the content root, while an asset names the file
|
|
176
|
+
* it *is*, relative to the package's asset directory. Both are paths within one
|
|
177
|
+
* package, so one total order covers the whole index — which is what keeps the
|
|
178
|
+
* artifact byte-stable across a rebuild.
|
|
179
|
+
*
|
|
180
|
+
* @param {Record<string, any>} record - An index record.
|
|
181
|
+
* @returns {string} The path, or `""` for a record naming neither.
|
|
182
|
+
*/
|
|
183
|
+
export function recordPath(record) {
|
|
184
|
+
return String(record?.file?.path ?? record?.asset?.path ?? "");
|
|
126
185
|
}
|
package/engine/index.mjs
CHANGED
|
@@ -120,6 +120,18 @@ export * as foundryEntries from "./foundry-entries.mjs";
|
|
|
120
120
|
/** Publishing the note tree as a queryable JSON Lines index. */
|
|
121
121
|
export * as contentIndex from "./content-index.mjs";
|
|
122
122
|
|
|
123
|
+
/** The asset types — `icon`, `image`, `audio` — and the roots they are walked from. */
|
|
124
|
+
export * as assetTypes from "./asset-types.mjs";
|
|
125
|
+
|
|
126
|
+
/** The asset record: one index line per addressable file, and its provenance. */
|
|
127
|
+
export * as assetIndex from "./asset-index.mjs";
|
|
128
|
+
|
|
129
|
+
/** The `<package>` segment's registry: the names no repository may claim. */
|
|
130
|
+
export * as packages from "./packages.mjs";
|
|
131
|
+
|
|
132
|
+
/** The toolchain's own content index — the files it ships, addressed. */
|
|
133
|
+
export * as packagebuildIndex from "./packagebuild-index.mjs";
|
|
134
|
+
|
|
123
135
|
/** Publishing a content tree as a website: the pass, and its integrity gates. */
|
|
124
136
|
export * as siteBuild from "./site-build.mjs";
|
|
125
137
|
|
|
@@ -138,6 +150,9 @@ export * as contentHtml from "./content-html.mjs";
|
|
|
138
150
|
/** An image saying how wide it is and where it sits, in two closed vocabularies. */
|
|
139
151
|
export * as contentImages from "./content-images.mjs";
|
|
140
152
|
|
|
153
|
+
/** `![[…]]` — the wikilink that renders a picture where it stands. */
|
|
154
|
+
export * as contentEmbeds from "./content-embeds.mjs";
|
|
155
|
+
|
|
141
156
|
/** One authored pathname, and the four addresses the surfaces derive from it. */
|
|
142
157
|
export * as pathnames from "./pathnames.mjs";
|
|
143
158
|
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { subtypeRow } from "./document-subtypes.mjs";
|
|
26
27
|
import { assertInfoboxSet, buildInfoboxes } from "./infobox.mjs";
|
|
28
|
+
import { packRouter } from "./pack-router.mjs";
|
|
27
29
|
import { carriesSystemBlock, resolveFieldValue } from "./system-block.mjs";
|
|
28
|
-
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
30
|
+
import { DEFAULT_DOCUMENT_SUBTYPES, KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
29
31
|
import { SOHL_INFOBOX } from "../sohl/infobox.mjs";
|
|
30
32
|
import { HM3_INFOBOX } from "../hm3/infobox.mjs";
|
|
31
33
|
|
|
@@ -52,6 +54,49 @@ export function infoboxFor(system) {
|
|
|
52
54
|
return KNOWN_INFOBOXES.find((entry) => entry.system === system);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Whether one system compiles a document for one note.
|
|
59
|
+
*
|
|
60
|
+
* This is what a system box's _available_ asserts, and it is the compile's own
|
|
61
|
+
* question rather than a reading of the frontmatter. A note carrying no block
|
|
62
|
+
* is not a note a system has nothing for: where the pack compiling its document
|
|
63
|
+
* declares no `system:`, the document is built from `data:` and the field
|
|
64
|
+
* defaults and ships exactly like any other.
|
|
65
|
+
*
|
|
66
|
+
* Three statements answer it, and they are the three the compile itself
|
|
67
|
+
* follows:
|
|
68
|
+
*
|
|
69
|
+
* 1. **The map** says which document class this system makes of the note's
|
|
70
|
+
* type. No row, no document — and no box either, which is why a caller
|
|
71
|
+
* reaching here already has one.
|
|
72
|
+
* 2. **The router** says which pack that document goes to, read from the pack
|
|
73
|
+
* list this build is driven by.
|
|
74
|
+
* 3. **That pack's `system:`** decides the rest. Declaring one, it writes that
|
|
75
|
+
* system's data and takes only notes that say something about it — the rule
|
|
76
|
+
* {@link module:engine/base-compiler.BasePackCompiler#eligibleFor} applies,
|
|
77
|
+
* asked here from outside. Declaring none, it is compiled by the fallback
|
|
78
|
+
* pass, which needs no block and answers for
|
|
79
|
+
* {@link module:engine/subtype-registry.DEFAULT_DOCUMENT_SUBTYPES} alone —
|
|
80
|
+
* so a tree with no HM3 pack ships no HM3 document however a note is
|
|
81
|
+
* written.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} fm - The note's frontmatter.
|
|
84
|
+
* @param {object} map - The system's note-type → document-subtype map.
|
|
85
|
+
* @param {object} router - The pack router this build is driven by.
|
|
86
|
+
* @returns {boolean} True when this system compiles a document for this note.
|
|
87
|
+
*/
|
|
88
|
+
export function compilesSystemDocument(fm, map, router) {
|
|
89
|
+
const row = subtypeRow(map, fm?.type);
|
|
90
|
+
if (!row?.document) return false;
|
|
91
|
+
|
|
92
|
+
const packName = router.resolveOrNull(fm, row.document, map.system);
|
|
93
|
+
if (!packName) return false;
|
|
94
|
+
|
|
95
|
+
const packSystem = router.systemOf(packName);
|
|
96
|
+
if (!packSystem) return map.system === DEFAULT_DOCUMENT_SUBTYPES.system;
|
|
97
|
+
return packSystem === map.system && carriesSystemBlock(fm, map.block);
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/**
|
|
56
101
|
* Every box one note carries, wired to the registries this toolchain ships.
|
|
57
102
|
*
|
|
@@ -63,15 +108,17 @@ export function infoboxFor(system) {
|
|
|
63
108
|
* @param {object} [options] - Options.
|
|
64
109
|
* @param {(ref: unknown, hint?: object) => object|undefined} [options.resolve] -
|
|
65
110
|
* Resolves a reference to `{name, url?, uuid?, address?, subType?}`.
|
|
111
|
+
* @param {object} [options.router] - The pack router deciding which system
|
|
112
|
+
* compiles a document for this note. Defaults to the consuming repository's.
|
|
66
113
|
* @returns {object[]} The boxes, in the order every medium renders them.
|
|
67
114
|
* @throws {Error} When the built set disagrees with what the note's type maps
|
|
68
115
|
* to — see {@link module:engine/infobox.assertInfoboxSet}.
|
|
69
116
|
*/
|
|
70
|
-
export function noteInfoboxes(fm, { resolve } = {}) {
|
|
117
|
+
export function noteInfoboxes(fm, { resolve, router = packRouter() } = {}) {
|
|
71
118
|
const boxes = buildInfoboxes(fm, {
|
|
72
119
|
maps: KNOWN_DOCUMENT_SUBTYPE_MAPS,
|
|
73
120
|
providers: KNOWN_INFOBOXES,
|
|
74
|
-
|
|
121
|
+
compilesDocument: (note, map) => compilesSystemDocument(note, map, router),
|
|
75
122
|
resolveField: resolveFieldValue,
|
|
76
123
|
resolve,
|
|
77
124
|
});
|
|
@@ -360,9 +360,10 @@ export function infoboxTypstPreamble() {
|
|
|
360
360
|
'#let infobox-rule = rgb("#7c3b1e")',
|
|
361
361
|
'#let infobox-faint = rgb("#6b6357")',
|
|
362
362
|
'#let infobox-hairline = rgb("#c9bfa8")',
|
|
363
|
+
'#let infobox-fill = rgb("#eae2d0")',
|
|
363
364
|
"#let infobox-panel(body) = block(width: 100%, breakable: true, " +
|
|
364
|
-
"inset: (x: 9pt, y: 8pt), below: 0.8em, " +
|
|
365
|
-
"
|
|
365
|
+
"inset: (x: 9pt, y: 8pt), below: 0.8em, radius: 2pt, " +
|
|
366
|
+
"fill: infobox-fill, stroke: 0.5pt + infobox-hairline)[" +
|
|
366
367
|
"#set par(first-line-indent: 0em, justify: false, leading: 0.40em)\n#body]",
|
|
367
368
|
'#let infobox-title(t) = { text(size: 9pt, weight: "bold", tracking: 1.3pt, ' +
|
|
368
369
|
"fill: infobox-rule)[#upper(t)]; v(0.20em) }",
|
|
@@ -373,7 +374,7 @@ export function infoboxTypstPreamble() {
|
|
|
373
374
|
"#let infobox-cell(lab, val) = align(center)[" +
|
|
374
375
|
"#text(size: 7pt, tracking: 0.6pt, fill: infobox-faint)[#lab]#h(2.5pt)" +
|
|
375
376
|
'#text(size: 8.6pt, weight: "bold")[#val]]',
|
|
376
|
-
"#let infobox-runin(lab, body) = block(below: 0.
|
|
377
|
+
"#let infobox-runin(lab, body) = block(below: 0.78em, breakable: false)[" +
|
|
377
378
|
"#set par(justify: false, first-line-indent: 0em, hanging-indent: 0.5cm, leading: 0.40em)\n" +
|
|
378
379
|
'#text(size: 7.8pt)[#if lab != "" [ #text(weight: "bold")[#lab: ] ]#body]]',
|
|
379
380
|
'#let infobox-statement(t) = text(size: 8pt, style: "italic", fill: infobox-faint)[#t]',
|
package/engine/infobox.mjs
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
* appears before it.
|
|
57
57
|
* 2. **It contains no image.** A picture is authored in the text with its own
|
|
58
58
|
* directive, and its position governs. {@link NOTE_FIELD_PRESENTATION}
|
|
59
|
-
* withholds
|
|
59
|
+
* withholds the art slots and `overlay` for that reason and no other.
|
|
60
60
|
* 3. **A section is the unit that flows.** Sections are whole and unbreakable;
|
|
61
61
|
* the panel breaks between them. This is what lets a long box cross a
|
|
62
62
|
* column or page boundary without splitting a stat grid.
|
|
@@ -266,8 +266,10 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
266
266
|
color: Object.freeze({
|
|
267
267
|
withheld: "sidebar machinery, not a fact about the subject",
|
|
268
268
|
}),
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
icon: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
270
|
+
tokenIcon: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
271
|
+
bgImage: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
272
|
+
banner: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
271
273
|
overlay: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
272
274
|
|
|
273
275
|
birthday: Object.freeze({ label: "Born" }),
|
|
@@ -834,9 +836,11 @@ export function systemRowsSection(
|
|
|
834
836
|
* build ships, which decide the box set.
|
|
835
837
|
* @param {readonly object[]} [options.providers] - The systems' infobox
|
|
836
838
|
* declarations, keyed by `system`.
|
|
837
|
-
* @param {(fm: object,
|
|
838
|
-
* Whether
|
|
839
|
-
* {@link NOT_AVAILABLE}.
|
|
839
|
+
* @param {(fm: object, map: object) => boolean} options.compilesDocument -
|
|
840
|
+
* Whether that system compiles a document for this note, which decides
|
|
841
|
+
* {@link NOT_AVAILABLE}. Asked of the routing and the passes rather than of
|
|
842
|
+
* the frontmatter: a pack declaring no `system:` compiles a note from `data:`
|
|
843
|
+
* and the field defaults, so the block's presence is not the question.
|
|
840
844
|
* @param {(field: object, fm: object, opts: object) => object} options.resolveField -
|
|
841
845
|
* Resolves one declared field against the note.
|
|
842
846
|
* @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
|
|
@@ -848,7 +852,7 @@ export function buildInfoboxes(fm, options) {
|
|
|
848
852
|
const {
|
|
849
853
|
maps,
|
|
850
854
|
providers = [],
|
|
851
|
-
|
|
855
|
+
compilesDocument,
|
|
852
856
|
resolveField,
|
|
853
857
|
resolve,
|
|
854
858
|
vocabulary = NOTE_VOCABULARY,
|
|
@@ -860,7 +864,7 @@ export function buildInfoboxes(fm, options) {
|
|
|
860
864
|
for (const map of maps ?? []) {
|
|
861
865
|
if (!subtypeRow(map, fm?.type)) continue;
|
|
862
866
|
const provider = providers.find((entry) => entry.system === map.system);
|
|
863
|
-
const available = Boolean(
|
|
867
|
+
const available = Boolean(compilesDocument(fm, map));
|
|
864
868
|
const box = {
|
|
865
869
|
id: map.system,
|
|
866
870
|
kind: "system",
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
import log from "loglevel";
|
|
50
50
|
|
|
51
|
-
import { resolveName
|
|
51
|
+
import { resolveName } from "./helpers.mjs";
|
|
52
52
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
53
53
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
54
54
|
import { foundryPackageId } from "./content-package.mjs";
|
|
@@ -117,15 +117,15 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
117
117
|
static requiresSystemBlock = true;
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* An Item carries
|
|
121
|
-
* note
|
|
120
|
+
* An Item carries one piece of art — its icon — which this pass writes from
|
|
121
|
+
* the address the note names, falling back to the type's default art.
|
|
122
122
|
*
|
|
123
|
-
* `
|
|
124
|
-
*
|
|
123
|
+
* `tokenIcon` is **not** among them: an Item places no token, so it has
|
|
124
|
+
* nowhere to put a second picture.
|
|
125
125
|
*
|
|
126
126
|
* @type {readonly string[]}
|
|
127
127
|
*/
|
|
128
|
-
static emitsArt = Object.freeze(["
|
|
128
|
+
static emitsArt = Object.freeze(["icon"]);
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* The note-type → document-subtype map this pass compiles against.
|
|
@@ -372,10 +372,10 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
372
372
|
// both registries are keyed by content type — while the document's
|
|
373
373
|
// own subtype comes from the system's map.
|
|
374
374
|
type: subType,
|
|
375
|
-
// Nullish, not `||`:
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
img:
|
|
375
|
+
// Nullish, not `||`: an art address resolves to `null` for a note
|
|
376
|
+
// that names none and `""` for one that wants none, and only the
|
|
377
|
+
// first may be replaced by the type's default.
|
|
378
|
+
img: this.artPath(fm, "icon") ?? itemArt(type, system),
|
|
379
379
|
_id: id,
|
|
380
380
|
system: built,
|
|
381
381
|
effects: Array.isArray(effects) ? [...effects] : [],
|
package/engine/journals.mjs
CHANGED
|
@@ -440,6 +440,9 @@ export class Journals extends BasePackCompiler {
|
|
|
440
440
|
// URLs: a player reading this at the table stays in Foundry.
|
|
441
441
|
const boxes = noteInfoboxes(fm, {
|
|
442
442
|
resolve: (ref, hint) => resolveReference(this.linkIndex, ref, hint),
|
|
443
|
+
// This compile's own router, so the panel's `available` is decided
|
|
444
|
+
// by the pack list this build is being driven by.
|
|
445
|
+
router: this.router,
|
|
443
446
|
});
|
|
444
447
|
|
|
445
448
|
return buildJournalEntry({
|
package/engine/macros.mjs
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
|
|
52
52
|
import log from "loglevel";
|
|
53
53
|
|
|
54
|
-
import { sohlField, resolveName,
|
|
54
|
+
import { sohlField, resolveName, defaultStats, folderField } from "./helpers.mjs";
|
|
55
55
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
56
56
|
import { splitPages } from "./journals.mjs";
|
|
57
57
|
|
|
@@ -79,11 +79,11 @@ export const MACRO_TYPES = Object.freeze(["script", "chat"]);
|
|
|
79
79
|
export const MACRO_SCOPES = Object.freeze(["global", "actors", "actor"]);
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Foundry's own default macro artwork, used when a note
|
|
82
|
+
* Foundry's own default macro artwork, used when a note names no `icon`.
|
|
83
83
|
*
|
|
84
|
-
* A core path, deliberately: it
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* A core path, deliberately: it names a file Foundry itself ships rather than an
|
|
85
|
+
* address in any package, so it is stated after the art resolution rather than
|
|
86
|
+
* as something a note could author.
|
|
87
87
|
*/
|
|
88
88
|
export const DEFAULT_MACRO_IMG = "icons/svg/dice-target.svg";
|
|
89
89
|
|
|
@@ -248,10 +248,13 @@ export function resolveMacroScope(fm, label) {
|
|
|
248
248
|
* @param {string} opts.command - The command, from {@link macroCommand}.
|
|
249
249
|
* @param {string|null} [opts.folder] - The resolved folder id.
|
|
250
250
|
* @param {object} [opts.stats] - The `_stats` block.
|
|
251
|
+
* @param {string|null} [opts.img] - The resolved art, from the note's `icon`
|
|
252
|
+
* address. Passed in rather than resolved here, because an address is
|
|
253
|
+
* answered by the compile's index and this function takes no index.
|
|
251
254
|
* @returns {MacroDocument} The Macro document.
|
|
252
255
|
* @throws {Error} When the frontmatter's macro type or scope is unusable.
|
|
253
256
|
*/
|
|
254
|
-
export function buildMacroEntry(fm, { command, folder = null, stats = defaultStats() }) {
|
|
257
|
+
export function buildMacroEntry(fm, { command, folder = null, stats = defaultStats(), img }) {
|
|
255
258
|
const name = resolveName(fm);
|
|
256
259
|
const id = fm.id;
|
|
257
260
|
return {
|
|
@@ -262,8 +265,8 @@ export function buildMacroEntry(fm, { command, folder = null, stats = defaultSta
|
|
|
262
265
|
type: resolveMacroType(fm, name),
|
|
263
266
|
author: null,
|
|
264
267
|
// Nullish, not `||`: a macro note that names no art gets the
|
|
265
|
-
// shared default, one that writes `
|
|
266
|
-
img:
|
|
268
|
+
// shared default, one that writes `icon: ""` ships blank on purpose.
|
|
269
|
+
img: (img === undefined ? null : img) ?? DEFAULT_MACRO_IMG,
|
|
267
270
|
scope: resolveMacroScope(fm, name),
|
|
268
271
|
command,
|
|
269
272
|
folder,
|
|
@@ -289,19 +292,18 @@ export class Macros extends BasePackCompiler {
|
|
|
289
292
|
|
|
290
293
|
/**
|
|
291
294
|
* The command must be exactly what the author typed, so this pass reads the
|
|
292
|
-
* note as authored: no table expansion
|
|
293
|
-
*
|
|
294
|
-
* compiles the converted copy of the same body independently.
|
|
295
|
+
* note as authored: no table expansion and no wikilink conversion. The
|
|
296
|
+
* journals pass compiles the converted copy of the same body independently.
|
|
295
297
|
*/
|
|
296
298
|
static convertsWikilinks = false;
|
|
297
299
|
|
|
298
300
|
/**
|
|
299
|
-
* A Macro carries
|
|
301
|
+
* A Macro carries one piece of art — the tile Foundry shows on the hotbar —
|
|
300
302
|
* defaulting to {@link DEFAULT_MACRO_IMG} where the note names none.
|
|
301
303
|
*
|
|
302
304
|
* @type {readonly string[]}
|
|
303
305
|
*/
|
|
304
|
-
static emitsArt = Object.freeze(["
|
|
306
|
+
static emitsArt = Object.freeze(["icon"]);
|
|
305
307
|
|
|
306
308
|
/**
|
|
307
309
|
* @param {object} fm - The note's frontmatter.
|
|
@@ -323,6 +325,7 @@ export class Macros extends BasePackCompiler {
|
|
|
323
325
|
const name = resolveName(fm);
|
|
324
326
|
return buildMacroEntry(fm, {
|
|
325
327
|
command: macroCommand(body, name),
|
|
328
|
+
img: this.artPath(fm, "icon"),
|
|
326
329
|
folder: this.folderResolver(folderField(fm).value, {
|
|
327
330
|
isAddress: folderField(fm).isAddress,
|
|
328
331
|
}),
|