@heroiclands/package-build 13.0.0 → 15.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 +361 -0
- package/CONTENT.md +193 -43
- package/MIGRATING.md +196 -5
- package/content-config.mjs +99 -61
- package/docs/content-format.md +28 -8
- package/engine/content-address.mjs +11 -19
- package/engine/field-reference.mjs +29 -0
- package/engine/field-spec.mjs +25 -0
- package/engine/frontmatter-lint.mjs +99 -32
- package/engine/helpers.mjs +38 -12
- package/engine/homepage.mjs +9 -4
- package/engine/item-registry.mjs +4 -1
- package/engine/macros.mjs +3 -1
- package/engine/manifest-emit.mjs +4 -7
- package/engine/note-vocabulary.mjs +34 -76
- package/engine/site-build.mjs +20 -14
- package/engine/system-block.mjs +29 -3
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -3
- package/sohl/item-fields.mjs +6 -0
- package/sohl/items.mjs +4 -1
- package/types/content-config.d.mts +25 -35
- package/types/engine/content-address.d.mts +7 -14
- package/types/engine/field-spec.d.mts +53 -0
- package/types/engine/helpers.d.mts +34 -12
- package/types/engine/homepage.d.mts +8 -4
- package/types/engine/manifest-emit.d.mts +3 -9
- package/types/engine/note-vocabulary.d.mts +17 -47
- package/types/engine/site-build.d.mts +11 -6
package/engine/helpers.mjs
CHANGED
|
@@ -234,25 +234,51 @@ export function makeFilename(name, id) {
|
|
|
234
234
|
* asset roots — `icons/...` and `images/...` — are served from the package
|
|
235
235
|
* directory, so they are rewritten to `<assetRoot>/<path>` — `systems/sohl/assets`
|
|
236
236
|
* for this repository, `modules/<id>/assets` for a module (#1508). Any other
|
|
237
|
-
* path (already package-rooted, an absolute URL) is returned unchanged
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* `
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
237
|
+
* path (already package-rooted, an absolute URL) is returned unchanged.
|
|
238
|
+
*
|
|
239
|
+
* **Two empties, and they mean opposite things (#218).** `null` — or an absent
|
|
240
|
+
* key, which reaches here as `undefined` — means _unset_: the note names no art
|
|
241
|
+
* and the caller's default applies. `""` means _blank on purpose_: the note
|
|
242
|
+
* names no art **and wants none**, so no default may replace it. Both come back
|
|
243
|
+
* distinguishable, `null` and `""` respectively, and neither is invented from
|
|
244
|
+
* the other.
|
|
245
|
+
*
|
|
246
|
+
* This used to open `if (!raw) return ""`, which made the two one case: every
|
|
247
|
+
* caller then applied its default with `||`, so a deliberate blank was
|
|
248
|
+
* unspellable and an unset key and an empty string compiled identically. That
|
|
249
|
+
* is the convention the project already rejects for an optional "not specified"
|
|
250
|
+
* DataModel string, where `nullable, initial: null` keeps "unset" a single
|
|
251
|
+
* honest value rather than two.
|
|
252
|
+
*
|
|
253
|
+
* **`title` does not follow this rule**, and must not be made to. On a
|
|
254
|
+
* `type: affiliation` note `title` is *also* a declared item field whose default
|
|
255
|
+
* is `""` (`sohl/item-fields.mjs`), resolved from the very same shared top-level
|
|
256
|
+
* key the site emitter reads as the page title — so `title: null` stringifies
|
|
257
|
+
* into the compiled document as the literal `"null"`. One key, two destinations
|
|
258
|
+
* that disagree about what empty means; see #218.
|
|
259
|
+
*
|
|
260
|
+
* This is translation only: the default for an unset path is domain-specific
|
|
261
|
+
* (actors default differently from items, and gear differently again), so each
|
|
262
|
+
* compiler owns its own default and applies it to the result with **nullish**
|
|
263
|
+
* coalescing — `resolveImg(fm.img) ?? <default>`. Not `||`: that would collapse
|
|
264
|
+
* a deliberate blank back into the default and undo the distinction. For items
|
|
265
|
+
* that default is the art paired with the type's builder, reached through
|
|
266
|
+
* `itemArt()`, which runs the path back through this function so a registry
|
|
267
|
+
* entry and a note's `img:` are spelled the same way (#7).
|
|
247
268
|
*
|
|
248
269
|
* @param {string | null | undefined} raw - content-relative path from frontmatter.
|
|
249
270
|
* @param {{assetRoot: string}} [config] - The resolved build configuration.
|
|
250
271
|
* Defaults to this repository's.
|
|
251
|
-
* @returns {string} the Foundry-relative path
|
|
272
|
+
* @returns {string | null} the Foundry-relative path; `""` for a deliberate
|
|
273
|
+
* blank, and `null` when the note names no art at all.
|
|
252
274
|
*/
|
|
253
275
|
export function resolveImg(raw, config = loadPackConfig()) {
|
|
254
|
-
|
|
276
|
+
// Unset — the caller's default applies. An absent key arrives as
|
|
277
|
+
// `undefined`, an authored one as `null`; they say the same thing.
|
|
278
|
+
if (raw == null) return null;
|
|
255
279
|
const s = String(raw);
|
|
280
|
+
// Blank on purpose — the caller's default must not apply.
|
|
281
|
+
if (s === "") return "";
|
|
256
282
|
if (s.startsWith("icons/") || s.startsWith("images/")) {
|
|
257
283
|
return `${config.assetRoot}/${s}`;
|
|
258
284
|
}
|
package/engine/homepage.mjs
CHANGED
|
@@ -371,6 +371,13 @@ export function homepageTitle(fm, config) {
|
|
|
371
371
|
* for one; it decides nothing while `url` is present, but a page carrying only
|
|
372
372
|
* `url` would report a slug Hugo had inferred from the filename.
|
|
373
373
|
*
|
|
374
|
+
* **Site-root relative, and so carrying no package base** (#217), exactly as
|
|
375
|
+
* `pageFrontmatter` states a content page's: Hugo resolves a `url`
|
|
376
|
+
* against `baseURL`, whose path is already where the package is served, so a
|
|
377
|
+
* stated base was written twice and published the landing at
|
|
378
|
+
* `/<package>/<package>/homepage-root/`. Where the package is served is what
|
|
379
|
+
* every *href* is composed from and it reaches this page's address not at all.
|
|
380
|
+
*
|
|
374
381
|
* An authored `aliases` is dropped for the same reason it is on every other
|
|
375
382
|
* page: Hugo reads it as URL redirects, so passing it through would publish a
|
|
376
383
|
* redirect stub at each one. The field is retired (#180) and refused before a
|
|
@@ -380,14 +387,12 @@ export function homepageTitle(fm, config) {
|
|
|
380
387
|
* @param {object} options - Options.
|
|
381
388
|
* @param {string} options.contentPackage - The package this build publishes.
|
|
382
389
|
* @param {string} options.title - The resolved title.
|
|
383
|
-
* @param {string} options.base - Where the package is served, with both
|
|
384
|
-
* slashes — `/<package>/`.
|
|
385
390
|
* @returns {object} The frontmatter to write.
|
|
386
391
|
* @throws {Error} When the note declares no shortcode, and so has no address.
|
|
387
392
|
*/
|
|
388
|
-
export function homepageFrontmatter(fm, { contentPackage, title
|
|
393
|
+
export function homepageFrontmatter(fm, { contentPackage, title }) {
|
|
389
394
|
const slug = addressSlug(fm);
|
|
390
|
-
const data = { ...fm, package: contentPackage, title, slug, url:
|
|
395
|
+
const data = { ...fm, package: contentPackage, title, slug, url: `/${slug}/` };
|
|
391
396
|
delete data.aliases;
|
|
392
397
|
return data;
|
|
393
398
|
}
|
package/engine/item-registry.mjs
CHANGED
|
@@ -188,5 +188,8 @@ export function itemArt(type, system) {
|
|
|
188
188
|
`note an \`img:\` of its own.`,
|
|
189
189
|
);
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
// `art` is non-empty by the guard above, so the translation never returns
|
|
192
|
+
// the `null` a note's unset `img:` would (#218); the coalesce states that
|
|
193
|
+
// rather than leaving the declared `string` return a half-truth.
|
|
194
|
+
return resolveImg(art) ?? "";
|
|
192
195
|
}
|
package/engine/macros.mjs
CHANGED
|
@@ -261,7 +261,9 @@ export function buildMacroEntry(fm, { command, folder = null, stats = defaultSta
|
|
|
261
261
|
// into chat instead of running.
|
|
262
262
|
type: resolveMacroType(fm, name),
|
|
263
263
|
author: null,
|
|
264
|
-
|
|
264
|
+
// Nullish, not `||` (#218): a macro note that names no art gets the
|
|
265
|
+
// shared default, one that writes `img: ""` ships blank on purpose.
|
|
266
|
+
img: resolveImg(fm.img) ?? DEFAULT_MACRO_IMG,
|
|
265
267
|
scope: resolveMacroScope(fm, name),
|
|
266
268
|
command,
|
|
267
269
|
folder,
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -210,8 +210,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
210
210
|
* exist.
|
|
211
211
|
*
|
|
212
212
|
* @param {string} contentBase - Absolute path to the content tree.
|
|
213
|
-
* @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter
|
|
214
|
-
* scheme }`.
|
|
213
|
+
* @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter }`.
|
|
215
214
|
* @returns {{entries: Array<object>, notes: number,
|
|
216
215
|
* skipped: Array<{file: string, reason: string}>}}
|
|
217
216
|
*/
|
|
@@ -249,7 +248,7 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
249
248
|
|
|
250
249
|
let address;
|
|
251
250
|
try {
|
|
252
|
-
address = packageAddress(fm
|
|
251
|
+
address = packageAddress(fm);
|
|
253
252
|
} catch (err) {
|
|
254
253
|
skipped.push({ file: rel, reason: err.message });
|
|
255
254
|
continue;
|
|
@@ -261,7 +260,7 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
/**
|
|
264
|
-
* The identities
|
|
263
|
+
* The identities an emission runs against, from configuration.
|
|
265
264
|
*
|
|
266
265
|
* Resolved in one place and passed down, rather than read at each use, so the
|
|
267
266
|
* pass itself is a pure function of its context and a test can drive it without
|
|
@@ -269,15 +268,13 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
269
268
|
*
|
|
270
269
|
* @param {object} [config] - A resolved configuration; loaded when omitted.
|
|
271
270
|
* @returns {{contentPackage: string, foundryPackageId: string, packRouter: object,
|
|
272
|
-
*
|
|
273
|
-
* skipDirectories: readonly string[]}}
|
|
271
|
+
* web: boolean, skipDirectories: readonly string[]}}
|
|
274
272
|
*/
|
|
275
273
|
export function manifestContext(config = loadPackConfig()) {
|
|
276
274
|
return {
|
|
277
275
|
contentPackage: config.contentPackage,
|
|
278
276
|
foundryPackageId: config.foundryPackage,
|
|
279
277
|
packRouter: routerFor(config),
|
|
280
|
-
scheme: config.publish.address,
|
|
281
278
|
web: publishesContentPages(config),
|
|
282
279
|
// The walk's own configuration, threaded through rather than left to
|
|
283
280
|
// its default, so a caller that passes a config drives every read.
|
|
@@ -663,11 +663,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
663
663
|
/* ----- core documents ------------------------------------------- */
|
|
664
664
|
|
|
665
665
|
doc: Object.freeze({
|
|
666
|
-
// `userguide`, not `user-guide`: a
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
666
|
+
// `userguide`, not `user-guide`: a subType is held to the address
|
|
667
|
+
// charset, and a segment carries no hyphen (#206). The old spelling was
|
|
668
|
+
// accepted transitionally for one release so the consumer trees could
|
|
669
|
+
// sweep; they have, so it is refused by the charset check now, with no
|
|
670
|
+
// retirement-specific code left over (#210).
|
|
671
671
|
subTypes: Object.freeze(["rules", "userguide", "reference"]),
|
|
672
672
|
data: Object.freeze([]),
|
|
673
673
|
}),
|
|
@@ -732,83 +732,32 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
732
732
|
});
|
|
733
733
|
|
|
734
734
|
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
* Keyed by type, because a retirement is a statement about *that type's*
|
|
738
|
-
* vocabulary: `user-guide` on a `doc` is the old spelling of `userguide`, while
|
|
739
|
-
* the same string on any other type is nothing but a charset violation, and
|
|
740
|
-
* saying "did you mean userguide" there would be a guess dressed as a fact.
|
|
741
|
-
*
|
|
742
|
-
* **Recorded here rather than left in `subTypes`** so the declared list stays
|
|
743
|
-
* the list of values a note *should* write. A retired value is accepted, not
|
|
744
|
-
* declared — the difference is exactly what makes the finding possible.
|
|
745
|
-
*
|
|
746
|
-
* **Deliberately not the shape of a type rename** ({@link
|
|
747
|
-
* import("./ids.mjs").RETIRED_TYPES}), which is an error: a retired type routes
|
|
748
|
-
* a note to the wrong pack, whereas a retired subType still compiles to the
|
|
749
|
-
* correct page. The sweep is the consumer's, and the ordering is the reverse of
|
|
750
|
-
* the usual — the acceptance ships *first*, because declaring only the new
|
|
751
|
-
* spelling while 43 `sohl` notes still author the old one would invalidate all
|
|
752
|
-
* 43 with a release they had no chance to sweep ahead of. A later change
|
|
753
|
-
* removes this map, and the old spelling then falls through to the ordinary
|
|
754
|
-
* undeclared-value error with no code left to remove.
|
|
755
|
-
*
|
|
756
|
-
* @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
|
|
757
|
-
*/
|
|
758
|
-
export const RETIRED_SUBTYPES = Object.freeze({
|
|
759
|
-
doc: Object.freeze({ "user-guide": "userguide" }),
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* What to write in place of a retired subType value, if it is one.
|
|
735
|
+
* What a note carrying a subType outside the address charset is told.
|
|
764
736
|
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (!forType || !Object.hasOwn(forType, value)) return undefined;
|
|
775
|
-
return forType[value];
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* What a note carrying a retired subType is told.
|
|
737
|
+
* **Why the charset holds for a subType, which reaches no address.** #206 said
|
|
738
|
+
* "the hyphen separates the segments of an address", and that was true of a
|
|
739
|
+
* subType when it shipped: `sectionOf` returned a `doc`'s subType, so the value
|
|
740
|
+
* was a URL path segment. #204 retired sections and it is not one now. The rule
|
|
741
|
+
* stays, on its own footing: a subType is a vocabulary term the whole toolchain
|
|
742
|
+
* keys on, and it is one closed set away from being an address segment again —
|
|
743
|
+
* so the reason to spell it in the address charset is that a charset holding
|
|
744
|
+
* for a type, a shortcode and a `contentPackage` but not for a subType is a
|
|
745
|
+
* rule nobody can state in a sentence.
|
|
780
746
|
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* @param {string} type - The note's `type`.
|
|
785
|
-
* @param {string} value - The retired spelling the note carries.
|
|
786
|
-
* @param {string} replacement - What to write instead.
|
|
787
|
-
* @returns {string} The message.
|
|
788
|
-
*/
|
|
789
|
-
export function retiredSubTypeMessage(type, value, replacement) {
|
|
790
|
-
return (
|
|
791
|
-
`\`subType\` "${value}" is a retired spelling of "${replacement}" on a ` +
|
|
792
|
-
`${type}; write "${replacement}". A subType is an address segment, and ` +
|
|
793
|
-
`a segment is ${ADDRESS_SEGMENT_PATTERN.source} — the hyphen separates ` +
|
|
794
|
-
`segments, so it can never occur inside one. The old spelling is still ` +
|
|
795
|
-
`accepted, and will stop being accepted once the trees have swept`
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* What a note carrying a subType outside the address charset is told.
|
|
747
|
+
* Contrast {@link typeCharsetMessage}, which keeps the address reasoning
|
|
748
|
+
* because a type genuinely is the first segment of every address.
|
|
801
749
|
*
|
|
802
750
|
* @param {string} value - The authored `subType`.
|
|
803
751
|
* @returns {string} The message.
|
|
804
752
|
*/
|
|
805
753
|
export function subTypeCharsetMessage(value) {
|
|
806
754
|
return (
|
|
807
|
-
`\`subType\` "${value}" is not
|
|
755
|
+
`\`subType\` "${value}" is not a well-formed subType — a subType is ` +
|
|
808
756
|
`letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}), the same ` +
|
|
809
|
-
`charset a shortcode
|
|
810
|
-
`
|
|
811
|
-
`
|
|
757
|
+
`charset a type, a shortcode and a contentPackage are held to. It is a ` +
|
|
758
|
+
`vocabulary term the whole toolchain keys on, and one closed set away ` +
|
|
759
|
+
`from being an address segment again, so a charset that held for every ` +
|
|
760
|
+
`term but this one would be a rule nobody could state in a sentence`
|
|
812
761
|
);
|
|
813
762
|
}
|
|
814
763
|
|
|
@@ -836,6 +785,11 @@ export function typeCharsetMessage(type) {
|
|
|
836
785
|
* a note's bad value is one author's mistake and belongs in a report, while a
|
|
837
786
|
* bad *declaration* would tell every author to write something unaddressable.
|
|
838
787
|
*
|
|
788
|
+
* The message states the reason **per key**, as {@link typeCharsetMessage} and
|
|
789
|
+
* {@link subTypeCharsetMessage} do: a type is an address segment, and a subType
|
|
790
|
+
* has not been one since #204 retired sections, so a single claim covering both
|
|
791
|
+
* would be half wrong (#210).
|
|
792
|
+
*
|
|
839
793
|
* @param {Readonly<Record<string, TypeVocabulary>>} vocabulary - The registry.
|
|
840
794
|
* @param {string} [where] - What declares it, for the message.
|
|
841
795
|
* @throws {Error} Naming every offending type and subType at once, rather than
|
|
@@ -854,9 +808,13 @@ export function assertVocabularyCharset(vocabulary, where = "the note vocabulary
|
|
|
854
808
|
if (!bad.length) return;
|
|
855
809
|
throw new Error(
|
|
856
810
|
`${where} declares ${bad.join(", ")}, which ${bad.length === 1 ? "is" : "are"} ` +
|
|
857
|
-
`not ${ADDRESS_SEGMENT_PATTERN.source}. A type
|
|
858
|
-
`
|
|
859
|
-
`
|
|
811
|
+
`not ${ADDRESS_SEGMENT_PATTERN.source}. A type is an address segment, ` +
|
|
812
|
+
`and the hyphen separates segments rather than occurring inside one. ` +
|
|
813
|
+
`A subType reaches no address since #204 retired sections, and is ` +
|
|
814
|
+
`held to the same charset anyway: it is a vocabulary term the whole ` +
|
|
815
|
+
`toolchain keys on, one closed set away from being a segment again, ` +
|
|
816
|
+
`and a charset holding for every term but that one would be a rule ` +
|
|
817
|
+
`nobody could state in a sentence.`,
|
|
860
818
|
);
|
|
861
819
|
}
|
|
862
820
|
|
package/engine/site-build.mjs
CHANGED
|
@@ -343,18 +343,13 @@ export function collectHomepages(contentBase, ctx) {
|
|
|
343
343
|
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
344
344
|
* @param {object} config - The resolved configuration, for the package name and
|
|
345
345
|
* the default title.
|
|
346
|
-
* @param {object} [options] - Options.
|
|
347
|
-
* @param {string} [options.base] - Where the package is served; defaults to the
|
|
348
|
-
* configured `site.base`, and to `/<contentPackage>/` below that.
|
|
349
346
|
* @returns {number} How many pages were written.
|
|
350
347
|
*/
|
|
351
|
-
export function writeHomepages(outRoot, pages, config
|
|
352
|
-
const at = base || config.site?.base || `/${config.contentPackage}/`;
|
|
348
|
+
export function writeHomepages(outRoot, pages, config) {
|
|
353
349
|
for (const page of pages) {
|
|
354
350
|
const data = homepageFrontmatter(page.fm, {
|
|
355
351
|
contentPackage: config.contentPackage,
|
|
356
352
|
title: homepageTitle(page.fm, config),
|
|
357
|
-
base: at,
|
|
358
353
|
});
|
|
359
354
|
const dest = path.join(outRoot, homepageDestination(page.fm));
|
|
360
355
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -544,6 +539,16 @@ export function sectionFrontmatter(meta) {
|
|
|
544
539
|
* package-wide address the link manifest records — the same address, one
|
|
545
540
|
* segment too deep. So the address is stated and the mount does not reach it.
|
|
546
541
|
*
|
|
542
|
+
* **It is stated relative to the site root, and so carries no package base**
|
|
543
|
+
* (#217). Hugo resolves a `url` against `baseURL`, whose path is already where
|
|
544
|
+
* the package is served — a consumer's Hugo site *is* its package — so writing
|
|
545
|
+
* `page.url`, which carries the base for every href this build renders, wrote
|
|
546
|
+
* that base a second time and published every content page a segment too deep
|
|
547
|
+
* (`/sohl/sohl/doc-rulesintro/`). The two are separate quantities: the page
|
|
548
|
+
* states `/<slug>/`, and everything that points *at* the page — the address
|
|
549
|
+
* index a wikilink resolves through, and the link manifest — composes
|
|
550
|
+
* `<base><slug>/`.
|
|
551
|
+
*
|
|
547
552
|
* A content page carries the package the build **derived** (#65). No note
|
|
548
553
|
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
549
554
|
* would publish a page that does not say which package it belongs to. The
|
|
@@ -571,13 +576,14 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
571
576
|
// Spread after the note's own frontmatter. Guarded because
|
|
572
577
|
// `package: undefined` is not a value YAML can carry.
|
|
573
578
|
...(page.pkg ? { package: page.pkg } : {}),
|
|
574
|
-
// The address, stated
|
|
575
|
-
// the
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
+
// The address, stated — site-root relative, because Hugo prefixes
|
|
580
|
+
// the site's own base to it (#217). `slug` is written beside it
|
|
581
|
+
// because it is the last segment of that address and Hugo's own key
|
|
582
|
+
// for one; it decides nothing while `url` is present, but a page
|
|
583
|
+
// that carried only `url` would report a slug Hugo had inferred
|
|
584
|
+
// from the filename.
|
|
579
585
|
slug,
|
|
580
|
-
url:
|
|
586
|
+
url: `/${slug}/`,
|
|
581
587
|
title: fm.title ?? name,
|
|
582
588
|
kbfolder: page.folder,
|
|
583
589
|
};
|
|
@@ -993,7 +999,7 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
993
999
|
tableErrors: [],
|
|
994
1000
|
wikiErrors: [],
|
|
995
1001
|
stats: {
|
|
996
|
-
homepages: writeHomepages(homeRoot, homepages, resolved
|
|
1002
|
+
homepages: writeHomepages(homeRoot, homepages, resolved),
|
|
997
1003
|
landings: 0,
|
|
998
1004
|
out: homeRoot,
|
|
999
1005
|
},
|
|
@@ -1078,7 +1084,7 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
1078
1084
|
|
|
1079
1085
|
// Last, and outside the mount: the package's front page is not part of the
|
|
1080
1086
|
// content tree it introduces.
|
|
1081
|
-
const homepagesWritten = writeHomepages(homeRoot, homepages, resolved
|
|
1087
|
+
const homepagesWritten = writeHomepages(homeRoot, homepages, resolved);
|
|
1082
1088
|
|
|
1083
1089
|
return {
|
|
1084
1090
|
gates,
|
package/engine/system-block.mjs
CHANGED
|
@@ -52,6 +52,28 @@
|
|
|
52
52
|
* may be a dotted path (`data.portrait`) rather than a sibling key;
|
|
53
53
|
* 4. the field's own default.
|
|
54
54
|
*
|
|
55
|
+
* ## A name that collides across the two vocabularies skips step 3
|
|
56
|
+
*
|
|
57
|
+
* A field's `name` is both its identity and the shared property it draws from,
|
|
58
|
+
* and those coincide only while the two vocabularies agree about what the
|
|
59
|
+
* spelling means. They do not always. An `affiliation` item's `system.title` is
|
|
60
|
+
* the style of address an office carries — "Ajaw", "Warden"; a note's top-level
|
|
61
|
+
* `title` is the note's own heading, which the site emitter publishes. Two
|
|
62
|
+
* unrelated quantities, one spelling, and step 3 fed the first from the second.
|
|
63
|
+
*
|
|
64
|
+
* It was not a harmless coincidence either, because step 3 answers **without**
|
|
65
|
+
* applying `field.default` — only step 2 does — so an authored `title: null`
|
|
66
|
+
* reached the field's coercion unguarded and shipped as the literal string
|
|
67
|
+
* `"null"` in fifteen documents (#218).
|
|
68
|
+
*
|
|
69
|
+
* So a field may declare `topLevelMeans`: what the top-level key of that name
|
|
70
|
+
* means *instead*. Declaring it removes step 3 for that field, leaving the two
|
|
71
|
+
* positions that describe the document rather than the note. It is deliberately
|
|
72
|
+
* a per-field opt-out rather than a change to the order — step 3 is right
|
|
73
|
+
* wherever the two levels state the same quantity, which is nearly everywhere —
|
|
74
|
+
* and its value is the reason rather than a bare flag, so the collision is
|
|
75
|
+
* legible where the field is declared. See {@link module:engine/field-spec.FieldSpec}.
|
|
76
|
+
*
|
|
55
77
|
* `sohlField()` — read `fm.sohl[key]`, fall back to `fm[key]` — is the
|
|
56
78
|
* degenerate case where source and destination happen to share a name. It stops
|
|
57
79
|
* being the general rule; {@link blockField} is what remains of it.
|
|
@@ -347,9 +369,13 @@ export function resolveFieldValue(field, fm, { block = "sohl" } = {}) {
|
|
|
347
369
|
if (nested !== undefined) return { value: nested, from: "block" };
|
|
348
370
|
}
|
|
349
371
|
|
|
350
|
-
// 3. The shared property this field declares as its source
|
|
351
|
-
|
|
352
|
-
|
|
372
|
+
// 3. The shared property this field declares as its source — unless the
|
|
373
|
+
// field declares that the top-level key of that name means something
|
|
374
|
+
// else, in which case there is no shared position to read (#218).
|
|
375
|
+
if (field.topLevelMeans === undefined) {
|
|
376
|
+
const shared = getFrontmatter(fm, field.name, undefined);
|
|
377
|
+
if (shared !== undefined) return { value: shared, from: "shared" };
|
|
378
|
+
}
|
|
353
379
|
|
|
354
380
|
// 4. The field's own default.
|
|
355
381
|
return { value: field.default, from: "default" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "15.0.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
package/sohl/actors.mjs
CHANGED
|
@@ -763,7 +763,9 @@ export class Actors extends BasePackCompiler {
|
|
|
763
763
|
// Required nullable number: a priority, or `null` for a being that
|
|
764
764
|
// is not an archetype (#126 / archetype contract #604).
|
|
765
765
|
archetype: systemArchetype(fm, ctx),
|
|
766
|
-
|
|
766
|
+
// Nullish, not `||` (#218): a note that names no portrait gets the
|
|
767
|
+
// subtype's default, one that writes `""` ships blank on purpose.
|
|
768
|
+
portrait: resolveImg(blockProperty(fm, SYSTEM, "portrait")) ?? defaultImg,
|
|
767
769
|
appearance: renderSection(body || "", "appearance"),
|
|
768
770
|
dossier: renderSection(body || "", "dossier"),
|
|
769
771
|
};
|
|
@@ -815,7 +817,8 @@ export class Actors extends BasePackCompiler {
|
|
|
815
817
|
return {
|
|
816
818
|
name,
|
|
817
819
|
type: subType,
|
|
818
|
-
|
|
820
|
+
// Nullish, not `||` — see the portrait above (#218).
|
|
821
|
+
img: resolveImg(blockProperty(fm, SYSTEM, "img")) ?? defaultImg,
|
|
819
822
|
_id: id,
|
|
820
823
|
system,
|
|
821
824
|
items,
|
|
@@ -823,7 +826,7 @@ export class Actors extends BasePackCompiler {
|
|
|
823
826
|
name,
|
|
824
827
|
displayName: 0,
|
|
825
828
|
actorLink: false,
|
|
826
|
-
texture: { src: resolveImg(blockProperty(fm, SYSTEM, "img"))
|
|
829
|
+
texture: { src: resolveImg(blockProperty(fm, SYSTEM, "img")) ?? defaultImg },
|
|
827
830
|
width: 1,
|
|
828
831
|
height: 1,
|
|
829
832
|
sight: { enabled: false },
|
package/sohl/item-fields.mjs
CHANGED
|
@@ -265,6 +265,12 @@ export const ITEM_FIELDS = Object.freeze({
|
|
|
265
265
|
to: "title",
|
|
266
266
|
...STRING,
|
|
267
267
|
default: "",
|
|
268
|
+
topLevelMeans:
|
|
269
|
+
"the note's own title — the heading its page is published under, " +
|
|
270
|
+
"which has nothing to do with the style of address an office " +
|
|
271
|
+
"carries. Author this on the membership instead: `sohl.system.title` " +
|
|
272
|
+
"on the item, or the `system.title` of the entry in a being's " +
|
|
273
|
+
"`sohl.items`.",
|
|
268
274
|
describe: "The style of address the office carries.",
|
|
269
275
|
},
|
|
270
276
|
{
|
package/sohl/items.mjs
CHANGED
|
@@ -255,7 +255,10 @@ export class Items extends BasePackCompiler {
|
|
|
255
255
|
// both registries are keyed by content type — while the document's
|
|
256
256
|
// own subtype comes from the system's map (#79).
|
|
257
257
|
type: subType,
|
|
258
|
-
|
|
258
|
+
// Nullish, not `||` (#218): `resolveImg` returns `null` for a note
|
|
259
|
+
// that names no art and `""` for one that wants none, and only the
|
|
260
|
+
// first may be replaced by the type's default.
|
|
261
|
+
img: resolveImg(blockProperty(fm, SYSTEM, "img")) ?? itemArt(type, SYSTEM),
|
|
259
262
|
_id: id,
|
|
260
263
|
system,
|
|
261
264
|
effects: Array.isArray(effects) ? [...effects] : [],
|
|
@@ -56,36 +56,31 @@ export namespace DEFAULT_PATHS {
|
|
|
56
56
|
*/
|
|
57
57
|
export const PACK_DOCUMENT_TYPES: readonly ["Actor", "Adventure", "Item", "JournalEntry", "Macro", "Scene"];
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* A
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* A retired *value* is refused the way a retired *field* is (see
|
|
81
|
-
* `engine/retired-fields.mjs`): left merely unrecognized it would be reported
|
|
82
|
-
* as a bad value, which names something to correct and leaves the author to
|
|
83
|
-
* work out for themselves that the mechanism is gone. The message says the rule
|
|
84
|
-
* is retired, what lands a section instead, and what to do with the key.
|
|
59
|
+
* Address-scheme keys a configuration may no longer declare.
|
|
60
|
+
*
|
|
61
|
+
* A retired key has exactly two possible fates, and only one of them is honest
|
|
62
|
+
* — the same reasoning `engine/retired-fields.mjs` applies to a retired
|
|
63
|
+
* frontmatter field. Left honoured, it keeps doing whatever it did, which is
|
|
64
|
+
* why it was retired. Left *ignored*, it reads to its author as though it still
|
|
65
|
+
* works: the configuration says one thing and the build does another, and
|
|
66
|
+
* nothing says so. This module has no third option, because it has no warning
|
|
67
|
+
* channel — every finding goes through `fail()`, which throws. So a retired
|
|
68
|
+
* key is **refused**, at the line it was written on, with a message that says
|
|
69
|
+
* the mechanism is gone rather than naming a value to correct.
|
|
70
|
+
*
|
|
71
|
+
* **What `landing` did (#204).** It named which note addressed a whole section
|
|
72
|
+
* rather than a page within one — a *landing page*, which therefore had no slug
|
|
73
|
+
* of its own. #203 retired the second of its two rules and #204 retired the
|
|
74
|
+
* concept both rules chose between: a section is a Hugo content directory that
|
|
75
|
+
* the note format does not carry, a page's address names no directory, and so
|
|
76
|
+
* no note lands anything. The key outlived its mechanism by one release only
|
|
77
|
+
* because both publishing consumers still declared the then-true
|
|
78
|
+
* `landing: readme`, and neither breaking them over a correct statement nor
|
|
79
|
+
* accepting the key in silence was acceptable. Neither declares it now.
|
|
85
80
|
*
|
|
86
81
|
* @type {Readonly<Record<string, string>>}
|
|
87
82
|
*/
|
|
88
|
-
export const
|
|
83
|
+
export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
|
|
89
84
|
/**
|
|
90
85
|
* A repository's address scheme, with the defaults an unconfigured one gets.
|
|
91
86
|
*
|
|
@@ -96,11 +91,12 @@ export const RETIRED_LANDING_RULES: Readonly<Record<string, string>>;
|
|
|
96
91
|
* knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
|
|
97
92
|
* resolve time, so it is never recorded here (#1465).
|
|
98
93
|
*
|
|
99
|
-
* `landing
|
|
94
|
+
* It is the whole scheme: `landing`, the key that named which note addressed a
|
|
95
|
+
* whole section, is retired with the sections themselves — see
|
|
96
|
+
* {@link RETIRED_ADDRESS_KEYS}.
|
|
100
97
|
*/
|
|
101
98
|
export const DEFAULT_ADDRESS_SCHEME: Readonly<{
|
|
102
99
|
prefix: "";
|
|
103
|
-
landing: "readme";
|
|
104
100
|
}>;
|
|
105
101
|
/**
|
|
106
102
|
* How much of a package reaches the web.
|
|
@@ -515,12 +511,6 @@ export type AddressSchemeInput = {
|
|
|
515
511
|
* Where the content tree mounts inside the package.
|
|
516
512
|
*/
|
|
517
513
|
prefix?: string | undefined;
|
|
518
|
-
/**
|
|
519
|
-
* Which note addressed a whole section. Inert
|
|
520
|
-
* since #204 retired sections from the note format — see
|
|
521
|
-
* {@link LANDING_RULES}.
|
|
522
|
-
*/
|
|
523
|
-
landing?: string | undefined;
|
|
524
514
|
};
|
|
525
515
|
/**
|
|
526
516
|
* One entry of a consumer's `itemBuilders` registry.
|
|
@@ -55,27 +55,20 @@ export function contentAddress(fm: object): string;
|
|
|
55
55
|
* `/<package>/api/` for generated API docs, neither of which contains a hyphen
|
|
56
56
|
* or names a type.
|
|
57
57
|
*
|
|
58
|
+
* **It takes no address scheme.** It took one until #215, to validate the
|
|
59
|
+
* `landing` rule it then discarded; with that key retired, `prefix` was the
|
|
60
|
+
* only thing left in the scheme and the paragraph above is the reason it never
|
|
61
|
+
* applied. A parameter read by nothing is the defect this deletion is about.
|
|
62
|
+
*
|
|
58
63
|
* @param {object} fm - Parsed frontmatter.
|
|
59
|
-
* @param {object} [options] - Options.
|
|
60
|
-
* @param {{prefix?: string, landing?: string}} [options.scheme] - The
|
|
61
|
-
* repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
|
|
62
|
-
* `landing` is validated against {@link LANDING_RULES} and selects nothing —
|
|
63
|
-
* it is accepted so a configuration declaring the still-true `landing: readme`
|
|
64
|
-
* keeps loading, and is removed once none does.
|
|
65
64
|
* @returns {string} The package-relative address, with a trailing slash and no
|
|
66
65
|
* leading one.
|
|
67
66
|
* @throws {Error} When the note has no type or no shortcode to be addressed by.
|
|
68
67
|
* Such a note is not published, and inventing an address for one would put a
|
|
69
68
|
* dead entry in the manifest.
|
|
70
69
|
*/
|
|
71
|
-
export function packageAddress(fm: object
|
|
72
|
-
|
|
73
|
-
prefix?: string;
|
|
74
|
-
landing?: string;
|
|
75
|
-
} | undefined;
|
|
76
|
-
}): string;
|
|
70
|
+
export function packageAddress(fm: object): string;
|
|
71
|
+
export { DEFAULT_ADDRESS_SCHEME };
|
|
77
72
|
/** The knowledgebase's mount within this package's site (#1470). */
|
|
78
73
|
export const KB_PREFIX: "kb/";
|
|
79
74
|
import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
|
|
80
|
-
import { LANDING_RULES } from "../content-config.mjs";
|
|
81
|
-
export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
|