@heroiclands/package-build 22.22.0 → 22.23.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 +32 -0
- package/bin/content-commands.mjs +51 -14
- package/ci/content-format-reference.mjs +1 -1
- package/content-config.mjs +80 -2
- package/docs/api.md +85 -72
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +37 -1
- package/docs/authoring/links-and-markup.md +112 -31
- package/docs/commands.md +12 -10
- package/docs/configuration.md +90 -13
- package/docs/content-format.md +1 -1
- package/docs/guides/book.md +5 -4
- package/docs/reference/format-details.md +323 -60
- package/docs/reference/note-types.md +9 -9
- package/engine/anchors.mjs +16 -10
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +54 -2
- package/engine/bundles.mjs +13 -1
- package/engine/content-blocks.mjs +4 -4
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +10 -2
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +36 -10
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +73 -1
- package/engine/heading-attributes.mjs +31 -18
- package/engine/helpers.mjs +97 -30
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox.mjs +47 -8
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +264 -39
- package/engine/lore-events.mjs +197 -0
- package/engine/markdown-expressions.mjs +257 -7
- package/engine/note-dates.mjs +305 -12
- package/engine/note-vocabulary.mjs +11 -5
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +357 -34
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +160 -36
- package/engine/scenes.mjs +16 -4
- package/engine/site-build.mjs +127 -13
- package/engine/site-index.mjs +72 -1
- package/engine/site-preview.mjs +2 -0
- package/engine/system-block.mjs +44 -16
- package/hm3/actors.mjs +2 -2
- package/manifest.mjs +30 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/item-fields.mjs +4 -3
- package/types/content-config.d.mts +48 -1
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +0 -17
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +7 -0
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/heading-attributes.d.mts +13 -8
- package/types/engine/helpers.d.mts +23 -7
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +31 -9
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +23 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +12 -0
- package/types/engine/site-build.d.mts +14 -3
- package/types/engine/site-index.d.mts +54 -0
- package/types/engine/system-block.d.mts +26 -2
- package/types/hm3/actors.d.mts +18 -0
- package/types/manifest.d.mts +19 -23
- package/engine/content-captions.mjs +0 -230
- package/types/engine/content-captions.d.mts +0 -58
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
* local pack's output — the resolution logic needs no knowledge of where an
|
|
39
39
|
* item came from.
|
|
40
40
|
*
|
|
41
|
+
* `assetArchive: true` is the sibling flag, for a dependency whose bytes are
|
|
42
|
+
* wanted and whose items are not: an asset-replacement source such as
|
|
43
|
+
* `thalornaaltart` ships no Item packs at all. It unpacks the same release
|
|
44
|
+
* archive without extracting anything from it, so a relationship declaring
|
|
45
|
+
* both flags on one entry is fetched once rather than twice.
|
|
46
|
+
*
|
|
41
47
|
* **The network is never touched by a compile.** Fetching is its own command
|
|
42
48
|
* (`package-build deps fetch`), and a compile whose cache is cold fails saying
|
|
43
49
|
* so. A build that silently downloads is not reproducible, fails strangely
|
|
@@ -73,17 +79,22 @@ import {
|
|
|
73
79
|
const STAMP = ".complete";
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
|
-
* Every declared relationship
|
|
82
|
+
* Every declared relationship carrying a given flag, in declaration order.
|
|
83
|
+
*
|
|
84
|
+
* Shared by {@link itemCatalogRelationships} and
|
|
85
|
+
* {@link assetArchiveRelationships}: both read the same shape out of the same
|
|
86
|
+
* walk, filtered on a different key.
|
|
77
87
|
*
|
|
78
88
|
* @param {object} config - The resolved build configuration.
|
|
89
|
+
* @param {"itemCatalog"|"assetArchive"} flag - Which boolean to filter on.
|
|
79
90
|
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
80
|
-
* The opted-in relationships
|
|
91
|
+
* The opted-in relationships.
|
|
81
92
|
*/
|
|
82
|
-
|
|
93
|
+
function flaggedRelationships(config, flag) {
|
|
83
94
|
const out = [];
|
|
84
95
|
for (const [kind, entries] of Object.entries(config.relationships ?? {})) {
|
|
85
96
|
for (const rel of entries ?? []) {
|
|
86
|
-
if (rel
|
|
97
|
+
if (rel[flag]) {
|
|
87
98
|
out.push({
|
|
88
99
|
id: rel.id,
|
|
89
100
|
manifest: rel.manifest,
|
|
@@ -96,6 +107,29 @@ export function itemCatalogRelationships(config) {
|
|
|
96
107
|
return out;
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Every declared relationship that opted into supplying an item catalogue.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} config - The resolved build configuration.
|
|
114
|
+
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
115
|
+
* The opted-in relationships, in declaration order.
|
|
116
|
+
*/
|
|
117
|
+
export function itemCatalogRelationships(config) {
|
|
118
|
+
return flaggedRelationships(config, "itemCatalog");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Every declared relationship that opted into supplying its release archive
|
|
123
|
+
* for asset bytes alone, with no item catalogue built from it.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} config - The resolved build configuration.
|
|
126
|
+
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
127
|
+
* The opted-in relationships, in declaration order.
|
|
128
|
+
*/
|
|
129
|
+
export function assetArchiveRelationships(config) {
|
|
130
|
+
return flaggedRelationships(config, "assetArchive");
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
/**
|
|
100
134
|
* The cache directory for one dependency at one version.
|
|
101
135
|
*
|
|
@@ -366,15 +400,21 @@ export function pinnedManifestUrl(url, verified) {
|
|
|
366
400
|
}
|
|
367
401
|
|
|
368
402
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* @
|
|
403
|
+
* Resolve a dependency's manifest, pinned to `compatibility.verified` the way
|
|
404
|
+
* both fetch paths require.
|
|
405
|
+
*
|
|
406
|
+
* Shared by {@link fetchCatalog} and {@link fetchAssetArchive}: both read the
|
|
407
|
+
* same manifest before deciding what to unpack from it.
|
|
408
|
+
*
|
|
409
|
+
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
410
|
+
* declared relationship.
|
|
411
|
+
* @param {string} subject - What floats when no version is verified —
|
|
412
|
+
* "catalogue" or "archive" — named in the warning a reader would otherwise
|
|
413
|
+
* get no word for.
|
|
414
|
+
* @returns {Promise<{manifest: object, version: string}>} The manifest and
|
|
415
|
+
* its resolved version.
|
|
376
416
|
*/
|
|
377
|
-
|
|
417
|
+
async function resolveManifest(rel, subject) {
|
|
378
418
|
const { url, pinned } = pinnedManifestUrl(rel.manifest, rel.verified);
|
|
379
419
|
const manifest = await fetchManifest(url);
|
|
380
420
|
const version = manifest.version;
|
|
@@ -391,10 +431,24 @@ export async function fetchCatalog(config, rel) {
|
|
|
391
431
|
}
|
|
392
432
|
if (!rel.verified) {
|
|
393
433
|
log.warn(
|
|
394
|
-
`${rel.id}: no \`compatibility.verified\`, so its
|
|
434
|
+
`${rel.id}: no \`compatibility.verified\`, so its ${subject} floats ` +
|
|
395
435
|
`with whatever ${url} currently serves`,
|
|
396
436
|
);
|
|
397
437
|
}
|
|
438
|
+
return { manifest, version };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Fetch one dependency and extract its Item packs.
|
|
443
|
+
*
|
|
444
|
+
* Idempotent: a complete cache for the resolved version is left alone.
|
|
445
|
+
*
|
|
446
|
+
* @param {object} config - The resolved build configuration.
|
|
447
|
+
* @param {{id: string, manifest: string}} rel - The declared relationship.
|
|
448
|
+
* @returns {Promise<string>} The dependency's cache directory.
|
|
449
|
+
*/
|
|
450
|
+
export async function fetchCatalog(config, rel) {
|
|
451
|
+
const { manifest, version } = await resolveManifest(rel, "catalogue");
|
|
398
452
|
const dir = catalogDir(config, rel.id, version);
|
|
399
453
|
if (isComplete(dir)) {
|
|
400
454
|
log.info(`${rel.id}@${version}: already cached`);
|
|
@@ -420,6 +474,61 @@ export async function fetchCatalog(config, rel) {
|
|
|
420
474
|
return dir;
|
|
421
475
|
}
|
|
422
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Whether an asset-archive-only cache is present and complete.
|
|
479
|
+
*
|
|
480
|
+
* No item catalogue is built on this path, so completeness never asks for
|
|
481
|
+
* `item-packs.json` — an asset-only dependency, such as `thalornaaltart`,
|
|
482
|
+
* ships none.
|
|
483
|
+
*
|
|
484
|
+
* @param {string} dir - The dependency's cache directory.
|
|
485
|
+
* @returns {boolean} True when the archive was fetched to completion.
|
|
486
|
+
*/
|
|
487
|
+
const isArchiveComplete = (dir) => fs.existsSync(path.join(dir, STAMP));
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Fetch one dependency's release archive for its asset bytes alone, building
|
|
491
|
+
* no item catalogue from it.
|
|
492
|
+
*
|
|
493
|
+
* Idempotent: a complete cache for the resolved version is left alone. Where a
|
|
494
|
+
* relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
|
|
495
|
+
* unpacked this same archive into this same cache directory, and
|
|
496
|
+
* {@link fetchAllCatalogs} does not call this function for it — the two never
|
|
497
|
+
* download the same bytes twice.
|
|
498
|
+
*
|
|
499
|
+
* @param {object} config - The resolved build configuration.
|
|
500
|
+
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
501
|
+
* declared relationship.
|
|
502
|
+
* @returns {Promise<string>} The dependency's cache directory.
|
|
503
|
+
*/
|
|
504
|
+
export async function fetchAssetArchive(config, rel) {
|
|
505
|
+
const { manifest, version } = await resolveManifest(rel, "archive");
|
|
506
|
+
const dir = catalogDir(config, rel.id, version);
|
|
507
|
+
if (isArchiveComplete(dir)) {
|
|
508
|
+
log.info(`${rel.id}@${version}: already cached`);
|
|
509
|
+
return dir;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const download = manifest.download;
|
|
513
|
+
if (!download) {
|
|
514
|
+
throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
518
|
+
// half-tree is worse than no tree.
|
|
519
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
520
|
+
const raw = path.join(dir, "package");
|
|
521
|
+
fs.mkdirSync(raw, { recursive: true });
|
|
522
|
+
|
|
523
|
+
log.info(`${rel.id}@${version}: downloading ${download}`);
|
|
524
|
+
await downloadAndUnzip(download, raw);
|
|
525
|
+
|
|
526
|
+
cacheSchemaArtifact(raw, dir);
|
|
527
|
+
// Last, so a fetch that died partway is never mistaken for a complete one.
|
|
528
|
+
fs.writeFileSync(path.join(dir, STAMP), `${version}\n`);
|
|
529
|
+
return dir;
|
|
530
|
+
}
|
|
531
|
+
|
|
423
532
|
/**
|
|
424
533
|
* Locate a declared pack inside an unpacked archive.
|
|
425
534
|
*
|
|
@@ -700,17 +809,33 @@ export async function fetchAllMetadata(config) {
|
|
|
700
809
|
/**
|
|
701
810
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
702
811
|
*
|
|
812
|
+
* A relationship declaring both `itemCatalog: true` and `assetArchive: true`
|
|
813
|
+
* is fetched once: {@link fetchCatalog} already unpacks the archive on its
|
|
814
|
+
* way to extracting the item catalogue, so this skips the archive-only fetch
|
|
815
|
+
* for any id {@link fetchCatalog} already handled.
|
|
816
|
+
*
|
|
703
817
|
* @param {object} config - The resolved build configuration.
|
|
704
818
|
* @returns {Promise<number>} How many dependencies were fetched.
|
|
705
819
|
*/
|
|
706
820
|
export async function fetchAllCatalogs(config) {
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
821
|
+
const itemRels = itemCatalogRelationships(config);
|
|
822
|
+
const archiveRels = assetArchiveRelationships(config);
|
|
823
|
+
if (!itemRels.length && !archiveRels.length) {
|
|
824
|
+
log.info(
|
|
825
|
+
"No relationship declares `itemCatalog: true` or `assetArchive: true`; " +
|
|
826
|
+
"nothing to fetch.",
|
|
827
|
+
);
|
|
710
828
|
return 0;
|
|
711
829
|
}
|
|
712
|
-
for (const rel of
|
|
713
|
-
|
|
830
|
+
for (const rel of itemRels) await fetchCatalog(config, rel);
|
|
831
|
+
const alreadyFetched = new Set(itemRels.map((rel) => rel.id));
|
|
832
|
+
let archiveFetched = 0;
|
|
833
|
+
for (const rel of archiveRels) {
|
|
834
|
+
if (alreadyFetched.has(rel.id)) continue;
|
|
835
|
+
await fetchAssetArchive(config, rel);
|
|
836
|
+
archiveFetched++;
|
|
837
|
+
}
|
|
838
|
+
return itemRels.length + archiveFetched;
|
|
714
839
|
}
|
|
715
840
|
|
|
716
841
|
/**
|
|
@@ -64,10 +64,12 @@ import {
|
|
|
64
64
|
} from "./note-frontmatter.mjs";
|
|
65
65
|
import { AddressEntries } from "./address-values.mjs";
|
|
66
66
|
import { addressPositions } from "./note-addresses.mjs";
|
|
67
|
-
import { authoredFields, readsLegacyKey } from "./field-spec.mjs";
|
|
67
|
+
import { authoredFields, readsLegacyKey, readsRetiredTopLevel } from "./field-spec.mjs";
|
|
68
68
|
import {
|
|
69
69
|
legacyKeyOf,
|
|
70
|
+
resolveDataProperty,
|
|
70
71
|
resolveFieldValue,
|
|
72
|
+
retiredTopLevelKey,
|
|
71
73
|
systemBlock,
|
|
72
74
|
SYSTEM_BLOCK_KEYS,
|
|
73
75
|
unknownBlockKeys,
|
|
@@ -101,6 +103,7 @@ import {
|
|
|
101
103
|
legacyKeyMessage,
|
|
102
104
|
readAliasedField,
|
|
103
105
|
retiredAliasMessage,
|
|
106
|
+
retiredTopLevelMessage,
|
|
104
107
|
sectionRetiredMessage,
|
|
105
108
|
traitsRetiredMessage,
|
|
106
109
|
} from "./retired-fields.mjs";
|
|
@@ -137,6 +140,19 @@ export const UNIVERSAL_KEYS = Object.freeze(
|
|
|
137
140
|
new Set(["packFolder", "pack", "archetype", "templatePriority", "kbcat"]),
|
|
138
141
|
);
|
|
139
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The shared `data:` keys declared for every note type that still have a
|
|
145
|
+
* retiring top-level spelling — `pack` and `packFolder`, the two
|
|
146
|
+
* {@link module:engine/note-vocabulary.SHARED_DATA_FIELDS} entries
|
|
147
|
+
* {@link module:engine/system-block.resolveDataProperty} reads off a tree's
|
|
148
|
+
* bare top level. Unlike a type's own declarations, which the retiring set
|
|
149
|
+
* below derives from `schemas`, these apply to every type at once, so they
|
|
150
|
+
* are named once here rather than attached to one type's vocabulary.
|
|
151
|
+
*
|
|
152
|
+
* @type {readonly string[]}
|
|
153
|
+
*/
|
|
154
|
+
const UNIVERSAL_RETIRING_DATA_KEYS = Object.freeze(["pack", "packFolder"]);
|
|
155
|
+
|
|
140
156
|
/**
|
|
141
157
|
* The system blocks a build checks, and what each accepts beyond the shared
|
|
142
158
|
* vocabulary.
|
|
@@ -511,6 +527,9 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
|
|
|
511
527
|
message:
|
|
512
528
|
type === "affiliation" && key === "commonSkills" ?
|
|
513
529
|
"`data.commonSkills` is not an affiliation field; write skill Addresses at `sohl.system.commonSkills`"
|
|
530
|
+
: type === "lore" && key === "event" ?
|
|
531
|
+
"`data.event` is retired — write `data.events`, a list of occurrences, " +
|
|
532
|
+
"instead, each `{ when, until?, recurs? }`; a note may state several"
|
|
514
533
|
: `"${key}" is not a \`data:\` property declared by ${type}; ` +
|
|
515
534
|
`the container is closed, so the key reaches no document ` +
|
|
516
535
|
`and no page` +
|
|
@@ -1209,6 +1228,22 @@ export function lintNote(
|
|
|
1209
1228
|
const type = String(fm.type ?? "");
|
|
1210
1229
|
const raw = () => note.raw ?? "";
|
|
1211
1230
|
const at = (key, literal) => positionInFrontmatter(raw(), key, literal ?? undefined);
|
|
1231
|
+
|
|
1232
|
+
// The keys the closed top-level region refuses in general, but a
|
|
1233
|
+
// declaration still reads at the retiring position — this type's own
|
|
1234
|
+
// fields, which `resolveFieldValue`'s step 3b resolves, plus `pack` and
|
|
1235
|
+
// `packFolder`, declared for every type and resolved the same way by
|
|
1236
|
+
// {@link resolveDataProperty}. Derived ahead of the schema lookup below,
|
|
1237
|
+
// which runs after the closed-region check, so that check can skip
|
|
1238
|
+
// exactly these and nothing else: a key no declaration names stays
|
|
1239
|
+
// refused.
|
|
1240
|
+
const retiringTopLevelKeys = new Set([
|
|
1241
|
+
...UNIVERSAL_RETIRING_DATA_KEYS,
|
|
1242
|
+
...authoredFields(schemas?.[currentType(type)] ?? [])
|
|
1243
|
+
.filter((field) => field.topLevelMeans === undefined)
|
|
1244
|
+
.map((field) => retiredTopLevelKey(field))
|
|
1245
|
+
.filter((key) => key !== undefined),
|
|
1246
|
+
]);
|
|
1212
1247
|
if (type === "being") {
|
|
1213
1248
|
const archetypes = fm.data?.archetypes;
|
|
1214
1249
|
if (
|
|
@@ -1589,6 +1624,9 @@ export function lintNote(
|
|
|
1589
1624
|
|
|
1590
1625
|
for (const key of authoredNoteKeys(raw())) {
|
|
1591
1626
|
if (NOTE_TOP_LEVEL_KEY_SET.has(key)) continue;
|
|
1627
|
+
// Reported below, as the retiring position it is rather than a key
|
|
1628
|
+
// nobody recognises.
|
|
1629
|
+
if (retiringTopLevelKeys.has(key)) continue;
|
|
1592
1630
|
const position = positionInFrontmatter(raw(), key, undefined, { topLevel: true });
|
|
1593
1631
|
if (
|
|
1594
1632
|
findings.some(
|
|
@@ -1610,6 +1648,28 @@ export function lintNote(
|
|
|
1610
1648
|
});
|
|
1611
1649
|
}
|
|
1612
1650
|
|
|
1651
|
+
// `pack` and `packFolder` are declared for every type, so each is
|
|
1652
|
+
// checked once here rather than in the per-type `fields` loop below,
|
|
1653
|
+
// which only iterates a type's own declarations. A **warning**, for the
|
|
1654
|
+
// reason the per-type retiring position above is one: the note compiles
|
|
1655
|
+
// to the correct document either way, so failing a build over it would
|
|
1656
|
+
// red a tree that has done nothing wrong yet. Checked against every
|
|
1657
|
+
// configured system block, because `resolveDataProperty` reads a
|
|
1658
|
+
// system's own override before the top level, and a note may configure
|
|
1659
|
+
// more than one.
|
|
1660
|
+
for (const key of UNIVERSAL_RETIRING_DATA_KEYS) {
|
|
1661
|
+
const retiring = Object.keys(systems ?? {}).some(
|
|
1662
|
+
(blockName) => resolveDataProperty(fm, blockName, key).from === "topLevel",
|
|
1663
|
+
);
|
|
1664
|
+
if (!retiring) continue;
|
|
1665
|
+
findings.push({
|
|
1666
|
+
file: note.file,
|
|
1667
|
+
...at(key),
|
|
1668
|
+
severity: "warning",
|
|
1669
|
+
message: retiredTopLevelMessage({ name: `data.${key}` }),
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1613
1673
|
// The type's charset, before anything that looks the type up. A
|
|
1614
1674
|
// hyphenated type is unaddressable, and every lookup below would report it
|
|
1615
1675
|
// as a type nobody declared — true, but not the reason, and it would send
|
|
@@ -1827,6 +1887,18 @@ export function lintNote(
|
|
|
1827
1887
|
message: legacyKeyMessage("sohl", field),
|
|
1828
1888
|
});
|
|
1829
1889
|
}
|
|
1890
|
+
// `readsLegacyKey`'s sibling for the other retiring position — the
|
|
1891
|
+
// note's own top level, which `data:` gathered the fact off. Also a
|
|
1892
|
+
// **warning**: the closed-region check above already let this exact
|
|
1893
|
+
// key through for this exact reason, and the two must agree.
|
|
1894
|
+
if (readsRetiredTopLevel(field, from)) {
|
|
1895
|
+
findings.push({
|
|
1896
|
+
file: note.file,
|
|
1897
|
+
...at(retiredTopLevelKey(field)),
|
|
1898
|
+
severity: "warning",
|
|
1899
|
+
message: retiredTopLevelMessage(field),
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1830
1902
|
const absent = from === "default" || value === undefined || value === null;
|
|
1831
1903
|
// Where the field belongs, as a message names it: a shared field is not
|
|
1832
1904
|
// under `sohl:`, so telling an author to write `sohl.img` would send
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* # The Harbor {#harbor .wide}
|
|
8
8
|
* ```
|
|
9
9
|
*
|
|
10
|
-
* The index, the link checker, the journal compiler, the
|
|
10
|
+
* The index, the link checker, the journal compiler, the figure pass and the
|
|
11
11
|
* book all ask what a heading declares, and they have to give the same answer:
|
|
12
12
|
* an id a surface publishes as an anchor, classes a surface may honour, and
|
|
13
13
|
* attributes an HTML surface writes onto the element. This is where that
|
|
14
14
|
* reading lives, so there is one grammar — the same one a `:::` block and a
|
|
15
|
-
* `:::
|
|
15
|
+
* `:::figure` are written in.
|
|
16
16
|
*
|
|
17
17
|
* **A leaf, deliberately.** `anchors.mjs` is shared by the compilers, the index
|
|
18
|
-
* and the link checker and cannot import any of them; the
|
|
18
|
+
* and the link checker and cannot import any of them; the figure pass is
|
|
19
19
|
* imported by `anchors.mjs` and so cannot import it back. A module that imports
|
|
20
20
|
* only the attribute grammar can be asked by all of them.
|
|
21
21
|
*
|
|
@@ -38,7 +38,7 @@ const SUFFIX = /^(.*?)\s*\{([^}\n]*)\}\s*$/;
|
|
|
38
38
|
/**
|
|
39
39
|
* A heading's id.
|
|
40
40
|
*
|
|
41
|
-
* Looser than a
|
|
41
|
+
* Looser than a figure's or a block's, which must begin with a letter: a
|
|
42
42
|
* heading's id is published as an HTML `id` and as a Foundry page anchor, both
|
|
43
43
|
* of which admit a digit first, and the corpus carries ids that are Foundry
|
|
44
44
|
* document ids and ids derived from a numbered heading.
|
|
@@ -108,9 +108,9 @@ export const WITHHELD_CLASS = "secret";
|
|
|
108
108
|
* A Foundry UUID addresses a page and nothing smaller, so a section a link can
|
|
109
109
|
* reach has to be one: a top-level heading starts a page, and so does any
|
|
110
110
|
* heading that names an anchor. {@link module:engine/journals.splitPages}
|
|
111
|
-
* decides *where* a page begins — a
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* decides *where* a page begins — a heading inside a fence or a named block is
|
|
112
|
+
* its business — and asks this which headings are eligible, so no caller
|
|
113
|
+
* re-decides it.
|
|
114
114
|
*
|
|
115
115
|
* @param {{level: number, id: string}} heading - From
|
|
116
116
|
* {@link scanHeadingAttributes}, or any `{level, id}` pair.
|
|
@@ -204,7 +204,7 @@ export function headingAttributesPlugin(md) {
|
|
|
204
204
|
* Parse one line as a heading, and say whether it starts a journal page.
|
|
205
205
|
*
|
|
206
206
|
* The single reading of that rule. {@link module:engine/journals.splitPages}
|
|
207
|
-
* builds its pages from it; `scanBlocks` and `
|
|
207
|
+
* builds its pages from it; `scanBlocks` and `scanFigures` refuse a heading
|
|
208
208
|
* that starts a page written where it cannot become one. All three ask this, so
|
|
209
209
|
* none of them can disagree about what a heading line means or about what starts
|
|
210
210
|
* a page — and the attribute block comes off here, so a class beside an anchor
|
|
@@ -240,24 +240,37 @@ export function parseHeadingLine(line) {
|
|
|
240
240
|
* builds the pages from this, and the passes that honour a class on a
|
|
241
241
|
* page-opening heading ask the same question of the same answer.
|
|
242
242
|
*
|
|
243
|
-
* A
|
|
244
|
-
* found by the
|
|
245
|
-
* dropped there.
|
|
243
|
+
* A figure fence also opens a page, which is `splitPages`' business: a fence is
|
|
244
|
+
* found by the figure pass, and the page it opens is named for its label.
|
|
246
245
|
*
|
|
247
246
|
* @param {string} source - A note's markdown body.
|
|
247
|
+
* @param {ReadonlySet<number>} [blockLines] - The 0-based lines sitting inside
|
|
248
|
+
* a named block, supplied by the caller. A named block is read by
|
|
249
|
+
* {@link module:engine/content-blocks.scanBlocks}, which knows every name in
|
|
250
|
+
* the registry, reads an opener's attribute block, and tracks nesting by
|
|
251
|
+
* depth — so the caller that already holds that answer passes it rather than
|
|
252
|
+
* a second reading of the same lines being kept here.
|
|
248
253
|
* @returns {Map<number, {level: number, line: number, text: string, id: string,
|
|
249
254
|
* classes: string[], values: Record<string, string>}>} Keyed by 0-based line.
|
|
250
255
|
*/
|
|
251
|
-
export function pageOpenings(source) {
|
|
256
|
+
export function pageOpenings(source, blockLines = new Set()) {
|
|
252
257
|
const lines = String(source ?? "").split("\n");
|
|
253
258
|
const openings = new Map();
|
|
254
|
-
let
|
|
255
|
-
let inBlock = false;
|
|
259
|
+
let codeFence = null;
|
|
256
260
|
for (const [index, line] of lines.entries()) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
// A fence's closer must carry the same character as its opener and
|
|
262
|
+
// be at least as long, same as {@link scanHeadingAttributes} tracks
|
|
263
|
+
// it below — a longer fence is what lets a fenced example carry a
|
|
264
|
+
// shorter fence of its own as literal content.
|
|
265
|
+
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
266
|
+
if (codeFence) {
|
|
267
|
+
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length) {
|
|
268
|
+
codeFence = null;
|
|
269
|
+
}
|
|
270
|
+
} else if (fence) {
|
|
271
|
+
codeFence = fence[1];
|
|
272
|
+
}
|
|
273
|
+
if (codeFence !== null || blockLines.has(index)) continue;
|
|
261
274
|
const heading = parseHeadingLine(line);
|
|
262
275
|
if (!heading?.startsPage) continue;
|
|
263
276
|
openings.set(index, { ...heading, line: index + 1 });
|
package/engine/helpers.mjs
CHANGED
|
@@ -42,8 +42,15 @@ import footnotePlugin from "markdown-it-footnote";
|
|
|
42
42
|
import deflistPlugin from "markdown-it-deflist";
|
|
43
43
|
import { iconPlugin } from "./content-icons.mjs";
|
|
44
44
|
import { imagePlugin, imagesIn } from "./content-images.mjs";
|
|
45
|
-
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
46
|
-
import {
|
|
45
|
+
import { embedRole, resolveEmbeds } from "./content-embeds.mjs";
|
|
46
|
+
import { collectAssetRecords } from "./asset-index.mjs";
|
|
47
|
+
import { assetImageInfoByPathname } from "./art-fields.mjs";
|
|
48
|
+
import {
|
|
49
|
+
assetPathnameKey,
|
|
50
|
+
foundryAddressProblem,
|
|
51
|
+
pathnameProblem,
|
|
52
|
+
resolvePathname,
|
|
53
|
+
} from "./pathnames.mjs";
|
|
47
54
|
import log from "loglevel";
|
|
48
55
|
|
|
49
56
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -67,7 +74,7 @@ import { DERIVED_PACKED_TYPES, NEVER_PACKED_TYPES } from "./note-claims.mjs";
|
|
|
67
74
|
import { expandContentTables } from "./content-tables.mjs";
|
|
68
75
|
import { renderBlocks } from "./content-blocks.mjs";
|
|
69
76
|
import { headingAttributesPlugin } from "./heading-attributes.mjs";
|
|
70
|
-
import {
|
|
77
|
+
import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
|
|
71
78
|
import { positionInBody } from "./diagnostics.mjs";
|
|
72
79
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
73
80
|
// registry can import them without reaching back through this one.
|
|
@@ -84,6 +91,9 @@ export {
|
|
|
84
91
|
parseValueDesc,
|
|
85
92
|
} from "./frontmatter.mjs";
|
|
86
93
|
|
|
94
|
+
/** @type {Map<string, object>|undefined} */
|
|
95
|
+
let localAssetImageInfoCache;
|
|
96
|
+
|
|
87
97
|
/**
|
|
88
98
|
* The markdown renderer every surface shares.
|
|
89
99
|
*
|
|
@@ -117,25 +127,65 @@ export const md = markdownit({ html: true })
|
|
|
117
127
|
// serves a file from inside the install, so a body image's address is
|
|
118
128
|
// translated by the same rule `img:` follows.
|
|
119
129
|
.use(
|
|
120
|
-
imagePlugin(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
imagePlugin(
|
|
131
|
+
(src) => {
|
|
132
|
+
// **Reported upstream, never here.** A renderer has no channel to
|
|
133
|
+
// report through, and this one runs inside the very passes whose
|
|
134
|
+
// job is to collect findings — a throw would take the whole lint
|
|
135
|
+
// down and lose every other finding in the tree. So nothing reaches
|
|
136
|
+
// this point unreported: {@link convertNoteWikilinks} refuses a body
|
|
137
|
+
// image with no address inside the install before a compiler renders
|
|
138
|
+
// one, and the address passes report the same pathname with a line
|
|
139
|
+
// and a column. What is left here is a fallback for a caller with no
|
|
140
|
+
// configuration to resolve against, where the authored pathname is
|
|
141
|
+
// the most honest thing to emit.
|
|
142
|
+
try {
|
|
143
|
+
return resolveImg(src, loadPackConfig()) ?? src;
|
|
144
|
+
} catch {
|
|
145
|
+
return src;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
(src) => {
|
|
149
|
+
// A body image is free to write the bare, own-package form,
|
|
150
|
+
// so the address is normalized to the pathname the cache
|
|
151
|
+
// below keys by before it is looked up.
|
|
152
|
+
try {
|
|
153
|
+
const key = assetPathnameKey(src, loadPackConfig());
|
|
154
|
+
return key ? localAssetImageInfo().get(key) : undefined;
|
|
155
|
+
} catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
),
|
|
137
160
|
);
|
|
138
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The pictures this package's own tree ships, by the pathname their address
|
|
164
|
+
* resolves to — read lazily and cached for the process, exactly as
|
|
165
|
+
* {@link loadPackConfig} is, since one compile reads one tree once.
|
|
166
|
+
*
|
|
167
|
+
* **Local only.** A picture embedded from a dependency carries no role or
|
|
168
|
+
* pixel size here: fetching a foreign package's own asset index to size one
|
|
169
|
+
* inline Foundry image is a cost this lazy cache does not pay, and the
|
|
170
|
+
* picture simply draws at the medium's ordinary size, as it always has.
|
|
171
|
+
*
|
|
172
|
+
* @returns {Map<string, {type: string, role?: string, width: number|"", height: number|""}>}
|
|
173
|
+
*/
|
|
174
|
+
function localAssetImageInfo() {
|
|
175
|
+
if (localAssetImageInfoCache) return localAssetImageInfoCache;
|
|
176
|
+
try {
|
|
177
|
+
const config = loadPackConfig();
|
|
178
|
+
const records = collectAssetRecords(config.paths.assets, {
|
|
179
|
+
contentPackage: config.contentPackage,
|
|
180
|
+
problems: [],
|
|
181
|
+
});
|
|
182
|
+
localAssetImageInfoCache = assetImageInfoByPathname(records);
|
|
183
|
+
} catch {
|
|
184
|
+
localAssetImageInfoCache = new Map();
|
|
185
|
+
}
|
|
186
|
+
return localAssetImageInfoCache;
|
|
187
|
+
}
|
|
188
|
+
|
|
139
189
|
md.renderer.rules.footnote_block_open = () =>
|
|
140
190
|
'<section class="footnotes"><h2>Footnotes</h2><ol class="footnotes-list">\n';
|
|
141
191
|
md.renderer.rules.footnote_caption = (tokens, idx, _options, env) => {
|
|
@@ -155,11 +205,26 @@ md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
|
|
|
155
205
|
return `<li id="fn${id}" class="footnote-item" value="${number}">`;
|
|
156
206
|
};
|
|
157
207
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Render a note body with Foundry's named-block markup.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} body - The body, tables expanded and wikilinks resolved.
|
|
212
|
+
* @param {Array<{id: string, label: string}>} [figures] - Labels assigned by
|
|
213
|
+
* a note-wide scan, matched by id — see
|
|
214
|
+
* {@link module:engine/content-figures.renderFigureBlocks}.
|
|
215
|
+
* @param {Map<string, number>} [footnoteNumbers] - Shared across the note.
|
|
216
|
+
* @param {string} [docId] - Threaded into footnote anchors.
|
|
217
|
+
* @param {(address: string) => string|undefined} [resolveRole] - From a
|
|
218
|
+
* picture's address to the role its asset declares — see
|
|
219
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
220
|
+
* @returns {string} The rendered HTML.
|
|
221
|
+
*/
|
|
222
|
+
export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, resolveRole) {
|
|
160
223
|
const blocks = renderBlocks(body, "foundry");
|
|
161
|
-
const
|
|
162
|
-
|
|
224
|
+
const figured = renderFigureBlocks(blocks.markdown, (block) => md.render(block), figures, {
|
|
225
|
+
resolveRole,
|
|
226
|
+
});
|
|
227
|
+
return md.render(figured.markdown, { footnoteNumbers, docId });
|
|
163
228
|
}
|
|
164
229
|
|
|
165
230
|
/**
|
|
@@ -545,11 +610,11 @@ export function makeFilename(name, id) {
|
|
|
545
610
|
* the other.
|
|
546
611
|
*
|
|
547
612
|
* **`title` does not follow this rule**, and must not be made to. On a
|
|
548
|
-
* `type: affiliation` note `title` is
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
613
|
+
* `type: affiliation` note `title` is a declared item field with its own
|
|
614
|
+
* default (`""`, in `sohl/item-fields.mjs`), authored only inside the item's
|
|
615
|
+
* own `sohl` block — the closed top-level vocabulary has no `title` entry, so
|
|
616
|
+
* there is no shared key for this function's null-versus-blank distinction to
|
|
617
|
+
* apply to.
|
|
553
618
|
*
|
|
554
619
|
* **`banner:` does not follow it either, deliberately.** It is not a file
|
|
555
620
|
* inside a Foundry install: it reaches no compiled document and no book, its
|
|
@@ -941,7 +1006,9 @@ export function convertNoteWikilinks(
|
|
|
941
1006
|
docPack,
|
|
942
1007
|
index,
|
|
943
1008
|
captionLabels: new Map(
|
|
944
|
-
|
|
1009
|
+
scanFigures(source, { resolveRole: (address) => embedRole(index, address) })
|
|
1010
|
+
.figures.filter((figure) => figure.id)
|
|
1011
|
+
.map((figure) => [figure.id, figure.label]),
|
|
945
1012
|
),
|
|
946
1013
|
});
|
|
947
1014
|
/**
|
|
@@ -112,11 +112,13 @@ export function compilesSystemDocument(fm, map, router) {
|
|
|
112
112
|
* @param {object} [options.router] - The pack router deciding which system
|
|
113
113
|
* compiles a document for this note. Defaults to the consuming repository's.
|
|
114
114
|
* @param {object} [options.dates] - Calendar conversion context.
|
|
115
|
+
* @param {string} [options.contentPackage] - This build's content package,
|
|
116
|
+
* the default a note box's short-form Address resolves against.
|
|
115
117
|
* @returns {object[]} The boxes, in the order every medium renders them.
|
|
116
118
|
* @throws {Error} When the built set disagrees with what the note's type maps
|
|
117
119
|
* to — see {@link module:engine/infobox.assertInfoboxSet}.
|
|
118
120
|
*/
|
|
119
|
-
export function noteInfoboxes(fm, { resolve, router = packRouter(), dates } = {}) {
|
|
121
|
+
export function noteInfoboxes(fm, { resolve, router = packRouter(), dates, contentPackage } = {}) {
|
|
120
122
|
const boxes = buildInfoboxes(fm, {
|
|
121
123
|
maps: KNOWN_DOCUMENT_SUBTYPE_MAPS,
|
|
122
124
|
providers: KNOWN_INFOBOXES,
|
|
@@ -124,6 +126,7 @@ export function noteInfoboxes(fm, { resolve, router = packRouter(), dates } = {}
|
|
|
124
126
|
resolveField: resolveFieldValue,
|
|
125
127
|
resolve,
|
|
126
128
|
dates,
|
|
129
|
+
contentPackage,
|
|
127
130
|
});
|
|
128
131
|
return /** @type {object[]} */ (
|
|
129
132
|
assertInfoboxSet(boxes, fm, { maps: KNOWN_DOCUMENT_SUBTYPE_MAPS })
|