@heroiclands/package-build 21.1.0 → 22.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 +245 -0
- package/CONTENT.md +107 -3
- 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/bin/package-build.mjs +6 -5
- package/ci/emit-asset-index.mjs +32 -0
- package/content-config.mjs +30 -1
- package/docs/api.md +103 -27
- package/docs/commands.md +13 -11
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +523 -111
- 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/dependency-bump.mjs +14 -16
- 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.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 -8
- package/engine/pdf-render.mjs +49 -12
- 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/manifest.mjs +67 -13
- package/package.json +5 -2
- package/packagebuild-metadata.jsonl +13 -0
- package/sohl/actors.mjs +12 -86
- 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/dependency-bump.d.mts +2 -2
- 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 -1
- 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/manifest.d.mts +42 -1
- package/types/sohl/actors.d.mts +0 -1
|
@@ -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/manifest.mjs
CHANGED
|
@@ -94,6 +94,49 @@ export function normalizeRepoUrl(repository) {
|
|
|
94
94
|
return url;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Where every HeroicLands package's homepage is served from.
|
|
99
|
+
*
|
|
100
|
+
* Stated once here and read by {@link packageHomepage}, so the origin a
|
|
101
|
+
* manifest advertises and the origin the configuration documents cannot come
|
|
102
|
+
* to disagree.
|
|
103
|
+
*
|
|
104
|
+
* @type {string}
|
|
105
|
+
*/
|
|
106
|
+
export const HOMEPAGE_ORIGIN = "https://www.heroiclands.org";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A package's homepage — the page `url` sends a reader to.
|
|
110
|
+
*
|
|
111
|
+
* Derived from the content package rather than declared, because the address is
|
|
112
|
+
* already fixed: every package publishes an authored homepage at
|
|
113
|
+
* `<origin>/<contentPackage>/`, and the shared site-deploy workflow publishes
|
|
114
|
+
* it there. A declared copy would be a second spelling of a settled fact, free
|
|
115
|
+
* to drift — and the one that drifts is this one, because nothing fetches `url`
|
|
116
|
+
* the way Foundry fetches `manifest`, so a wrong value is never reported.
|
|
117
|
+
*
|
|
118
|
+
* The site's own `baseURL` says the same thing, and is deliberately not the
|
|
119
|
+
* source: it sits at `site/hugo.toml` in most repositories and `kb/hugo.toml`
|
|
120
|
+
* in the system, so reading it would mean the packaging half knowing where each
|
|
121
|
+
* repository keeps its site configuration.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} contentPackage - The package's content-package name.
|
|
124
|
+
* @returns {string} The homepage URL, with its trailing slash.
|
|
125
|
+
* @throws {TypeError} When no name is given. Interpolating a missing one yields
|
|
126
|
+
* `<origin>/undefined/`, a URL that resolves and is wrong — and nothing
|
|
127
|
+
* fetches `url`, so it would be advertised for as long as nobody clicked it.
|
|
128
|
+
*/
|
|
129
|
+
export function packageHomepage(contentPackage) {
|
|
130
|
+
const name = String(contentPackage ?? "").trim();
|
|
131
|
+
if (!name) {
|
|
132
|
+
throw new TypeError(
|
|
133
|
+
"the configuration declares no `contentPackage`, so the manifest has " +
|
|
134
|
+
"no homepage to advertise.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return `${HOMEPAGE_ORIGIN}/${name}/`;
|
|
138
|
+
}
|
|
139
|
+
|
|
97
140
|
/**
|
|
98
141
|
* The addresses a Foundry manifest advertises.
|
|
99
142
|
*
|
|
@@ -103,15 +146,23 @@ export function normalizeRepoUrl(repository) {
|
|
|
103
146
|
* install at that release forever. `download` points at this exact version,
|
|
104
147
|
* because that is the archive this manifest describes.
|
|
105
148
|
*
|
|
149
|
+
* **`url` is the homepage, not the repository.** It is the Project Homepage
|
|
150
|
+
* link a reader follows from the package listing *before* installing anything,
|
|
151
|
+
* so it answers "what is this?" — which a page written for that question does
|
|
152
|
+
* and a source tree does not. `bugs`, `manifest` and `download` address release
|
|
153
|
+
* artefacts and stay on the repository that holds them.
|
|
154
|
+
*
|
|
106
155
|
* @param {object} opts
|
|
107
156
|
* @param {string} opts.repoUrl - Normalised repository URL.
|
|
157
|
+
* @param {string} opts.homeUrl - The package's homepage, from
|
|
158
|
+
* {@link packageHomepage}.
|
|
108
159
|
* @param {string} opts.version - The version being built.
|
|
109
160
|
* @param {"system"|"module"} opts.artifact - Which artifact is shipped.
|
|
110
161
|
* @returns {{url: string, bugs: string, manifest: string, download: string}}
|
|
111
162
|
*/
|
|
112
|
-
export function releaseUrls({ repoUrl, version, artifact }) {
|
|
163
|
+
export function releaseUrls({ repoUrl, homeUrl, version, artifact }) {
|
|
113
164
|
return {
|
|
114
|
-
url:
|
|
165
|
+
url: homeUrl,
|
|
115
166
|
bugs: `${repoUrl}/issues`,
|
|
116
167
|
manifest: `${repoUrl}/releases/latest/download/${artifact}.json`,
|
|
117
168
|
download: `${repoUrl}/releases/download/v${version}/${artifact}.zip`,
|
|
@@ -434,7 +485,12 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
|
434
485
|
id: config.foundryPackage,
|
|
435
486
|
version: packageJson.version,
|
|
436
487
|
packs: manifestPacks(config),
|
|
437
|
-
...releaseUrls({
|
|
488
|
+
...releaseUrls({
|
|
489
|
+
repoUrl,
|
|
490
|
+
homeUrl: packageHomepage(config.contentPackage),
|
|
491
|
+
version: packageJson.version,
|
|
492
|
+
artifact,
|
|
493
|
+
}),
|
|
438
494
|
};
|
|
439
495
|
if (config.compatibility) derived.compatibility = config.compatibility;
|
|
440
496
|
|
|
@@ -482,16 +538,14 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
|
482
538
|
// A flag rather than a top-level key because Foundry's manifest schema is
|
|
483
539
|
// closed and `flags` is its declared extension point; an unknown key at the
|
|
484
540
|
// top level is dropped by some readers and rejected by others.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
};
|
|
494
|
-
}
|
|
541
|
+
merged.flags = {
|
|
542
|
+
...(declared.flags ?? {}),
|
|
543
|
+
metadataUrl: metadataUrl({
|
|
544
|
+
repoUrl,
|
|
545
|
+
version: packageJson.version,
|
|
546
|
+
contentPackage: config.contentPackage,
|
|
547
|
+
}),
|
|
548
|
+
};
|
|
495
549
|
|
|
496
550
|
if (flags && Object.keys(flags).length) {
|
|
497
551
|
merged.flags = { ...(merged.flags ?? declared.flags ?? {}) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.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",
|
|
@@ -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"}
|
package/sohl/actors.mjs
CHANGED
|
@@ -46,13 +46,7 @@
|
|
|
46
46
|
* @module
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
-
import {
|
|
50
|
-
sohlField,
|
|
51
|
-
resolveName,
|
|
52
|
-
resolveImg,
|
|
53
|
-
systemTemplatePriority,
|
|
54
|
-
folderField,
|
|
55
|
-
} from "../engine/helpers.mjs";
|
|
49
|
+
import { sohlField, resolveName, systemTemplatePriority, folderField } from "../engine/helpers.mjs";
|
|
56
50
|
import { openingMasteryLevel } from "./skill-base.mjs";
|
|
57
51
|
import { SystemActorCompiler, renderSection } from "../engine/actor-compiler.mjs";
|
|
58
52
|
// Which Foundry Actor subtype a note's `type` compiles into. Looked up in the
|
|
@@ -63,14 +57,6 @@ import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
|
63
57
|
// verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
|
|
64
58
|
// shared top-level forms for this system alone.
|
|
65
59
|
import { blockProperty, mergeSystemData } from "../engine/system-block.mjs";
|
|
66
|
-
import { readField, retiredTopLevelKey } from "../engine/field-spec.mjs";
|
|
67
|
-
// The retirement window's reports, shared with the frontmatter lint so the two
|
|
68
|
-
// cannot say different things about the same key.
|
|
69
|
-
import {
|
|
70
|
-
legacyKeyMessage,
|
|
71
|
-
locateFrontmatterKey,
|
|
72
|
-
retiredTopLevelMessage,
|
|
73
|
-
} from "../engine/retired-fields.mjs";
|
|
74
60
|
|
|
75
61
|
/**
|
|
76
62
|
* The system this pass compiles for — the block its notes write.
|
|
@@ -89,38 +75,6 @@ const DEFAULT_IMG = {
|
|
|
89
75
|
being: "systems/sohl/assets/icons/game-icons/delapouite/person.svg",
|
|
90
76
|
};
|
|
91
77
|
|
|
92
|
-
/**
|
|
93
|
-
* The being's sheet portrait — the one row of the content format's actor
|
|
94
|
-
* mapping table that is authored rather than derived.
|
|
95
|
-
*
|
|
96
|
-
* **Declared, because the position is not a spelling anyone can guess.** This
|
|
97
|
-
* was read with `blockProperty(fm, SYSTEM, "portrait")`, which knows the block
|
|
98
|
-
* and the note's top level and nothing else — so `data.portrait`, the position
|
|
99
|
-
* the specification names and `sohl-thalorna` writes on 646 beings, was
|
|
100
|
-
* invisible, and `?? defaultImg` on the next line turned every miss into the
|
|
101
|
-
* generic person icon rather than into a complaint. Going through
|
|
102
|
-
* {@link module:engine/field-spec.readField} is what makes the mapping table
|
|
103
|
-
* executable here as it already is for HM3's `data.species`.
|
|
104
|
-
*
|
|
105
|
-
* `img` is deliberately **not** declared beside it: the mapping table keeps a
|
|
106
|
-
* note's token art at the top level, so `blockProperty` is the whole of its
|
|
107
|
-
* resolution and there is no `data.img` to reach for.
|
|
108
|
-
*
|
|
109
|
-
* @type {import("../engine/field-spec.mjs").FieldSpec}
|
|
110
|
-
*/
|
|
111
|
-
const PORTRAIT_FIELD = Object.freeze({
|
|
112
|
-
name: "data.portrait",
|
|
113
|
-
legacyKey: "portrait",
|
|
114
|
-
to: "portrait",
|
|
115
|
-
shape: "path",
|
|
116
|
-
// The two empties survive, because the caller's `?? defaultImg` is what
|
|
117
|
-
// tells them apart: `null` and an absent key mean "no art named, default
|
|
118
|
-
// me", `""` means "ship blank on purpose".
|
|
119
|
-
read: (raw) => resolveImg(raw),
|
|
120
|
-
default: null,
|
|
121
|
-
describe: "Path to the portrait image.",
|
|
122
|
-
});
|
|
123
|
-
|
|
124
78
|
/**
|
|
125
79
|
* The default art for an actor subtype.
|
|
126
80
|
*
|
|
@@ -366,43 +320,16 @@ export class Actors extends SystemActorCompiler {
|
|
|
366
320
|
absPath: this.currentNote?.absPath,
|
|
367
321
|
})
|
|
368
322
|
);
|
|
369
|
-
|
|
323
|
+
// The being's own default sits above the subtype's: only the note's
|
|
324
|
+
// tags say whether it is a person or a creature, and only this pass
|
|
325
|
+
// reads them.
|
|
326
|
+
const art = this.actorArt(fm, defaultActorImg(subType));
|
|
370
327
|
|
|
371
328
|
const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
|
|
372
329
|
|
|
373
330
|
const { value: authoredFolder, isAddress } = folderField(fm);
|
|
374
331
|
const folder = this.folderResolver(authoredFolder, { isAddress });
|
|
375
332
|
|
|
376
|
-
// The two retiring positions a declared field may be read from.
|
|
377
|
-
// **Warnings**, on the pattern every retirement in this package
|
|
378
|
-
// follows: the note compiles to the correct document either way, so
|
|
379
|
-
// reddening a tree over one would refuse before the sweep rather than
|
|
380
|
-
// after it. What they buy is a count — without one, nothing says which
|
|
381
|
-
// position a value came from, and a default is indistinguishable from
|
|
382
|
-
// a miss.
|
|
383
|
-
const portraitReports = {
|
|
384
|
-
block: SYSTEM,
|
|
385
|
-
onLegacyKey: (field) =>
|
|
386
|
-
this.noteWarn(
|
|
387
|
-
legacyKeyMessage(SYSTEM, field),
|
|
388
|
-
locateFrontmatterKey(this.currentNote?.absPath, field.legacyKey),
|
|
389
|
-
),
|
|
390
|
-
// Anchored at column 1: the two positions share a spelling here —
|
|
391
|
-
// `sohl.portrait` and `portrait` — so a locator that took the first
|
|
392
|
-
// match would point at the block key while the message named the
|
|
393
|
-
// top-level one.
|
|
394
|
-
onRetiredTopLevel: (field) =>
|
|
395
|
-
this.noteWarn(
|
|
396
|
-
retiredTopLevelMessage(field),
|
|
397
|
-
locateFrontmatterKey(
|
|
398
|
-
this.currentNote?.absPath,
|
|
399
|
-
retiredTopLevelKey(field),
|
|
400
|
-
undefined,
|
|
401
|
-
{ topLevel: true },
|
|
402
|
-
),
|
|
403
|
-
),
|
|
404
|
-
};
|
|
405
|
-
|
|
406
333
|
const system = {
|
|
407
334
|
// The frontmatter shortcode is the actor's stable `(type, shortcode)`
|
|
408
335
|
// key — and, for a being that is an archetype, its archetype
|
|
@@ -414,11 +341,11 @@ export class Actors extends SystemActorCompiler {
|
|
|
414
341
|
// with the schema, since an undeclared `system` key is discarded at
|
|
415
342
|
// construction without a warning.
|
|
416
343
|
templatePriority: systemTemplatePriority(fm, ctx),
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
344
|
+
// `system.portrait` is **not** written here. A picture of the
|
|
345
|
+
// subject is a picture, so a being's portrait is the lead image of
|
|
346
|
+
// its `{#appearance}` section — which is the markup below — and the
|
|
347
|
+
// key that used to declare one is not a key. With no authored
|
|
348
|
+
// source left, the field keeps the schema's own initial.
|
|
422
349
|
appearance: renderSection(body || "", "appearance"),
|
|
423
350
|
dossier: renderSection(body || "", "dossier"),
|
|
424
351
|
};
|
|
@@ -470,8 +397,7 @@ export class Actors extends SystemActorCompiler {
|
|
|
470
397
|
return {
|
|
471
398
|
name,
|
|
472
399
|
type: subType,
|
|
473
|
-
|
|
474
|
-
img: resolveImg(blockProperty(fm, SYSTEM, "img")) ?? defaultImg,
|
|
400
|
+
img: art.img,
|
|
475
401
|
_id: id,
|
|
476
402
|
system,
|
|
477
403
|
items,
|
|
@@ -479,7 +405,7 @@ export class Actors extends SystemActorCompiler {
|
|
|
479
405
|
name,
|
|
480
406
|
displayName: 0,
|
|
481
407
|
actorLink: false,
|
|
482
|
-
texture: { src:
|
|
408
|
+
texture: { src: art.token },
|
|
483
409
|
width: 1,
|
|
484
410
|
height: 1,
|
|
485
411
|
sight: { enabled: false },
|