@heroiclands/package-build 23.1.1 → 23.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/bin/content-commands.mjs +68 -149
- package/bin/package-build.mjs +56 -10
- package/ci/check-banners.mjs +42 -6
- package/ci/confirm-npm-version.mjs +135 -0
- package/ci/content-format-reference.mjs +14 -5
- package/config.mjs +1 -0
- package/container.mjs +185 -4
- package/content-config.mjs +333 -210
- package/deploy.mjs +116 -10
- package/docs/api.md +238 -246
- package/docs/authoring/assets.md +11 -5
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +87 -0
- package/docs/commands.md +138 -99
- package/docs/configuration.md +277 -125
- package/docs/content-format.md +2 -0
- package/docs/diagnostics.md +48 -13
- package/docs/guides/book.md +341 -21
- package/docs/guides/e2e.md +13 -2
- package/docs/guides/icons.md +14 -4
- package/docs/guides/module-project.md +18 -8
- package/docs/guides/release-and-deploy.md +32 -0
- package/docs/guides/site.md +5 -2
- package/docs/project-setup.md +30 -10
- package/docs/reference/format-details.md +373 -170
- package/docs/reference/note-types.md +128 -109
- package/docs/reference/theme-pages.md +31 -6
- package/e2e.mjs +140 -28
- package/engine/actor-compiler.mjs +22 -31
- package/engine/address-charset.mjs +4 -17
- package/engine/address-fields.mjs +1 -1
- package/engine/address-render.mjs +12 -1
- package/engine/address-values.mjs +6 -0
- package/engine/address.mjs +46 -33
- package/engine/art-fields.mjs +30 -5
- package/engine/art-slots.mjs +10 -1
- package/engine/asset-bindings.mjs +12 -1
- package/engine/base-compiler.mjs +34 -65
- package/engine/being-age.mjs +2 -7
- package/engine/being-measurements.mjs +12 -1
- package/engine/calendar-artifacts.mjs +6 -0
- package/engine/calendar-choice.mjs +12 -1
- package/engine/calendar-format.mjs +12 -1
- package/engine/calendar-human.mjs +12 -1
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-address.mjs +9 -12
- package/engine/content-alerts.mjs +13 -1
- package/engine/content-blocks.mjs +14 -3
- package/engine/content-embeds.mjs +11 -3
- package/engine/content-figures.mjs +9 -0
- package/engine/content-footnotes.mjs +12 -1
- package/engine/content-format-check.mjs +11 -262
- package/engine/content-format.yaml +5 -2
- package/engine/content-images.mjs +3 -0
- package/engine/content-index.mjs +443 -20
- package/engine/content-links.mjs +29 -35
- package/engine/content-lint.mjs +2 -3
- package/engine/content-poetry.mjs +12 -1
- package/engine/content-slug.mjs +2 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-tables.mjs +12 -1
- package/engine/culture-choice.mjs +210 -13
- package/engine/data-keys.mjs +19 -16
- package/engine/date-conversion.mjs +12 -1
- package/engine/declaration-check.mjs +9 -0
- package/engine/derived-fields.mjs +6 -7
- package/engine/diagnostics.mjs +34 -4
- package/engine/docs-checks.mjs +9 -0
- package/engine/document-subtypes.mjs +47 -34
- package/engine/event-views.mjs +139 -14
- package/engine/exported-scene.mjs +92 -28
- package/engine/extension-attributes.mjs +12 -1
- package/engine/field-spec.mjs +11 -102
- package/engine/folder-notes.mjs +0 -64
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/format-generated.mjs +9 -0
- package/engine/foundry-entries.mjs +10 -28
- package/engine/frontmatter-lint.mjs +83 -392
- package/engine/frontmatter.mjs +20 -70
- package/engine/generate.mjs +10 -15
- package/engine/heading-attributes.mjs +12 -1
- package/engine/helpers.mjs +41 -102
- package/engine/holdings.mjs +1 -1
- package/engine/homepage.mjs +3 -10
- package/engine/ids.mjs +1 -167
- package/engine/index-records.mjs +6 -4
- package/engine/index.mjs +0 -3
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +13 -14
- package/engine/item-compiler.mjs +4 -10
- package/engine/item-docs.mjs +3 -5
- package/engine/item-registry.mjs +2 -10
- package/engine/journals.mjs +1 -5
- package/engine/literature-notes.mjs +4 -12
- package/engine/macros.mjs +1 -2
- package/engine/map-graphviz.mjs +12 -1
- package/engine/markdown-expressions.mjs +12 -1
- package/engine/markdownlint-run.mjs +567 -0
- package/engine/metadata-index.mjs +42 -12
- package/engine/note-addresses.mjs +143 -35
- package/engine/note-claims.mjs +14 -28
- package/engine/note-dates.mjs +3 -3
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-ids.mjs +8 -14
- package/engine/note-package.mjs +8 -25
- package/engine/note-state.mjs +1 -3
- package/engine/note-vocabulary.mjs +148 -84
- package/engine/number-words.mjs +12 -1
- package/engine/pack-config.mjs +15 -25
- package/engine/pack-router.mjs +10 -16
- package/engine/pathnames.mjs +8 -22
- package/engine/pdf-build.mjs +773 -121
- package/engine/pdf-coverage.mjs +188 -0
- package/engine/pdf-index.mjs +54 -7
- package/engine/pdf-pages.mjs +247 -0
- package/engine/pdf-render.mjs +114 -4
- package/engine/pdf-toc.mjs +29 -32
- package/engine/project-init.mjs +46 -6
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +52 -88
- package/engine/readability-lint.mjs +9 -0
- package/engine/reckoning-markers.mjs +12 -1
- package/engine/reference-targets.mjs +7 -0
- package/engine/runtime-only-fields.mjs +5 -8
- package/engine/scene-levels.mjs +2 -2
- package/engine/scenes.mjs +1 -1
- package/engine/shortcode-references.mjs +7 -0
- package/engine/site-build.mjs +30 -16
- package/engine/site-index.mjs +10 -16
- package/engine/site-preview.mjs +6 -0
- package/engine/site-root.mjs +8 -1
- package/engine/social-tie-terms.mjs +12 -1
- package/engine/social-ties.mjs +3 -0
- package/engine/sql-tables.mjs +56 -20
- package/engine/standing-terms.mjs +11 -1
- package/engine/standings.mjs +11 -27
- package/engine/system-block.mjs +39 -141
- package/engine/system-vocabulary.mjs +6 -0
- package/engine/walk-files.mjs +69 -0
- package/engine/web-wikilinks.mjs +23 -3
- package/engine/wikilink-syntax.mjs +32 -21
- package/engine/wikilinks.mjs +27 -16
- package/githooks/attribution.mjs +8 -0
- package/githooks/commit-message.mjs +9 -0
- package/hm3/actors.mjs +9 -48
- package/hm3/default-item-art.mjs +9 -9
- package/hm3/item-fields.mjs +2 -9
- package/hm3/template-priority.mjs +6 -7
- package/hugo-theme/data/banner-kinds.yaml +78 -0
- package/hugo-theme/data/banners.yaml +2 -3
- package/hugo-theme/layouts/404.html +3 -3
- package/hugo-theme/layouts/partials/banner-url.html +31 -7
- package/hugo-theme/layouts/partials/hero-banner.html +13 -14
- package/hugo-theme/layouts/partials/related.html +1 -1
- package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
- package/hugo-theme/static/css/style.css +1 -1
- package/manifest.mjs +11 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +6 -3
- package/release.mjs +21 -14
- package/sohl/actors.mjs +16 -27
- package/sohl/default-item-art.mjs +3 -3
- package/sohl/document-subtypes.mjs +1 -16
- package/sohl/infobox.mjs +2 -3
- package/sohl/item-builders.mjs +5 -8
- package/sohl/item-fields.mjs +5 -35
- package/sohl/items.mjs +0 -4
- package/sohl/kb-passes.mjs +3 -3
- package/types/container.d.mts +116 -5
- package/types/content-config.d.mts +83 -35
- package/types/deploy.d.mts +42 -6
- package/types/e2e.d.mts +35 -3
- package/types/engine/address-charset.d.mts +4 -17
- package/types/engine/address.d.mts +17 -8
- package/types/engine/art-fields.d.mts +7 -4
- package/types/engine/art-slots.d.mts +6 -0
- package/types/engine/base-compiler.d.mts +15 -17
- package/types/engine/content-address.d.mts +9 -12
- package/types/engine/content-embeds.d.mts +4 -2
- package/types/engine/content-format-check.d.mts +0 -64
- package/types/engine/content-index.d.mts +82 -3
- package/types/engine/content-links.d.mts +2 -4
- package/types/engine/culture-choice.d.mts +51 -8
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/derived-fields.d.mts +1 -2
- package/types/engine/diagnostics.d.mts +27 -4
- package/types/engine/document-subtypes.d.mts +12 -31
- package/types/engine/event-views.d.mts +14 -2
- package/types/engine/exported-scene.d.mts +3 -2
- package/types/engine/field-spec.d.mts +13 -122
- package/types/engine/folder-notes.d.mts +0 -39
- package/types/engine/foreign-catalog.d.mts +2 -1
- package/types/engine/foundry-entries.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +2 -2
- package/types/engine/frontmatter.d.mts +19 -45
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +25 -42
- package/types/engine/homepage.d.mts +3 -10
- package/types/engine/ids.d.mts +0 -114
- package/types/engine/index-records.d.mts +6 -4
- package/types/engine/index.d.mts +0 -1
- package/types/engine/infobox-render.d.mts +7 -1
- package/types/engine/infobox.d.mts +3 -2
- package/types/engine/item-docs.d.mts +1 -1
- package/types/engine/item-registry.d.mts +2 -4
- package/types/engine/literature-notes.d.mts +1 -1
- package/types/engine/markdownlint-run.d.mts +40 -0
- package/types/engine/metadata-index.d.mts +9 -1
- package/types/engine/note-addresses.d.mts +27 -2
- package/types/engine/note-claims.d.mts +3 -5
- package/types/engine/note-ids.d.mts +2 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-state.d.mts +1 -3
- package/types/engine/note-vocabulary.d.mts +28 -21
- package/types/engine/pack-config.d.mts +2 -6
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/pathnames.d.mts +3 -3
- package/types/engine/pdf-build.d.mts +98 -12
- package/types/engine/pdf-coverage.d.mts +60 -0
- package/types/engine/pdf-index.d.mts +34 -5
- package/types/engine/pdf-pages.d.mts +143 -0
- package/types/engine/pdf-render.d.mts +45 -1
- package/types/engine/pdf-toc.d.mts +12 -14
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +11 -10
- package/types/engine/runtime-only-fields.d.mts +2 -2
- package/types/engine/site-build.d.mts +9 -5
- package/types/engine/site-index.d.mts +4 -5
- package/types/engine/site-root.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -2
- package/types/engine/standing-terms.d.mts +2 -1
- package/types/engine/standings.d.mts +7 -9
- package/types/engine/system-block.d.mts +10 -44
- package/types/engine/walk-files.d.mts +15 -0
- package/types/engine/wikilink-syntax.d.mts +18 -19
- package/types/hm3/actors.d.mts +1 -2
- package/types/hm3/item-fields.d.mts +2 -4
- package/types/hm3/template-priority.d.mts +6 -7
- package/types/manifest.d.mts +3 -2
- package/types/release.d.mts +8 -7
- package/types/sohl/actors.d.mts +2 -1
- package/types/sohl/document-subtypes.d.mts +1 -2
- package/types/sohl/item-fields.d.mts +3 -7
- package/engine/retired-fields.mjs +0 -631
- package/types/engine/retired-fields.d.mts +0 -354
package/e2e.mjs
CHANGED
|
@@ -46,13 +46,16 @@ import path from "node:path";
|
|
|
46
46
|
import process from "node:process";
|
|
47
47
|
import { spawnSync } from "node:child_process";
|
|
48
48
|
|
|
49
|
-
import { deployStage } from "./deploy.mjs";
|
|
49
|
+
import { deployStage, isRemoteTarget, packageSubpath } from "./deploy.mjs";
|
|
50
|
+
import { emitDiagnostic } from "./engine/diagnostics.mjs";
|
|
50
51
|
import {
|
|
51
52
|
containerAction,
|
|
52
53
|
containerExists,
|
|
54
|
+
containerTreeRemover,
|
|
53
55
|
dataEnvVar,
|
|
54
56
|
resolveContainer,
|
|
55
57
|
resolveDataRoot,
|
|
58
|
+
runDataRootScript,
|
|
56
59
|
runDocker,
|
|
57
60
|
runningFoundryContainers,
|
|
58
61
|
} from "./container.mjs";
|
|
@@ -365,24 +368,41 @@ export function parseFastArgs(argv, build) {
|
|
|
365
368
|
*
|
|
366
369
|
* Writes `world.json` and compiles each LevelDB collection from JSON, so the
|
|
367
370
|
* result is a genuine world Foundry launches without migration or setup. The
|
|
368
|
-
* world directory is
|
|
369
|
-
*
|
|
371
|
+
* world directory is replaced each time, which is what makes a run repeatable.
|
|
372
|
+
*
|
|
373
|
+
* The world is built in a staging directory beside the worlds, then moved into
|
|
374
|
+
* place and handed to the Foundry container's user by a throwaway container
|
|
375
|
+
* mounting the same data root. Replacing the old world happens there too. Both
|
|
376
|
+
* halves matter: a database the host user wrote cannot be locked by the
|
|
377
|
+
* container's user, and a directory the container's user wrote (such as the
|
|
378
|
+
* `lost/` folder Foundry's repair makes) cannot be unlinked by the host user.
|
|
370
379
|
*
|
|
371
380
|
* @param {object} opts
|
|
372
381
|
* @param {object} opts.config - The resolved package-build configuration.
|
|
373
382
|
* @param {object} opts.packageJson - The repository's `package.json`.
|
|
374
383
|
* @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
|
|
375
384
|
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
385
|
+
* @param {(opts: object) => string} [opts.runScript] - Runs a shell script in a
|
|
386
|
+
* throwaway container on the data root; replaceable for tests.
|
|
376
387
|
* @returns {Promise<{worldDir: string, world: E2EWorld}>} Where it landed.
|
|
377
388
|
*/
|
|
378
|
-
export async function seedTestWorld({
|
|
389
|
+
export async function seedTestWorld({
|
|
390
|
+
config,
|
|
391
|
+
packageJson,
|
|
392
|
+
env = process.env,
|
|
393
|
+
log = () => {},
|
|
394
|
+
runScript = runDataRootScript,
|
|
395
|
+
}) {
|
|
379
396
|
const stage = config.e2eStage;
|
|
380
397
|
const dataRoot = requireIsolatedDataRoot(stage, env);
|
|
381
398
|
const world = resolveE2EWorld(config, env);
|
|
382
399
|
|
|
383
400
|
const worldDir = path.join(dataRoot, "Data", "worlds", world.worldId);
|
|
384
|
-
|
|
385
|
-
|
|
401
|
+
// Beside `Data/`, not inside it, so Foundry never sees a half-built world.
|
|
402
|
+
const stagingName = `.seed-${world.worldId}`;
|
|
403
|
+
const stagingDir = path.join(dataRoot, stagingName);
|
|
404
|
+
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
405
|
+
await fs.mkdir(stagingDir, { recursive: true });
|
|
386
406
|
|
|
387
407
|
const coreVersion = String(config.compatibilityMinimum ?? "").split(".")[0];
|
|
388
408
|
if (!coreVersion) {
|
|
@@ -394,7 +414,7 @@ export async function seedTestWorld({ config, packageJson, env = process.env, lo
|
|
|
394
414
|
}
|
|
395
415
|
|
|
396
416
|
await fs.writeFile(
|
|
397
|
-
path.join(
|
|
417
|
+
path.join(stagingDir, "world.json"),
|
|
398
418
|
JSON.stringify(
|
|
399
419
|
worldManifest({
|
|
400
420
|
...world,
|
|
@@ -427,7 +447,7 @@ export async function seedTestWorld({ config, packageJson, env = process.env, lo
|
|
|
427
447
|
}
|
|
428
448
|
|
|
429
449
|
const { compilePack } = await import("@foundryvtt/foundryvtt-cli");
|
|
430
|
-
const sourceRoot = path.join(
|
|
450
|
+
const sourceRoot = path.join(stagingDir, ".seed-src");
|
|
431
451
|
for (const [collection, documents] of Object.entries(collections)) {
|
|
432
452
|
const sourceDir = path.join(sourceRoot, collection);
|
|
433
453
|
await fs.mkdir(sourceDir, { recursive: true });
|
|
@@ -453,18 +473,126 @@ export async function seedTestWorld({ config, packageJson, env = process.env, lo
|
|
|
453
473
|
for (const collection of await fs.readdir(sourceRoot)) {
|
|
454
474
|
await compilePack(
|
|
455
475
|
path.join(sourceRoot, collection),
|
|
456
|
-
path.join(
|
|
476
|
+
path.join(stagingDir, "data", collection),
|
|
457
477
|
{ log: false },
|
|
458
478
|
);
|
|
459
479
|
}
|
|
460
480
|
await fs.rm(sourceRoot, { recursive: true, force: true });
|
|
461
481
|
|
|
482
|
+
// Hand the world to the user Foundry runs as.
|
|
483
|
+
const container = resolveContainer({ stage, config, env });
|
|
484
|
+
const owner = containerOwner({ container, dataRoot, runScript });
|
|
485
|
+
runScript({
|
|
486
|
+
image: container.image,
|
|
487
|
+
dataRoot,
|
|
488
|
+
user: "0:0",
|
|
489
|
+
script: [
|
|
490
|
+
"set -e",
|
|
491
|
+
'rm -rf "/data/Data/worlds/$1"',
|
|
492
|
+
"mkdir -p /data/Data/worlds",
|
|
493
|
+
'mv "/data/$2" "/data/Data/worlds/$1"',
|
|
494
|
+
'chown -R "$3" "/data/Data/worlds/$1"',
|
|
495
|
+
].join("\n"),
|
|
496
|
+
args: [world.worldId, stagingName, owner],
|
|
497
|
+
});
|
|
498
|
+
|
|
462
499
|
log(`Seeded world '${world.worldId}' at ${worldDir}`);
|
|
463
500
|
log(` GM user: ${world.gmName} (id ${world.gmId})`);
|
|
464
501
|
log(` password: ${world.gmPassword}`);
|
|
465
502
|
return { worldDir, world };
|
|
466
503
|
}
|
|
467
504
|
|
|
505
|
+
/**
|
|
506
|
+
* The `uid:gid` the Foundry container runs as, asked of the container's own
|
|
507
|
+
* user so it is right whatever `--user` or the image says.
|
|
508
|
+
*
|
|
509
|
+
* @param {object} opts
|
|
510
|
+
* @param {import("./container.mjs").ResolvedContainer} opts.container - The stage's container.
|
|
511
|
+
* @param {string} opts.dataRoot - The stage's local data root.
|
|
512
|
+
* @param {(opts: object) => string} opts.runScript - Throwaway-container runner.
|
|
513
|
+
* @returns {string} For example `1000:1000`.
|
|
514
|
+
* @throws {Error} When the answer is not a numeric pair.
|
|
515
|
+
*/
|
|
516
|
+
function containerOwner({ container, dataRoot, runScript }) {
|
|
517
|
+
const owner = runScript({
|
|
518
|
+
image: container.image,
|
|
519
|
+
dataRoot,
|
|
520
|
+
user: container.user,
|
|
521
|
+
script: 'echo "$(id -u):$(id -g)"',
|
|
522
|
+
}).trim();
|
|
523
|
+
if (!/^\d+:\d+$/.test(owner)) {
|
|
524
|
+
throw new Error(`Could not determine the container's user: '${owner}'.`);
|
|
525
|
+
}
|
|
526
|
+
return owner;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Deploy the staged package to the end-to-end stage so the container's user
|
|
531
|
+
* can run it.
|
|
532
|
+
*
|
|
533
|
+
* The package's databases (compendium packs) are written by the host user, and
|
|
534
|
+
* Foundry cannot lock them unless it owns them; a pack Foundry has repaired
|
|
535
|
+
* holds directories the host user cannot unlink. So the previous install is
|
|
536
|
+
* removed, and the new one handed over, by a throwaway container on the same
|
|
537
|
+
* data root. A remote destination is deployed as {@link deployStage} does.
|
|
538
|
+
*
|
|
539
|
+
* @param {object} opts
|
|
540
|
+
* @param {object} opts.config - The resolved package-build configuration.
|
|
541
|
+
* @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
|
|
542
|
+
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
543
|
+
* @param {(opts: object) => string} [opts.runScript] - Throwaway-container runner.
|
|
544
|
+
* @returns {Promise<void>}
|
|
545
|
+
*/
|
|
546
|
+
export async function deployForContainer({
|
|
547
|
+
config,
|
|
548
|
+
env = process.env,
|
|
549
|
+
log = () => {},
|
|
550
|
+
runScript = runDataRootScript,
|
|
551
|
+
}) {
|
|
552
|
+
const stage = config.e2eStage;
|
|
553
|
+
const target = env[dataEnvVar(stage)]?.trim() ?? "";
|
|
554
|
+
const local = target !== "" && !isRemoteTarget(target);
|
|
555
|
+
const relative = packageSubpath(config.packageKind, config.packageId).join("/");
|
|
556
|
+
const container = resolveContainer({ stage, config, env });
|
|
557
|
+
|
|
558
|
+
if (local) {
|
|
559
|
+
runScript({
|
|
560
|
+
image: container.image,
|
|
561
|
+
dataRoot: target,
|
|
562
|
+
user: "0:0",
|
|
563
|
+
script: 'rm -rf "/data/$1"',
|
|
564
|
+
args: [relative],
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const { leftovers } = await deployStage({
|
|
568
|
+
stage,
|
|
569
|
+
source: path.join(config.rootDir, config.stageDir),
|
|
570
|
+
packageKind: config.packageKind,
|
|
571
|
+
packageId: config.packageId,
|
|
572
|
+
env,
|
|
573
|
+
prefix: config.envPrefix,
|
|
574
|
+
log,
|
|
575
|
+
removeTree: containerTreeRemover({ container, runScript }),
|
|
576
|
+
});
|
|
577
|
+
for (const leftover of leftovers) {
|
|
578
|
+
emitDiagnostic({
|
|
579
|
+
file: leftover.path,
|
|
580
|
+
severity: "warning",
|
|
581
|
+
message: `the new package is installed, but this previous tree could not be removed (${leftover.reason})`,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (local) {
|
|
585
|
+
const owner = containerOwner({ container, dataRoot: target, runScript });
|
|
586
|
+
runScript({
|
|
587
|
+
image: container.image,
|
|
588
|
+
dataRoot: target,
|
|
589
|
+
user: "0:0",
|
|
590
|
+
script: 'chown -R "$2" "/data/$1"',
|
|
591
|
+
args: [relative, owner],
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
468
596
|
/**
|
|
469
597
|
* The end-to-end stage's data root, refused when it is another stage's.
|
|
470
598
|
*
|
|
@@ -543,7 +671,7 @@ export async function waitForWorld({ url, container, stage, timeoutMs = 180_000,
|
|
|
543
671
|
}
|
|
544
672
|
throw new Error(
|
|
545
673
|
`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the ` +
|
|
546
|
-
`world to activate. Check \`package-build container logs\`.`,
|
|
674
|
+
`world to activate. Check \`package-build container ${stage} logs\`.`,
|
|
547
675
|
);
|
|
548
676
|
}
|
|
549
677
|
|
|
@@ -944,15 +1072,7 @@ export async function e2eRun({
|
|
|
944
1072
|
}
|
|
945
1073
|
|
|
946
1074
|
log(`▸ deploy ${stage}`);
|
|
947
|
-
await
|
|
948
|
-
stage,
|
|
949
|
-
source: path.join(config.rootDir, config.stageDir),
|
|
950
|
-
packageKind: config.packageKind,
|
|
951
|
-
packageId: config.packageId,
|
|
952
|
-
env,
|
|
953
|
-
prefix: config.envPrefix,
|
|
954
|
-
log,
|
|
955
|
-
});
|
|
1075
|
+
await deployForContainer({ config, env, log });
|
|
956
1076
|
|
|
957
1077
|
log("▸ seed");
|
|
958
1078
|
const { world } = await seedTestWorld({ config, packageJson, env, log });
|
|
@@ -1044,15 +1164,7 @@ export async function e2eFast({ config, argv = [], env = process.env, log = () =
|
|
|
1044
1164
|
const exists = containerExists(container.name);
|
|
1045
1165
|
|
|
1046
1166
|
log(`▸ deploy ${stage}`);
|
|
1047
|
-
await
|
|
1048
|
-
stage,
|
|
1049
|
-
source: path.join(config.rootDir, config.stageDir),
|
|
1050
|
-
packageKind: config.packageKind,
|
|
1051
|
-
packageId: config.packageId,
|
|
1052
|
-
env,
|
|
1053
|
-
prefix: config.envPrefix,
|
|
1054
|
-
log,
|
|
1055
|
-
});
|
|
1167
|
+
await deployForContainer({ config, env, log });
|
|
1056
1168
|
|
|
1057
1169
|
const action = !exists || recreate ? "recreate" : "restart";
|
|
1058
1170
|
log(`▸ container ${action}`);
|
|
@@ -52,7 +52,7 @@ import log from "loglevel";
|
|
|
52
52
|
|
|
53
53
|
import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
54
54
|
import { makeId } from "./helpers.mjs";
|
|
55
|
-
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
55
|
+
import { emitDiagnostic, locateFrontmatterKey } from "./diagnostics.mjs";
|
|
56
56
|
// The `{#appearance}` / `{#dossier}` convention is the note format's, so the
|
|
57
57
|
// extraction is shared; which field a section lands in stays the system's.
|
|
58
58
|
export { extractAnchorSection, renderSection } from "./anchored-sections.mjs";
|
|
@@ -64,13 +64,12 @@ import { contentPackage } from "./content-package.mjs";
|
|
|
64
64
|
// types are actors at all. Looked up in the system's declared map, never
|
|
65
65
|
// inferred from the type itself.
|
|
66
66
|
import { mapsNoteType, noteTypesFor, referencedSubtype } from "./document-subtypes.mjs";
|
|
67
|
-
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
68
67
|
// An `items:` entry's `system:` overlay is merged verbatim, so it reaches the
|
|
69
68
|
// document by a path no field declaration sits on — including the
|
|
70
69
|
// fields the document is supposed to write for itself in play. `itemArt` is
|
|
71
70
|
// the same table the Item pass defaults from, so an entry that copies no
|
|
72
71
|
// template and names no art of its own carries what a note of its type would.
|
|
73
|
-
import { itemArt, itemFields } from "./item-registry.mjs";
|
|
72
|
+
import { itemArt, itemFields, itemTypes } from "./item-registry.mjs";
|
|
74
73
|
import { runtimeOnlyIn, runtimeOnlyMessage } from "./runtime-only-fields.mjs";
|
|
75
74
|
// A `model:` is an address, read by the same grammar every wikilink is,
|
|
76
75
|
// so an author writes one form and meets one set of messages.
|
|
@@ -326,7 +325,7 @@ export function loadItemsMap(itemsSourceDirs, foreignSourceDirs = [], system = n
|
|
|
326
325
|
// so a whole-package build cannot reach this. What can is a run
|
|
327
326
|
// restricted to this one pack, or a caller constructing the
|
|
328
327
|
// compiler itself — neither of which reordering a pack list fixes,
|
|
329
|
-
// so the message
|
|
328
|
+
// so the message does not suggest it.
|
|
330
329
|
throw new Error(
|
|
331
330
|
`Items source directory ${itemsSourceDir} does not exist — ` +
|
|
332
331
|
`an actor resolves its embedded items against the Item ` +
|
|
@@ -467,8 +466,7 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
467
466
|
const address = beingDefaultArt(fm);
|
|
468
467
|
const byTag = address ? this.artPathOf(address, "icon", "icon") : null;
|
|
469
468
|
const base = byTag ?? fallback;
|
|
470
|
-
//
|
|
471
|
-
// that writes `""` ships blank on purpose.
|
|
469
|
+
// A note that names no art takes the default.
|
|
472
470
|
const img = this.artPath(fm, "icon") ?? base;
|
|
473
471
|
return { img, token: this.artPath(fm, "tokenIcon") ?? img };
|
|
474
472
|
}
|
|
@@ -574,26 +572,20 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
574
572
|
// configuration, and a consumer may put them anywhere. Every
|
|
575
573
|
// Item pack, because a repository may ship more than one.
|
|
576
574
|
//
|
|
577
|
-
// **Optional, and empty is a legitimate package.**
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
// 2,512 beings whose embedded items address the `sohl` and `hm3`
|
|
584
|
-
// catalogues, and five affiliation notes of its own.
|
|
575
|
+
// **Optional, and empty is a legitimate package.** An Item pack is
|
|
576
|
+
// system-bound by construction — Foundry requires `system` on Item
|
|
577
|
+
// packs — so a deliberately system-agnostic module could satisfy a
|
|
578
|
+
// requirement to declare one only by naming a system. `harn-ensemble`
|
|
579
|
+
// is the case: its beings' embedded items address the `sohl` and `hm3`
|
|
580
|
+
// catalogues, and it has a few affiliation notes of its own.
|
|
585
581
|
//
|
|
586
|
-
// The
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
// condition actually cared about is checked where it can be reported
|
|
590
|
-
// precisely: {@link SystemActorCompiler#resolveEmbedded} already errors
|
|
591
|
-
// per unresolved `(type, shortcode)`, naming the actor. A package whose
|
|
582
|
+
// The condition that matters is checked where it can be reported
|
|
583
|
+
// precisely: {@link SystemActorCompiler#resolveEmbedded} errors per
|
|
584
|
+
// unresolved `(type, shortcode)`, naming the actor. A package whose
|
|
592
585
|
// actors embed nothing, or whose every address resolves against a
|
|
593
|
-
// dependency catalogue through `foreignSourceDirs`,
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
// pack is absent.
|
|
586
|
+
// dependency catalogue through `foreignSourceDirs`, compiles with no
|
|
587
|
+
// Item pack at all — and one that is genuinely missing an item fails,
|
|
588
|
+
// saying which item and which actor rather than which pack is absent.
|
|
597
589
|
Object.defineProperty(this, "itemsSourceDirs", {
|
|
598
590
|
value: Object.freeze([...itemsSourceDirs]),
|
|
599
591
|
writable: false,
|
|
@@ -679,7 +671,7 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
679
671
|
* subtype, or why the reference names none.
|
|
680
672
|
*/
|
|
681
673
|
embeddedSubtype(type) {
|
|
682
|
-
return referencedSubtype(this.documentSubtypes, type, "Item");
|
|
674
|
+
return referencedSubtype(this.documentSubtypes, type, "Item", { declared: itemTypes() });
|
|
683
675
|
}
|
|
684
676
|
|
|
685
677
|
/**
|
|
@@ -742,13 +734,13 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
742
734
|
const read = parseAddress(
|
|
743
735
|
model,
|
|
744
736
|
{ types, packages, package: contentPackage(), system: this.system },
|
|
745
|
-
{ declared: true
|
|
737
|
+
{ declared: true },
|
|
746
738
|
);
|
|
747
739
|
if (!read || read.reason) {
|
|
748
740
|
const why =
|
|
749
|
-
read?.reason === "not-lowercase" ?
|
|
750
|
-
|
|
751
|
-
"
|
|
741
|
+
read?.reason === "shortcode-not-lowercase" ?
|
|
742
|
+
`spells its shortcode \`${read.shortcode}\` with a capital — ` +
|
|
743
|
+
"a shortcode is lowercase"
|
|
752
744
|
: read?.reason === "unknown-type" ? "names no known content type"
|
|
753
745
|
: "is not an address — write `type-shortcode`, or " +
|
|
754
746
|
"`package-system-type-shortcode` for another package's item";
|
|
@@ -892,8 +884,7 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
892
884
|
// The two art rules an item note compiles under, applied to an entry
|
|
893
885
|
// that is one in every respect but where it is written: the address it
|
|
894
886
|
// names wins, the template it copies answers next, and the type's own
|
|
895
|
-
// default answers last.
|
|
896
|
-
// ships blank on purpose rather than collecting a default.
|
|
887
|
+
// default answers last.
|
|
897
888
|
merged.img =
|
|
898
889
|
this.artPath(overlay ?? {}, "icon") ??
|
|
899
890
|
merged.img ??
|
|
@@ -42,26 +42,13 @@
|
|
|
42
42
|
* The shape every address segment must match: **lowercase** ASCII letters and
|
|
43
43
|
* digits only.
|
|
44
44
|
*
|
|
45
|
-
* Case *was* deliberately unconstrained, on the reasoning that case has no
|
|
46
|
-
* bearing on the separator — which is true, and beside the point.
|
|
47
|
-
*
|
|
48
45
|
* **Two names that differ only in case are two names nobody can tell apart.** A
|
|
49
46
|
* shortcode is how a person names a thing when writing a reference —
|
|
50
47
|
* `model: weapongear-dgr`, `[[skill-melee|…]]` — and `Dgr` beside `dgr` is a
|
|
51
|
-
* distinction you cannot say out loud and can only see by looking twice.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `sohl-sohl-weapongear-clb` and its `_id` derived from that. The authored name
|
|
56
|
-
* and its address disagreed, and everything downstream keys on the address —
|
|
57
|
-
* which left two notes differing only in case sharing one address, one `_id` and
|
|
58
|
-
* one URL, with nothing to report it. It also forces two exceptions elsewhere:
|
|
59
|
-
* the shortcode has to be exempted from the lowercase rule pinned on every
|
|
60
|
-
* other segment, and its case has to be folded in the item catalogue because
|
|
61
|
-
* an address is lowercased when read.
|
|
62
|
-
*
|
|
63
|
-
* One case, one spelling, no exceptions. Every tree already complies but two,
|
|
64
|
-
* and nothing in any of them collides when folded.
|
|
48
|
+
* distinction you cannot say out loud and can only see by looking twice. So the
|
|
49
|
+
* shortcode is held to the same rule as every other segment: a note declaring
|
|
50
|
+
* `shortcode: Dgr` is refused, and so is a declared Address spelling one with a
|
|
51
|
+
* capital.
|
|
65
52
|
*
|
|
66
53
|
* @type {RegExp}
|
|
67
54
|
*/
|
|
@@ -307,7 +307,7 @@ function isMapping(value) {
|
|
|
307
307
|
* `data.seat` — and the note writes `seat:` one level inside it. The finding
|
|
308
308
|
* names that key for two reasons at once: it is the word on the line the
|
|
309
309
|
* diagnostic opens, and it is what {@link
|
|
310
|
-
* module:engine/
|
|
310
|
+
* module:engine/diagnostics.locateFrontmatterKey} matches when the authored
|
|
311
311
|
* value cannot be found in the fence, which a dotted path never would.
|
|
312
312
|
*
|
|
313
313
|
* @param {object} field - The declaration.
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
2
13
|
|
|
3
14
|
import { assertSystemSegment } from "./systems.mjs";
|
|
4
15
|
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
3
|
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
4
|
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
5
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
12
|
*/
|
|
7
13
|
|
package/engine/address.mjs
CHANGED
|
@@ -138,7 +138,7 @@ import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
|
138
138
|
// `ids.mjs` is a leaf with no local imports, and `pack-config.mjs` reaches only
|
|
139
139
|
// the configuration loader and the diagnostics writer — neither reads an
|
|
140
140
|
// address, so the grammar sits below everything that consumes it.
|
|
141
|
-
import {
|
|
141
|
+
import { ITEM_PACK, JOURNAL_TYPES, packForType } from "./ids.mjs";
|
|
142
142
|
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
143
143
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
144
144
|
|
|
@@ -167,7 +167,7 @@ export const CANONICAL_KEY_SEGMENTS = 4;
|
|
|
167
167
|
/** The accepted `doc<type>` input prefix for readable note links. */
|
|
168
168
|
export const ITEM_DOC_PREFIX = "doc";
|
|
169
169
|
|
|
170
|
-
/** Package, system and type are
|
|
170
|
+
/** Package, system and type are read lowercased; a capital in one is refused as `not-lowercase`. */
|
|
171
171
|
const norm = (s) => String(s).toLowerCase().trim();
|
|
172
172
|
|
|
173
173
|
/**
|
|
@@ -194,11 +194,16 @@ const norm = (s) => String(s).toLowerCase().trim();
|
|
|
194
194
|
* - `no-content-index` — the package named publishes no content index, so there
|
|
195
195
|
* is nothing to resolve the rest of the target against.
|
|
196
196
|
* - `not-lowercase` — a package, system or type segment carries a capital.
|
|
197
|
+
* - `shortcode-not-lowercase` — the Address spells its shortcode with a
|
|
198
|
+
* capital and is otherwise well formed. `shortcode` carries the segment as
|
|
199
|
+
* written, and `lowercase` the reading of the lowercase spelling: complete
|
|
200
|
+
* for a declared Address, and as partial as the written value otherwise.
|
|
197
201
|
* - `invalid-segment` — a declared Address contains a malformed segment.
|
|
198
202
|
* - `no-package` — a declared Address cannot supply its package.
|
|
199
203
|
* - `invalid-system` — a declared Address cannot supply a registered system.
|
|
200
204
|
*
|
|
201
|
-
* @typedef {{reason: string, package?: string
|
|
205
|
+
* @typedef {{reason: string, package?: string, shortcode?: string,
|
|
206
|
+
* lowercase?: AddressTuple}} AddressProblem
|
|
202
207
|
*/
|
|
203
208
|
|
|
204
209
|
/**
|
|
@@ -220,12 +225,9 @@ export function docEntryTypes() {
|
|
|
220
225
|
* `macro`; false for `doc`.
|
|
221
226
|
*/
|
|
222
227
|
export function hasDocEntry(type) {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
// before — this is the one lookup between an item compiling and its prose
|
|
227
|
-
// silently compiling into nothing.
|
|
228
|
-
return docEntryTypes().has(String(currentType(type)));
|
|
228
|
+
// The set is derived from the item registry's keys: the one lookup between
|
|
229
|
+
// an item compiling and its prose silently compiling into nothing.
|
|
230
|
+
return docEntryTypes().has(String(type));
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
/** The segment of a note's own document. */
|
|
@@ -294,32 +296,34 @@ export function resolveItemDocType(qualifier, types) {
|
|
|
294
296
|
* label already removed.
|
|
295
297
|
* @param {AddressDefaults} [defaults] - The position's defaults and the tree's
|
|
296
298
|
* vocabularies.
|
|
297
|
-
* @param {{declared?: boolean
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* completed tuple contains their canonical lowercase identity.
|
|
299
|
+
* @param {{declared?: boolean}} [options] - A declared Address is validated as
|
|
300
|
+
* written: every segment, the shortcode included, is lowercase, and the
|
|
301
|
+
* defaults must be complete. Package membership is a separate resolution
|
|
302
|
+
* question in this mode; prose recognition keeps its vocabulary checks.
|
|
302
303
|
* @returns {AddressTuple|AddressProblem} The complete Address, or why there is
|
|
303
304
|
* none.
|
|
304
305
|
*/
|
|
305
|
-
export function parseAddress(
|
|
306
|
-
written,
|
|
307
|
-
defaults = {},
|
|
308
|
-
{ declared = false, legacyShortcodeCase = false } = {},
|
|
309
|
-
) {
|
|
306
|
+
export function parseAddress(written, defaults = {}, { declared = false } = {}) {
|
|
310
307
|
if (isAddressTuple(written)) return written;
|
|
311
308
|
if (declared) {
|
|
312
309
|
if (typeof written !== "string") return { reason: "not-an-address" };
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
310
|
+
const separator = written.includes("/") ? "/" : ADDRESS_SEPARATOR;
|
|
311
|
+
const segments = written.split(separator);
|
|
312
|
+
const shortcode = segments.at(-1);
|
|
313
|
+
if (!segments.slice(0, -1).every(isAddressSegment)) return { reason: "invalid-segment" };
|
|
314
|
+
if (!isAddressSegment(shortcode)) {
|
|
315
|
+
if (!/[A-Z]/.test(shortcode) || !isAddressSegment(shortcode.toLowerCase()))
|
|
316
|
+
return { reason: "invalid-segment" };
|
|
317
|
+
const lowercase = parseAddress(
|
|
318
|
+
[...segments.slice(0, -1), shortcode.toLowerCase()].join(separator),
|
|
319
|
+
defaults,
|
|
320
|
+
{ declared },
|
|
321
|
+
);
|
|
322
|
+
// A value that is no Address even in lowercase is refused for that,
|
|
323
|
+
// the more basic fault, rather than for its case.
|
|
324
|
+
if (!isAddressTuple(lowercase)) return lowercase;
|
|
325
|
+
return { reason: "shortcode-not-lowercase", shortcode, lowercase };
|
|
326
|
+
}
|
|
323
327
|
}
|
|
324
328
|
const read = readWritten(written, defaults, declared);
|
|
325
329
|
if (read.reason) return read;
|
|
@@ -565,15 +569,24 @@ function readWritten(written, vocabulary, declared = false) {
|
|
|
565
569
|
const target = typeof written === "string" ? written : String(written ?? "");
|
|
566
570
|
if (!target) return { reason: "not-an-address" };
|
|
567
571
|
const read = readWrittenCased(target, vocabulary, declared);
|
|
568
|
-
if (
|
|
572
|
+
if (read.reason) return read;
|
|
573
|
+
if (qualifyingSegments(target).some((s) => /[A-Z]/.test(s))) {
|
|
569
574
|
return { reason: "not-lowercase" };
|
|
570
575
|
}
|
|
576
|
+
if (/[A-Z]/.test(read.shortcode)) {
|
|
577
|
+
return {
|
|
578
|
+
reason: "shortcode-not-lowercase",
|
|
579
|
+
shortcode: read.shortcode,
|
|
580
|
+
lowercase: { ...read, shortcode: read.shortcode.toLowerCase() },
|
|
581
|
+
};
|
|
582
|
+
}
|
|
571
583
|
return read;
|
|
572
584
|
}
|
|
573
585
|
|
|
574
586
|
/**
|
|
575
|
-
* The segments of a written value
|
|
576
|
-
* shortcode
|
|
587
|
+
* The package, system and type segments of a written value, which carry the
|
|
588
|
+
* `not-lowercase` finding. The shortcode is checked on its own, so its finding
|
|
589
|
+
* can name the segment and the lowercase spelling.
|
|
577
590
|
*
|
|
578
591
|
* @param {string} target - The value as authored.
|
|
579
592
|
* @returns {string[]} The package / system / type segments, as written.
|
|
@@ -666,7 +679,7 @@ function readWrittenCased(target, vocabulary, declared = false) {
|
|
|
666
679
|
* `null` when the segment names no known type, or the shortcode is empty.
|
|
667
680
|
*/
|
|
668
681
|
function readTypeAndCode(rawType, rawCode, types) {
|
|
669
|
-
const shortcode =
|
|
682
|
+
const shortcode = String(rawCode).trim();
|
|
670
683
|
if (!shortcode) return null;
|
|
671
684
|
|
|
672
685
|
const type = norm(rawType);
|
package/engine/art-fields.mjs
CHANGED
|
@@ -196,7 +196,8 @@ export function assetImageInfoByPathname(records = []) {
|
|
|
196
196
|
* slot's. Omitted, any asset type is taken, which is an embed's rule: it
|
|
197
197
|
* draws a file, and every asset type is a file.
|
|
198
198
|
* @returns {{record: {package: string, asset: {path: string}}, pathname: string}
|
|
199
|
-
* |{record: null, reason: string, type?: string
|
|
199
|
+
* |{record: null, reason: string, type?: string, shortcode?: string,
|
|
200
|
+
* declared?: string}} The asset and the pathname
|
|
200
201
|
* it is at, or a reason from
|
|
201
202
|
* {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS}, or `not-accepted`
|
|
202
203
|
* when `accepts` is given and the parsed type falls outside it.
|
|
@@ -214,6 +215,31 @@ export function readAssetAddress(index, value, defaultType, accepts) {
|
|
|
214
215
|
if (read.reason === "not-accepted") {
|
|
215
216
|
return { record: null, reason: "not-accepted", type: read.type };
|
|
216
217
|
}
|
|
218
|
+
if (read.reason === "shortcode-not-lowercase") {
|
|
219
|
+
// Refused, never lowercased; the lowercase spelling is looked up only
|
|
220
|
+
// to name it in the finding.
|
|
221
|
+
const written = String(value);
|
|
222
|
+
const lower = artTarget(
|
|
223
|
+
`${written.slice(0, -read.shortcode.length)}${read.lowercase.shortcode}`,
|
|
224
|
+
defaultType,
|
|
225
|
+
accepts,
|
|
226
|
+
{
|
|
227
|
+
system: ASSET_SYSTEM,
|
|
228
|
+
package: index?.contentPackage ?? index?.packageId,
|
|
229
|
+
types: index?.types,
|
|
230
|
+
packages: index?.packages,
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
const canonical = lower.reason ? undefined : renderAddress(lower);
|
|
234
|
+
const declared =
|
|
235
|
+
canonical && (index?.assets?.get(canonical) ?? index?.foreign?.get(canonical));
|
|
236
|
+
return {
|
|
237
|
+
record: null,
|
|
238
|
+
reason: read.reason,
|
|
239
|
+
shortcode: read.shortcode,
|
|
240
|
+
...(declared ? { declared: read.lowercase.shortcode } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
217
243
|
if (read.reason) return { record: null, reason: read.reason, type: read.type };
|
|
218
244
|
// **Asset types only**, where the position declares no narrower set. An
|
|
219
245
|
// address may name any type the vocabulary holds, and most of them name a
|
|
@@ -322,9 +348,9 @@ export function checkArtSlotSizes(records = [], { config, assetsBase = "" } = {}
|
|
|
322
348
|
* The pathname an art value names, in the form an authored one takes.
|
|
323
349
|
*
|
|
324
350
|
* Handing the result to {@link module:engine/helpers.resolveImg} is what puts an
|
|
325
|
-
* art address and a body image through one ownership rule.
|
|
326
|
-
*
|
|
327
|
-
*
|
|
351
|
+
* art address and a body image through one ownership rule. `null` and an
|
|
352
|
+
* absent key mean *no art named, apply the default*; an empty string names
|
|
353
|
+
* nothing and is unresolved like any other address nothing answers.
|
|
328
354
|
*
|
|
329
355
|
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
|
|
330
356
|
* @param {unknown} value - The value as authored.
|
|
@@ -339,7 +365,6 @@ export function checkArtSlotSizes(records = [], { config, assetsBase = "" } = {}
|
|
|
339
365
|
*/
|
|
340
366
|
export function artPathname(index, value, defaultType, accepts) {
|
|
341
367
|
if (value == null) return { pathname: null, resolved: true };
|
|
342
|
-
if (value === "") return { pathname: "", resolved: true };
|
|
343
368
|
const read = readAssetAddress(index, value, defaultType, accepts);
|
|
344
369
|
if (!read.record) {
|
|
345
370
|
return { pathname: null, resolved: false, reason: read.reason, type: read.type };
|