@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/site-build.mjs
CHANGED
|
@@ -55,7 +55,6 @@ import { protectCode } from "./code-fences.mjs";
|
|
|
55
55
|
import { expandContentTables } from "./content-tables.mjs";
|
|
56
56
|
import { renderImageFigures } from "./content-images.mjs";
|
|
57
57
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
58
|
-
import { ART_FIELDS } from "./frontmatter-lint.mjs";
|
|
59
58
|
import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
60
59
|
import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
61
60
|
import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
@@ -63,10 +62,12 @@ import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
|
63
62
|
import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
|
|
64
63
|
import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
65
64
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
65
|
+
import { routerFor } from "./pack-router.mjs";
|
|
66
66
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
67
67
|
// The corpus, from the one pass that derives it.
|
|
68
68
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
69
69
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
70
|
+
import { ART_SLOTS, artPathname, assetAddressIndex } from "./art-fields.mjs";
|
|
70
71
|
import {
|
|
71
72
|
checkHomepageCount,
|
|
72
73
|
homepageDestination,
|
|
@@ -627,13 +628,16 @@ export function sectionFrontmatter(meta) {
|
|
|
627
628
|
* for.
|
|
628
629
|
* @param {(data: object, page: object) => void} [options.decorate] - Called
|
|
629
630
|
* with each page's frontmatter, for whatever a consumer's own pass adds.
|
|
631
|
+
* @param {(value: unknown, type: string) => string|null} [options.artSrc] -
|
|
632
|
+
* Translates an art address into the pathname the website resolver takes, or
|
|
633
|
+
* `null` where nothing answers it.
|
|
630
634
|
* @param {(src: string) => string} [options.webSrc] - Translates an authored
|
|
631
635
|
* pathname into the address the website serves. Every artwork field goes
|
|
632
636
|
* through it, so a page's `img:` and its body images name the same file the
|
|
633
637
|
* same way.
|
|
634
638
|
* @returns {object} The frontmatter to write.
|
|
635
639
|
*/
|
|
636
|
-
export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc }) {
|
|
640
|
+
export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc, artSrc }) {
|
|
637
641
|
const { fm, name, slug, sec, isReadme } = page;
|
|
638
642
|
let data;
|
|
639
643
|
if (page.kind === "content") {
|
|
@@ -665,29 +669,39 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc })
|
|
|
665
669
|
if (meta) Object.assign(data, sectionFrontmatter(meta));
|
|
666
670
|
}
|
|
667
671
|
delete data.aliases;
|
|
668
|
-
if (webSrc) resolveArtFields(data, webSrc);
|
|
672
|
+
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
669
673
|
return data;
|
|
670
674
|
}
|
|
671
675
|
|
|
672
676
|
/**
|
|
673
|
-
* Rewrite a page's artwork
|
|
677
|
+
* Rewrite a page's artwork addresses into the URLs the website serves.
|
|
674
678
|
*
|
|
675
|
-
* The
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
679
|
+
* The slots are read from {@link module:engine/art-fields.ART_SLOTS}, so a slot
|
|
680
|
+
* added to the vocabulary reaches the page without anyone remembering this
|
|
681
|
+
* function exists. Only an authored **string** is touched: `null` is a note
|
|
682
|
+
* naming no art and `""` is one naming none on purpose, and neither is an
|
|
683
|
+
* address to resolve.
|
|
684
|
+
*
|
|
685
|
+
* **An address nothing answers is dropped**, not emitted as written. The theme
|
|
686
|
+
* renders nothing where a value is absent — its silent-disappear convention —
|
|
687
|
+
* and a raw address left in place would reach the reader as a broken image
|
|
688
|
+
* source with nothing saying why.
|
|
680
689
|
*
|
|
681
690
|
* @param {object} data - The frontmatter being emitted, rewritten in place.
|
|
682
|
-
* @param {(src: string) => string} webSrc - The website's resolver.
|
|
691
|
+
* @param {(src: string) => string} webSrc - The website's pathname resolver.
|
|
692
|
+
* @param {(value: unknown, type: string) => string|null} artSrc - The address
|
|
693
|
+
* resolver, yielding the pathname `webSrc` takes.
|
|
683
694
|
* @returns {void}
|
|
684
695
|
*/
|
|
685
|
-
function resolveArtFields(data, webSrc) {
|
|
686
|
-
for (const { key,
|
|
687
|
-
const holder =
|
|
688
|
-
|
|
696
|
+
function resolveArtFields(data, webSrc, artSrc) {
|
|
697
|
+
for (const { key, type } of ART_SLOTS) {
|
|
698
|
+
const holder = isPlainObject(data.data) ? data.data : null;
|
|
699
|
+
if (!holder) continue;
|
|
700
|
+
const value = holder[key];
|
|
689
701
|
if (typeof value !== "string" || value === "") continue;
|
|
690
|
-
|
|
702
|
+
const pathname = artSrc(value, type);
|
|
703
|
+
if (pathname === null) delete holder[key];
|
|
704
|
+
else holder[key] = webSrc(pathname);
|
|
691
705
|
}
|
|
692
706
|
}
|
|
693
707
|
|
|
@@ -758,8 +772,17 @@ export function renderPages(pages, options) {
|
|
|
758
772
|
linkable = (d) => Boolean(d.fm.shortcode),
|
|
759
773
|
sqlTables,
|
|
760
774
|
config,
|
|
775
|
+
records = [],
|
|
761
776
|
} = options;
|
|
762
777
|
|
|
778
|
+
// The address space the art slots and the body's embeds resolve against:
|
|
779
|
+
// the files this package ships, and the ones a dependency published.
|
|
780
|
+
const artIndex = assetAddressIndex(records, {
|
|
781
|
+
config,
|
|
782
|
+
foreign,
|
|
783
|
+
types: index?.contentTypes ?? [],
|
|
784
|
+
});
|
|
785
|
+
|
|
763
786
|
const tableErrors = [];
|
|
764
787
|
const wikiErrors = [];
|
|
765
788
|
const imageErrors = [];
|
|
@@ -817,9 +840,11 @@ export function renderPages(pages, options) {
|
|
|
817
840
|
type: page.fm.type ?? null,
|
|
818
841
|
errors: wikiErrors,
|
|
819
842
|
foreignIndex: foreign.index,
|
|
843
|
+
assets: artIndex,
|
|
820
844
|
});
|
|
821
845
|
|
|
822
846
|
const webSrc = webAddresses(page.file);
|
|
847
|
+
const artSrc = (value, type) => artPathname(artIndex, value, type).pathname;
|
|
823
848
|
const resolve = (text) => {
|
|
824
849
|
let t = text;
|
|
825
850
|
if (pass.beforeLinks) t = pass.beforeLinks(t, page);
|
|
@@ -851,7 +876,7 @@ export function renderPages(pages, options) {
|
|
|
851
876
|
body = markdown;
|
|
852
877
|
}
|
|
853
878
|
|
|
854
|
-
const data = pageFrontmatter(page, { readmeSections, decorate, webSrc });
|
|
879
|
+
const data = pageFrontmatter(page, { readmeSections, decorate, webSrc, artSrc });
|
|
855
880
|
const dest = path.join(outRoot, pageDestination(page));
|
|
856
881
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
857
882
|
fs.writeFileSync(dest, matter.stringify(protectCode(body, resolve), data));
|
|
@@ -1235,6 +1260,7 @@ export function buildSite({ config, outRoot, sqlTables } = {}) {
|
|
|
1235
1260
|
outRoot: out,
|
|
1236
1261
|
sqlTables,
|
|
1237
1262
|
config: resolved,
|
|
1263
|
+
records: ctx.records,
|
|
1238
1264
|
index: gates.index,
|
|
1239
1265
|
foreign: gates.foreign,
|
|
1240
1266
|
universe: tableUniverse(pages),
|
|
@@ -1254,6 +1280,7 @@ export function buildSite({ config, outRoot, sqlTables } = {}) {
|
|
|
1254
1280
|
// book from one definition.
|
|
1255
1281
|
data.infoboxes = noteInfoboxes(page.fm, {
|
|
1256
1282
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
1283
|
+
router: routerFor(resolved),
|
|
1257
1284
|
});
|
|
1258
1285
|
},
|
|
1259
1286
|
});
|
package/engine/site-index.mjs
CHANGED
|
@@ -322,6 +322,8 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
322
322
|
* diagnostic names. Absent, `src` stands in.
|
|
323
323
|
* @param {Map<string, object>} [options.foreignIndex] - The foreign index, for
|
|
324
324
|
* resolvers that distinguish a foreign hit from a local one.
|
|
325
|
+
* @param {object} [options.assets] - The address space an `![[…]]` embed
|
|
326
|
+
* resolves against, shaped as every asset resolver reads one.
|
|
325
327
|
* @returns {object} The resolver context.
|
|
326
328
|
*
|
|
327
329
|
* There is deliberately **no `manifestsComplete`**. It used to let a resolver
|
|
@@ -330,10 +332,17 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
330
332
|
* checker never had it and one authored link must not get two verdicts. A
|
|
331
333
|
* caller still passing it is ignored rather than obeyed.
|
|
332
334
|
*/
|
|
333
|
-
export function wikiContext(
|
|
335
|
+
export function wikiContext(
|
|
336
|
+
built,
|
|
337
|
+
{ src, file, type = null, errors, foreignIndex = new Map(), assets },
|
|
338
|
+
) {
|
|
334
339
|
return {
|
|
335
340
|
index: built.index,
|
|
336
341
|
foreign: foreignIndex,
|
|
342
|
+
// The address space an embed resolves against. Separate from `index`
|
|
343
|
+
// because the two hold different record shapes: a note's entry carries
|
|
344
|
+
// a URL and a name, an asset's carries the path to a file.
|
|
345
|
+
assets,
|
|
337
346
|
collide: built.ambiguous,
|
|
338
347
|
sections: built.sections,
|
|
339
348
|
contentTypes: built.contentTypes,
|
|
@@ -65,6 +65,27 @@ export const KNOWN_DOCUMENT_SUBTYPE_MAPS = Object.freeze([
|
|
|
65
65
|
HM3_DOCUMENT_SUBTYPES,
|
|
66
66
|
]);
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The map a pack declaring no `system:` compiles its documents against.
|
|
70
|
+
*
|
|
71
|
+
* A pack names the system its documents are shaped for, and one declaring none
|
|
72
|
+
* is compiled by the fallback pass for its document type — a single-system
|
|
73
|
+
* package's ordinary arrangement, and the one `sohl-thalorna` ships. That pass
|
|
74
|
+
* follows one map whatever a note carries, which makes this the answer to "a
|
|
75
|
+
* document compiled here belongs to *which* system?" wherever a pack has not
|
|
76
|
+
* said.
|
|
77
|
+
*
|
|
78
|
+
* Stated here rather than read off the compiler classes because those are not
|
|
79
|
+
* reachable from a leaf: `engine/item-compiler.mjs` imports the journals pass,
|
|
80
|
+
* which draws infoboxes, so anything the infobox reads cannot import a pass.
|
|
81
|
+
* `tests/default-document-subtypes.test.ts` holds this to `compilerFor`, which
|
|
82
|
+
* is where the fallback is actually decided — so the two cannot drift apart in
|
|
83
|
+
* silence.
|
|
84
|
+
*
|
|
85
|
+
* @type {import("./document-subtypes.mjs").DocumentSubtypeMap}
|
|
86
|
+
*/
|
|
87
|
+
export const DEFAULT_DOCUMENT_SUBTYPES = SOHL_DOCUMENT_SUBTYPES;
|
|
88
|
+
|
|
68
89
|
/**
|
|
69
90
|
* Every note type any shipped map compiles into an **Actor**.
|
|
70
91
|
*
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import { slugify } from "./content-slug.mjs";
|
|
|
61
61
|
/** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
|
|
62
62
|
export { slugify };
|
|
63
63
|
import { authoredLabel, WIKILINK, isSamePage, parseWikilink } from "./wikilink-syntax.mjs";
|
|
64
|
+
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* The index key a **piped** target resolves to, or `null` when it does not
|
|
@@ -300,15 +301,25 @@ function isPlainMap(value) {
|
|
|
300
301
|
* web pages, so the author wrote a real address and there is simply nothing to
|
|
301
302
|
* link to.
|
|
302
303
|
*
|
|
304
|
+
* ## Embeds first, and they are not links
|
|
305
|
+
*
|
|
306
|
+
* `![[address|label]]` names a file rather than a note, so it is resolved into
|
|
307
|
+
* an ordinary markdown image before anything looks for a link — which is what
|
|
308
|
+
* stops the link pass reading an embed's interior as a link to a note nobody
|
|
309
|
+
* wrote. The image the rewrite leaves behind is what
|
|
310
|
+
* {@link module:engine/content-images.renderImageFigures} turns into a figure
|
|
311
|
+
* for the website, and what the book reads its staging list out of.
|
|
312
|
+
*
|
|
303
313
|
* @param {string} body - The markdown body.
|
|
304
|
-
* @param {object} ctx - `{ index, collide, sections, contentTypes,
|
|
305
|
-
* foreign, type, errors, src, file }`.
|
|
314
|
+
* @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
|
|
315
|
+
* packages, foreign, type, errors, src, file }`.
|
|
306
316
|
* `packages` is every package an address may name, without which the leading
|
|
307
317
|
* package segment of a canonical address reads as an unknown type; `foreign`
|
|
308
|
-
* is the cross-package manifest index
|
|
318
|
+
* is the cross-package manifest index; `assets` is the address space an embed
|
|
319
|
+
* resolves against. `src` is the page's display
|
|
309
320
|
* path and `file` the source file a diagnostic should name — absent, `src`
|
|
310
321
|
* stands in.
|
|
311
|
-
* @returns {string} The body with wikilinks rewritten.
|
|
322
|
+
* @returns {string} The body with embeds and wikilinks rewritten.
|
|
312
323
|
*/
|
|
313
324
|
export function resolveWebWikilinks(body, ctx) {
|
|
314
325
|
// How many times each authored link has been seen, so two identical links
|
|
@@ -317,15 +328,13 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
317
328
|
// `file:line:column:` diagnostic rather than a note-wide one.
|
|
318
329
|
const seen = new Map();
|
|
319
330
|
/**
|
|
320
|
-
* Records a finding
|
|
331
|
+
* Records a finding against one authored literal.
|
|
321
332
|
*
|
|
322
|
-
* @param {string} all - The
|
|
323
|
-
* @param {object} finding -
|
|
324
|
-
*
|
|
325
|
-
* @param {string} text - What the link renders as.
|
|
326
|
-
* @returns {string} The marked span.
|
|
333
|
+
* @param {string} all - The literal, exactly as authored.
|
|
334
|
+
* @param {object} finding - The finding's own fields.
|
|
335
|
+
* @returns {void}
|
|
327
336
|
*/
|
|
328
|
-
const
|
|
337
|
+
const record = (all, finding) => {
|
|
329
338
|
const occurrence = (seen.get(all) ?? 0) + 1;
|
|
330
339
|
seen.set(all, occurrence);
|
|
331
340
|
ctx.errors.push({
|
|
@@ -337,12 +346,37 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
337
346
|
occurrence,
|
|
338
347
|
...finding,
|
|
339
348
|
});
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Records a finding, and returns the marked-up link it renders as.
|
|
353
|
+
*
|
|
354
|
+
* @param {string} all - The authored link, brackets and all.
|
|
355
|
+
* @param {object} finding - `{ target, reason }`, plus any extras the class
|
|
356
|
+
* carries.
|
|
357
|
+
* @param {string} text - What the link renders as.
|
|
358
|
+
* @returns {string} The marked span.
|
|
359
|
+
*/
|
|
360
|
+
const report = (all, finding, text) => {
|
|
361
|
+
record(all, finding);
|
|
340
362
|
return unresolvedLink(text, finding.target);
|
|
341
363
|
};
|
|
342
364
|
|
|
365
|
+
// An embed names a file, so it resolves before anything looks for a link.
|
|
366
|
+
// Its findings are recorded rather than rendered: the rewrite already
|
|
367
|
+
// leaves an embed it cannot resolve exactly as authored, which is the
|
|
368
|
+
// visible degradation an unresolved link gets from `unresolvedLink`.
|
|
369
|
+
const embedded = resolveEmbeds(body, { index: ctx.assets });
|
|
370
|
+
for (const u of embedded.unresolved) {
|
|
371
|
+
record(u.link, { target: u.target, reason: u.reason, ...(u.type ? { type: u.type } : {}) });
|
|
372
|
+
}
|
|
373
|
+
for (const problem of embedded.problems) {
|
|
374
|
+
record(problem.link, { target: problem.link, message: problem.message });
|
|
375
|
+
}
|
|
376
|
+
|
|
343
377
|
// Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
|
|
344
378
|
// inline span is source text, not a link.
|
|
345
|
-
return replaceOutsideCode(
|
|
379
|
+
return replaceOutsideCode(embedded.markdown, WIKILINK, (all, rawInner) => {
|
|
346
380
|
const parsed = parseWikilink(rawInner);
|
|
347
381
|
const { target, anchor, display } = parsed;
|
|
348
382
|
// An empty label is not a label: `[[x|]]` addresses the target and
|
|
@@ -57,8 +57,15 @@
|
|
|
57
57
|
* unclosed `[[` is a typo, and the alternative is letting it swallow arbitrary
|
|
58
58
|
* prose in search of a closer. Erring towards "not a link" leaves the author's
|
|
59
59
|
* text as written, which is the safe direction for a rewriter.
|
|
60
|
+
*
|
|
61
|
+
* **A leading `!` makes it an embed, not a link**, and the lookbehind is what
|
|
62
|
+
* keeps every reader from seeing one as the other. Stated here rather than in
|
|
63
|
+
* each reader for the reason the rest of this module exists: the checker, the
|
|
64
|
+
* pack compilers and the web resolver all match on this pattern, and three
|
|
65
|
+
* copies of the exclusion is three places for it to be forgotten. An embed's
|
|
66
|
+
* own grammar is {@link module:engine/content-embeds}.
|
|
60
67
|
*/
|
|
61
|
-
export const WIKILINK =
|
|
68
|
+
export const WIKILINK = /(?<!!)\[\[([^\]\n]+)\]\]/g;
|
|
62
69
|
|
|
63
70
|
/**
|
|
64
71
|
* The parts of a wikilink's interior.
|
|
@@ -171,7 +178,9 @@ export function unlabelledLinkMessage(target) {
|
|
|
171
178
|
*
|
|
172
179
|
* - `unlabelled` — no `|`, so the link addresses nothing.
|
|
173
180
|
* - `not-an-address` — labelled, but the target does not parse as an address.
|
|
181
|
+
* - `not-lowercase` — a package, system or type segment carries a capital.
|
|
174
182
|
* - `unknown-type` — definitely qualified, but names no type this build knows.
|
|
183
|
+
* - `not-an-asset` — a real address, embedded where only a file can be drawn.
|
|
175
184
|
* - `unresolved` — parses as an address, and nothing publishes it.
|
|
176
185
|
* - `ambiguous` — more than one package publishes the short address.
|
|
177
186
|
* - `unknown-anchor` — the address resolved, the `#section` it names did not.
|
|
@@ -184,6 +193,7 @@ export const LINK_FINDING_REASONS = Object.freeze(
|
|
|
184
193
|
"not-an-address",
|
|
185
194
|
"not-lowercase",
|
|
186
195
|
"unknown-type",
|
|
196
|
+
"not-an-asset",
|
|
187
197
|
"unresolved",
|
|
188
198
|
"ambiguous",
|
|
189
199
|
"unknown-anchor",
|
|
@@ -262,11 +272,13 @@ export function ambiguousAddressMessage(target, packages = []) {
|
|
|
262
272
|
* @param {Iterable<string>} [finding.packages] - For `ambiguous`, the
|
|
263
273
|
* claimants.
|
|
264
274
|
* @param {string} [finding.anchor] - For `unknown-anchor`, the section named.
|
|
275
|
+
* @param {string} [finding.type] - For `not-an-asset`, the type the address
|
|
276
|
+
* named.
|
|
265
277
|
* @returns {string} The message.
|
|
266
278
|
* @throws {Error} On a reason outside the closed set — a resolver inventing one
|
|
267
279
|
* would otherwise report a link with no explanation at all.
|
|
268
280
|
*/
|
|
269
|
-
export function linkFindingMessage({ reason, target, packages, anchor }) {
|
|
281
|
+
export function linkFindingMessage({ reason, target, packages, anchor, type }) {
|
|
270
282
|
switch (reason) {
|
|
271
283
|
case "unlabelled":
|
|
272
284
|
return unlabelledLinkMessage(target);
|
|
@@ -285,6 +297,12 @@ export function linkFindingMessage({ reason, target, packages, anchor }) {
|
|
|
285
297
|
);
|
|
286
298
|
case "unknown-type":
|
|
287
299
|
return `address [[${target}]] names no known content type`;
|
|
300
|
+
case "not-an-asset":
|
|
301
|
+
return (
|
|
302
|
+
`![[${target}]] embeds a \`${type ?? "note"}\`, and an embed draws a ` +
|
|
303
|
+
`file — only an \`icon\`, an \`image\` or an \`audio\` address can be ` +
|
|
304
|
+
`embedded. Write [[${target}|Text]] to link to it instead`
|
|
305
|
+
);
|
|
288
306
|
case "ambiguous":
|
|
289
307
|
return ambiguousAddressMessage(target, packages ?? []);
|
|
290
308
|
case "unknown-anchor":
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -86,6 +86,7 @@ import crypto from "crypto";
|
|
|
86
86
|
|
|
87
87
|
import { compendiumUuid, ITEM_PACK, packForType, pageUuid, PACK_BY_TYPE } from "./ids.mjs";
|
|
88
88
|
import { readCanonicalKey } from "./content-address.mjs";
|
|
89
|
+
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
89
90
|
import { isSystemSegment, NO_SYSTEM } from "./systems.mjs";
|
|
90
91
|
import { systemOf } from "./document-subtypes.mjs";
|
|
91
92
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
@@ -354,11 +355,15 @@ export function anchorPageId(noteId, anchorSlug) {
|
|
|
354
355
|
* vendored manifests of packages this build links into but does not publish.
|
|
355
356
|
* @param {string} [contentPackage] - This build's *content* package, which an
|
|
356
357
|
* authored address may name explicitly. Defaults to `packageId`.
|
|
358
|
+
* @param {object} [opts] - Options.
|
|
359
|
+
* @param {Map<string, object>} [opts.assets] - The files this package ships, by
|
|
360
|
+
* canonical address. They resolve no link — an asset is not a document — and
|
|
361
|
+
* answer only the art fields, which name a file and never a document.
|
|
357
362
|
* @returns {{byShortcode: Map<string, object>, types: Set<string>}} `types` is
|
|
358
363
|
* every type the tree actually contains, so a qualifier naming no real type
|
|
359
364
|
* can be told apart from a missing target.
|
|
360
365
|
*/
|
|
361
|
-
export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
|
|
366
|
+
export function buildWikilinkIndex(docs, packageId, foreign, contentPackage, { assets } = {}) {
|
|
362
367
|
if (!packageId) {
|
|
363
368
|
throw new Error(
|
|
364
369
|
"buildWikilinkIndex: packageId is required — it is the first " +
|
|
@@ -368,7 +373,11 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
|
|
|
368
373
|
}
|
|
369
374
|
|
|
370
375
|
const byShortcode = new Map();
|
|
371
|
-
|
|
376
|
+
// The **asset** types join unconditionally, whether or not this tree holds
|
|
377
|
+
// a file of each: they are a closed vocabulary rather than a census of what
|
|
378
|
+
// was found, and an art field hands the resolver `icon-<shortcode>` whose
|
|
379
|
+
// type has to parse before anything can be looked up.
|
|
380
|
+
const types = new Set(ASSET_TYPE_NAMES);
|
|
372
381
|
|
|
373
382
|
// Each note's address is computed once, here, and every reference to it is
|
|
374
383
|
// that stored value. Nothing downstream assembles a UUID from parts, so a
|
|
@@ -439,8 +448,12 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
|
|
|
439
448
|
types,
|
|
440
449
|
uuidByDoc,
|
|
441
450
|
packageId,
|
|
451
|
+
/** The content package this build publishes, which an art address defaults to. */
|
|
452
|
+
contentPackage: contentPackage ?? packageId,
|
|
442
453
|
packages,
|
|
443
454
|
foreign: foreignByKey,
|
|
455
|
+
/** The files this package ships, by canonical address. */
|
|
456
|
+
assets: assets ?? new Map(),
|
|
444
457
|
};
|
|
445
458
|
}
|
|
446
459
|
|
package/hm3/actors.mjs
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
* @module
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import { resolveName
|
|
53
|
-
import { buildFromFields,
|
|
52
|
+
import { resolveName } from "../engine/helpers.mjs";
|
|
53
|
+
import { buildFromFields, retiredTopLevelKey, STRING } from "../engine/field-spec.mjs";
|
|
54
54
|
import { SystemActorCompiler } from "../engine/actor-compiler.mjs";
|
|
55
55
|
import { renderSection } from "../engine/anchored-sections.mjs";
|
|
56
56
|
import { documentSubtype } from "../engine/document-subtypes.mjs";
|
|
@@ -99,33 +99,6 @@ const ACTOR_FIELDS = Object.freeze([
|
|
|
99
99
|
},
|
|
100
100
|
]);
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
* The actor's bio image — the fourth row of the specification's `being` table,
|
|
104
|
-
* and the last one that was still read by hand.
|
|
105
|
-
*
|
|
106
|
-
* Declared for the reason {@link ACTOR_FIELDS} is, and fixed for the reason
|
|
107
|
-
* `data.species` was: read with `blockProperty(fm, block, "portrait")` it saw
|
|
108
|
-
* the block and the note's top level and nothing else, so the `data.portrait`
|
|
109
|
-
* the specification names never reached the document and the `?? defaultImg`
|
|
110
|
-
* beside it dressed the miss up as "this note names no art".
|
|
111
|
-
*
|
|
112
|
-
* It is **not** in `ACTOR_FIELDS`, because `buildFromFields` has no seam for
|
|
113
|
-
* the subtype default that has to follow it — the `?? defaultImg` is the whole
|
|
114
|
-
* of what distinguishes an unnamed portrait from a deliberately blank one,
|
|
115
|
-
* and it needs a subtype the coercion is not handed.
|
|
116
|
-
*
|
|
117
|
-
* @type {import("../engine/field-spec.mjs").FieldSpec}
|
|
118
|
-
*/
|
|
119
|
-
const BIO_IMAGE_FIELD = Object.freeze({
|
|
120
|
-
name: "data.portrait",
|
|
121
|
-
legacyKey: "portrait",
|
|
122
|
-
to: "bioImage",
|
|
123
|
-
shape: "path",
|
|
124
|
-
read: (raw) => resolveImg(raw),
|
|
125
|
-
default: null,
|
|
126
|
-
describe: "Path to the portrait image.",
|
|
127
|
-
});
|
|
128
|
-
|
|
129
102
|
/**
|
|
130
103
|
* The two `data:` facts HM3 declares on a `character` and not on a `creature`.
|
|
131
104
|
*
|
|
@@ -285,7 +258,10 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
285
258
|
absPath: this.currentNote?.absPath,
|
|
286
259
|
})
|
|
287
260
|
);
|
|
288
|
-
|
|
261
|
+
// The being's own default sits above the subtype's: only the note's
|
|
262
|
+
// tags say whether it is a person or a creature, and only this pass
|
|
263
|
+
// reads them.
|
|
264
|
+
const art = this.actorArt(fm, defaultActorImg(subType));
|
|
289
265
|
|
|
290
266
|
const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
|
|
291
267
|
|
|
@@ -326,11 +302,9 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
326
302
|
});
|
|
327
303
|
|
|
328
304
|
const system = {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
// all — see {@link BIO_IMAGE_FIELD}.
|
|
333
|
-
bioImage: readField(BIO_IMAGE_FIELD, fm, reports) ?? defaultImg,
|
|
305
|
+
// `system.bioImage` is **not** written here, for the reason SoHL's
|
|
306
|
+
// `system.portrait` is not: a being's portrait is the lead image of
|
|
307
|
+
// its `{#appearance}` section, which is the markup below.
|
|
334
308
|
description: renderSection(body || "", "appearance"),
|
|
335
309
|
biography: renderSection(body || "", "dossier"),
|
|
336
310
|
...buildFromFields(ACTOR_FIELDS, reports)(fm),
|
|
@@ -356,12 +330,11 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
356
330
|
});
|
|
357
331
|
|
|
358
332
|
const effects = blockProperty(fm, block, "effects");
|
|
359
|
-
const img = resolveImg(blockProperty(fm, block, "img")) ?? defaultImg;
|
|
360
333
|
|
|
361
334
|
return {
|
|
362
335
|
name,
|
|
363
336
|
type: subType,
|
|
364
|
-
img,
|
|
337
|
+
img: art.img,
|
|
365
338
|
_id: id,
|
|
366
339
|
system,
|
|
367
340
|
items,
|
|
@@ -369,7 +342,7 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
369
342
|
name,
|
|
370
343
|
displayName: 0,
|
|
371
344
|
actorLink: false,
|
|
372
|
-
texture: { src:
|
|
345
|
+
texture: { src: art.token },
|
|
373
346
|
width: 1,
|
|
374
347
|
height: 1,
|
|
375
348
|
sight: { enabled: false },
|
package/hm3/infobox.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import { GEAR_UNITS, defineInfobox } from "../engine/infobox.mjs";
|
|
|
33
33
|
import { HM3_ITEM_FIELDS } from "./item-fields.mjs";
|
|
34
34
|
|
|
35
35
|
/** What this system's box is called. @type {string} */
|
|
36
|
-
export const HM3_INFOBOX_TITLE = "
|
|
36
|
+
export const HM3_INFOBOX_TITLE = "HârnMaster 3";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* HM3's presentation overlay: what one of this system's fields is called where
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "21.
|
|
3
|
+
"version": "21.2.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",
|
|
@@ -102,6 +102,8 @@
|
|
|
102
102
|
"CHANGELOG.md",
|
|
103
103
|
"CONTENT.md",
|
|
104
104
|
"README.md",
|
|
105
|
+
"assets",
|
|
106
|
+
"packagebuild-metadata.jsonl",
|
|
105
107
|
"bin",
|
|
106
108
|
"bundle.mjs",
|
|
107
109
|
"ci",
|
|
@@ -160,8 +162,9 @@
|
|
|
160
162
|
"scripts": {
|
|
161
163
|
"test": "vitest run",
|
|
162
164
|
"test:watch": "vitest",
|
|
163
|
-
"prepack": "
|
|
165
|
+
"prepack": "run-s build:types build:asset-index",
|
|
164
166
|
"build:types": "tsc -p tsconfig.dts.json",
|
|
167
|
+
"build:asset-index": "node ci/emit-asset-index.mjs",
|
|
165
168
|
"format": "prettier --write .",
|
|
166
169
|
"format:check": "prettier --check .",
|
|
167
170
|
"lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:content-format",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{"address":{"canonical":"packagebuild-none-image-afflictionbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","source":""},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
|
|
2
|
+
{"address":{"canonical":"packagebuild-none-image-armorclothingbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","source":""},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
|
|
3
|
+
{"address":{"canonical":"packagebuild-none-image-attributebnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","source":""},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
|
|
4
|
+
{"address":{"canonical":"packagebuild-none-image-containerbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","source":""},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
|
|
5
|
+
{"address":{"canonical":"packagebuild-none-image-devdocsbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","source":""},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
|
|
6
|
+
{"address":{"canonical":"packagebuild-none-image-miscgearbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","source":""},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
|
|
7
|
+
{"address":{"canonical":"packagebuild-none-image-mysticalabilitybnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","source":""},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
|
|
8
|
+
{"address":{"canonical":"packagebuild-none-image-projectilebnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","source":""},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
|
|
9
|
+
{"address":{"canonical":"packagebuild-none-image-rulesbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","source":""},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
|
|
10
|
+
{"address":{"canonical":"packagebuild-none-image-skillbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","source":""},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
|
|
11
|
+
{"address":{"canonical":"packagebuild-none-image-traumabnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","source":""},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
|
|
12
|
+
{"address":{"canonical":"packagebuild-none-image-userguidebnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","source":""},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
|
|
13
|
+
{"address":{"canonical":"packagebuild-none-image-weapongearbnr"},"asset":{"attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","source":""},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
|