@heroiclands/package-build 23.1.0 → 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 +71 -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 +54 -6
- 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/engine/yaml-lint.mjs +7 -4
- 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 +83 -18
- 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/engine/yaml-lint.d.mts +5 -4
- 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 +12 -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/engine/docs-checks.mjs
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
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
|
+
*
|
|
2
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
12
|
*/
|
|
4
13
|
|
|
@@ -63,11 +63,10 @@
|
|
|
63
63
|
* @module
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
|
-
import { assertTypeNotRetired, currentType } from "./ids.mjs";
|
|
67
66
|
import { NO_SYSTEM } from "./systems.mjs";
|
|
68
67
|
|
|
69
68
|
export { NO_SYSTEM };
|
|
70
|
-
import { locateFrontmatterKey } from "./
|
|
69
|
+
import { locateFrontmatterKey } from "./diagnostics.mjs";
|
|
71
70
|
|
|
72
71
|
/**
|
|
73
72
|
* One row of a system's map: what a note of this type becomes.
|
|
@@ -201,12 +200,9 @@ function frozenRow(system, noteType, row) {
|
|
|
201
200
|
/**
|
|
202
201
|
* The row a system declares for a note type, or nothing.
|
|
203
202
|
*
|
|
204
|
-
* The **one** place a map is indexed
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* {@link systemOf} all arrive here. A note still spelling `armorgear` finds the
|
|
208
|
-
* `armor` row, compiles into the `armorgear` document it always did, and keeps
|
|
209
|
-
* the address it publishes at; only the *report* tells it to move.
|
|
203
|
+
* The **one** place a map is indexed — {@link documentSubtype},
|
|
204
|
+
* {@link mapsNoteType}, {@link noteTypesFor}'s consumers and {@link systemOf}
|
|
205
|
+
* all arrive here.
|
|
210
206
|
*
|
|
211
207
|
* @param {DocumentSubtypeMap} map - The system's map.
|
|
212
208
|
* @param {string|undefined} noteType - The note's declared `type`.
|
|
@@ -215,7 +211,7 @@ function frozenRow(system, noteType, row) {
|
|
|
215
211
|
*/
|
|
216
212
|
export function subtypeRow(map, noteType) {
|
|
217
213
|
if (!noteType || typeof noteType !== "string") return undefined;
|
|
218
|
-
return map?.types?.[
|
|
214
|
+
return map?.types?.[noteType];
|
|
219
215
|
}
|
|
220
216
|
|
|
221
217
|
/**
|
|
@@ -315,6 +311,29 @@ export function documentSubtype(map, noteType, fm, { file, absPath } = {}) {
|
|
|
315
311
|
* @property {string} [problem] - Why the reference names no document subtype.
|
|
316
312
|
*/
|
|
317
313
|
|
|
314
|
+
/** Every subtype a map's rows produce, per map. @type {WeakMap<object, Set<string>>} */
|
|
315
|
+
const PRODUCED = new WeakMap();
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The document subtypes the rows of a map produce.
|
|
319
|
+
*
|
|
320
|
+
* @param {DocumentSubtypeMap} map - The system's map.
|
|
321
|
+
* @returns {Set<string>} Each one-to-one row's subtype and every subtype a
|
|
322
|
+
* one-to-many row permits.
|
|
323
|
+
*/
|
|
324
|
+
function producedSubtypes(map) {
|
|
325
|
+
let produced = PRODUCED.get(map);
|
|
326
|
+
if (!produced) {
|
|
327
|
+
produced = new Set();
|
|
328
|
+
for (const row of Object.values(map?.types ?? {})) {
|
|
329
|
+
if (row.subType) produced.add(row.subType);
|
|
330
|
+
for (const subType of row.subTypes ?? []) produced.add(subType);
|
|
331
|
+
}
|
|
332
|
+
PRODUCED.set(map, produced);
|
|
333
|
+
}
|
|
334
|
+
return produced;
|
|
335
|
+
}
|
|
336
|
+
|
|
318
337
|
/**
|
|
319
338
|
* The document subtype a `(type, shortcode)` **reference** addresses.
|
|
320
339
|
*
|
|
@@ -354,41 +373,35 @@ export function documentSubtype(map, noteType, fm, { file, absPath } = {}) {
|
|
|
354
373
|
* only the row has no block to read a discriminator from, so nothing here
|
|
355
374
|
* can choose, and choosing anyway would be right about half the time.
|
|
356
375
|
*
|
|
357
|
-
* A
|
|
358
|
-
*
|
|
359
|
-
* a document
|
|
360
|
-
* retirement exists to stop.
|
|
361
|
-
*
|
|
362
|
-
* A **renamed** spelling is the opposite case and resolves normally, through
|
|
363
|
-
* the same {@link currentType} every other lookup goes through. It has to
|
|
364
|
-
* be this side as much as the note's own `type:`: the overwhelming majority of
|
|
365
|
-
* the 31,000 occurrences of the old names are `(type, shortcode)` references
|
|
366
|
-
* inside a being's `items:` list, so a window that resolved notes but not
|
|
367
|
-
* references would silently drop 30,000 embedded items rather than compile them
|
|
368
|
-
* unchanged.
|
|
376
|
+
* A type that no row names, that no row produces as a subtype and that the
|
|
377
|
+
* consumer does not declare is refused: stamping the authored spelling on it
|
|
378
|
+
* would address a document that cannot exist.
|
|
369
379
|
*
|
|
370
380
|
* @param {DocumentSubtypeMap} map - The system's map.
|
|
371
381
|
* @param {string|undefined} noteType - The type the reference names.
|
|
372
382
|
* @param {string} document - The Foundry document class the reference must
|
|
373
383
|
* address — `"Item"` for a being's embedded items.
|
|
384
|
+
* @param {object} [options] - Options.
|
|
385
|
+
* @param {ReadonlySet<string>} [options.declared] - The item types the
|
|
386
|
+
* consuming repository declares itself, which the map does not name.
|
|
374
387
|
* @returns {ReferencedSubtype} The subtype, or why there is none.
|
|
375
388
|
*/
|
|
376
|
-
export function referencedSubtype(map, noteType, document) {
|
|
389
|
+
export function referencedSubtype(map, noteType, document, { declared } = {}) {
|
|
377
390
|
if (!noteType || typeof noteType !== "string") {
|
|
378
391
|
return { problem: "the reference names no type" };
|
|
379
392
|
}
|
|
380
|
-
try {
|
|
381
|
-
assertTypeNotRetired(noteType);
|
|
382
|
-
} catch (err) {
|
|
383
|
-
return { problem: /** @type {Error} */ (err).message };
|
|
384
|
-
}
|
|
385
393
|
|
|
386
394
|
const row = subtypeRow(map, noteType);
|
|
387
|
-
// No row: the type is the consumer's own,
|
|
388
|
-
// the note type. See the note on the unmapped
|
|
389
|
-
// *authored
|
|
390
|
-
// with `fm.type` verbatim, and the two answers have to agree.
|
|
391
|
-
if (!row)
|
|
395
|
+
// No row: the type is the consumer's own, or a subtype a row produces, and
|
|
396
|
+
// its document is stamped with the note type. See the note on the unmapped
|
|
397
|
+
// fallback above. Returned as *authored* — the Item pass stamps an unmapped
|
|
398
|
+
// document with `fm.type` verbatim, and the two answers have to agree.
|
|
399
|
+
if (!row) {
|
|
400
|
+
if (declared?.has(noteType) || producedSubtypes(map).has(noteType)) {
|
|
401
|
+
return { subType: noteType };
|
|
402
|
+
}
|
|
403
|
+
return { problem: `"${noteType}" is not a type any registry declares` };
|
|
404
|
+
}
|
|
392
405
|
|
|
393
406
|
if (row.document !== document) {
|
|
394
407
|
return {
|
|
@@ -406,8 +419,8 @@ export function referencedSubtype(map, noteType, document) {
|
|
|
406
419
|
// reference does by never matching this row's key at all. Only a
|
|
407
420
|
// reference that names the row without naming a subtype is genuinely
|
|
408
421
|
// undecidable.
|
|
409
|
-
if (permitted.includes(
|
|
410
|
-
return { subType:
|
|
422
|
+
if (permitted.includes(noteType)) {
|
|
423
|
+
return { subType: noteType };
|
|
411
424
|
}
|
|
412
425
|
return {
|
|
413
426
|
problem:
|
package/engine/event-views.mjs
CHANGED
|
@@ -59,11 +59,14 @@ const BY_DATE = "ORDER BY e.whenSort NULLS LAST, e.address";
|
|
|
59
59
|
* index record, and the `query` it writes for a note — given the note's
|
|
60
60
|
* Address in each form an event may name it by, and the shape of the corpus —
|
|
61
61
|
* or `null` where the corpus states nothing the view could select. A view's
|
|
62
|
-
* `own` query, where it has one, is what decides whether the view has rows
|
|
62
|
+
* `own` query, where it has one, is what decides whether the view has rows;
|
|
63
|
+
* a view with `owners` answers that for many notes in one query, given each
|
|
64
|
+
* note's Address forms.
|
|
63
65
|
*
|
|
64
66
|
* @type {ReadonlyArray<Readonly<{slug: string, heading: string,
|
|
65
67
|
* applies: (record: object) => boolean,
|
|
66
|
-
* query: (self: string[], shape: CorpusShape) => {fence: string, own?: string}|null
|
|
68
|
+
* query: (self: string[], shape: CorpusShape) => {fence: string, own?: string}|null,
|
|
69
|
+
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
|
|
67
70
|
*/
|
|
68
71
|
export const EVENT_VIEWS = Object.freeze([
|
|
69
72
|
Object.freeze({
|
|
@@ -71,6 +74,7 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
71
74
|
heading: "Chronology",
|
|
72
75
|
applies: (record) => record.type === "place",
|
|
73
76
|
query: chronologyQuery,
|
|
77
|
+
owners: chronologyOwners,
|
|
74
78
|
}),
|
|
75
79
|
Object.freeze({
|
|
76
80
|
slug: "events",
|
|
@@ -79,6 +83,8 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
79
83
|
record.type === "being" ||
|
|
80
84
|
record.type === "affiliation" ||
|
|
81
85
|
(record.type === "lore" && record.subType === "culture"),
|
|
86
|
+
owners: (db, selves, shape) =>
|
|
87
|
+
namedOwners(db, selves, "SELECT unnest(who).ref AS ref FROM events", shape.who),
|
|
82
88
|
query: (self, shape) => {
|
|
83
89
|
if (!shape.who) return null;
|
|
84
90
|
const named = `list_filter(e.who, w -> w.ref IN (${literals(self)}))`;
|
|
@@ -102,6 +108,13 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
102
108
|
record.type === "affiliation" ||
|
|
103
109
|
record.type === "place" ||
|
|
104
110
|
(record.type === "lore" && record.subType === "culture"),
|
|
111
|
+
owners: (db, selves, shape) =>
|
|
112
|
+
namedOwners(
|
|
113
|
+
db,
|
|
114
|
+
selves,
|
|
115
|
+
'SELECT account."by" AS ref FROM (SELECT unnest(accounts) AS account FROM events)',
|
|
116
|
+
shape.accounts,
|
|
117
|
+
),
|
|
105
118
|
query: (self, shape) => {
|
|
106
119
|
if (!shape.accounts) return null;
|
|
107
120
|
return {
|
|
@@ -122,6 +135,14 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
122
135
|
slug: "followed",
|
|
123
136
|
heading: "What followed",
|
|
124
137
|
applies: (record) => Array.isArray(record.data?.events) && record.data.events.length > 0,
|
|
138
|
+
owners: (db, selves, shape) =>
|
|
139
|
+
namedOwners(
|
|
140
|
+
db,
|
|
141
|
+
selves,
|
|
142
|
+
"SELECT split_part(edge.event, '#', 1) AS ref " +
|
|
143
|
+
"FROM (SELECT unnest(follows) AS edge FROM events)",
|
|
144
|
+
shape.follows,
|
|
145
|
+
),
|
|
125
146
|
query: (self, shape) => {
|
|
126
147
|
if (!shape.follows) return null;
|
|
127
148
|
const edges =
|
|
@@ -153,6 +174,96 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
153
174
|
* @property {boolean} depth - Some event states `depth`.
|
|
154
175
|
*/
|
|
155
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Which notes an event names, asked of every note in one query rather than one
|
|
179
|
+
* query per note: the references the events state are listed once and joined to
|
|
180
|
+
* every note's Address forms.
|
|
181
|
+
*
|
|
182
|
+
* @param {object} db - From {@link module:engine/sql-tables.openNotesDatabase}.
|
|
183
|
+
* @param {string[][]} selves - Each note's Address, in each form.
|
|
184
|
+
* @param {string} refs - A query selecting one `ref` per reference an event
|
|
185
|
+
* states.
|
|
186
|
+
* @param {boolean} stated - Whether the corpus states the field `refs` reads.
|
|
187
|
+
* @returns {Promise<Set<string>>} The first form of each note some event names.
|
|
188
|
+
*/
|
|
189
|
+
async function namedOwners(db, selves, refs, stated) {
|
|
190
|
+
if (!stated) return new Set();
|
|
191
|
+
const values = selves
|
|
192
|
+
.flatMap((forms) => forms.map((form) => `(${literal(forms[0])}, ${literal(form)})`))
|
|
193
|
+
.join(", ");
|
|
194
|
+
const { rows } = await db.query(
|
|
195
|
+
[
|
|
196
|
+
`WITH selves(root, form) AS (VALUES ${values}),`,
|
|
197
|
+
`named AS (${refs})`,
|
|
198
|
+
"SELECT DISTINCT selves.root AS root",
|
|
199
|
+
"FROM named JOIN selves ON selves.form = named.ref",
|
|
200
|
+
].join("\n"),
|
|
201
|
+
);
|
|
202
|
+
return new Set(rows.map((row) => String(row.root)));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The `reach` clauses felt in the places `here` holds. */
|
|
206
|
+
function chronologyFelt() {
|
|
207
|
+
return 'list_filter(e."where".reach, r -> list_contains(here.places, r.place))';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The conditions under which an event is one of a place's own rows: it
|
|
212
|
+
* happened at the place or below it, or it is felt there.
|
|
213
|
+
*
|
|
214
|
+
* @param {CorpusShape} shape - What the corpus states.
|
|
215
|
+
* @returns {string[]} The alternatives, over `e` and `here.places`.
|
|
216
|
+
*/
|
|
217
|
+
function chronologyOwnRows(shape) {
|
|
218
|
+
return [
|
|
219
|
+
...(shape.locus ?
|
|
220
|
+
['len(list_filter(e."where".locus, l -> list_contains(here.places, l))) > 0']
|
|
221
|
+
: []),
|
|
222
|
+
...(shape.reach ? [`len(${chronologyFelt()}) > 0`] : []),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Which places have a chronology of their own rows, asked of every place in one
|
|
228
|
+
* query rather than one query per place.
|
|
229
|
+
*
|
|
230
|
+
* The places below each root are walked together, so the walk over `entries`
|
|
231
|
+
* is paid once for the whole corpus instead of once per place.
|
|
232
|
+
*
|
|
233
|
+
* @param {object} db - From {@link module:engine/sql-tables.openNotesDatabase}.
|
|
234
|
+
* @param {string[][]} selves - Each place's Address, in each form; the `note`
|
|
235
|
+
* system's is the first.
|
|
236
|
+
* @param {CorpusShape} shape - What the corpus states.
|
|
237
|
+
* @returns {Promise<Set<string>>} The roots with at least one own row.
|
|
238
|
+
*/
|
|
239
|
+
async function chronologyOwners(db, selves, shape) {
|
|
240
|
+
const values = [...new Set(selves.map((self) => self[0]))]
|
|
241
|
+
.map((root) => `(${literal(root)})`)
|
|
242
|
+
.join(", ");
|
|
243
|
+
const below =
|
|
244
|
+
shape.parents ?
|
|
245
|
+
[
|
|
246
|
+
"WITH RECURSIVE below(root, place) AS (",
|
|
247
|
+
` SELECT r, r FROM (VALUES ${values}) AS t(r)`,
|
|
248
|
+
" UNION",
|
|
249
|
+
" SELECT b.root, lower(p.package || '-note-place-' || p.shortcode)",
|
|
250
|
+
" FROM entries p JOIN below b ON list_contains(p.data.parents, b.place)",
|
|
251
|
+
" WHERE p.type = 'place'",
|
|
252
|
+
"),",
|
|
253
|
+
]
|
|
254
|
+
: [`WITH RECURSIVE below(root, place) AS (SELECT r, r FROM (VALUES ${values}) AS t(r)),`];
|
|
255
|
+
const { rows } = await db.query(
|
|
256
|
+
[
|
|
257
|
+
...below,
|
|
258
|
+
"here AS (SELECT root, list(place) AS places FROM below GROUP BY root)",
|
|
259
|
+
"SELECT DISTINCT here.root AS root",
|
|
260
|
+
"FROM events e, here",
|
|
261
|
+
`WHERE ${chronologyOwnRows(shape).join(" OR ")}`,
|
|
262
|
+
].join("\n"),
|
|
263
|
+
);
|
|
264
|
+
return new Set(rows.map((row) => String(row.root)));
|
|
265
|
+
}
|
|
266
|
+
|
|
156
267
|
/**
|
|
157
268
|
* A place's chronology: every event that happened at the place or anywhere
|
|
158
269
|
* below it in `parents`, every event felt there — with the clause saying how —
|
|
@@ -181,13 +292,8 @@ function chronologyQuery(self, shape) {
|
|
|
181
292
|
"),",
|
|
182
293
|
]
|
|
183
294
|
: [`WITH RECURSIVE below(place) AS (SELECT ${literal(self[0])}),`];
|
|
184
|
-
const felt =
|
|
185
|
-
const ownRows =
|
|
186
|
-
...(shape.locus ?
|
|
187
|
-
['len(list_filter(e."where".locus, l -> list_contains(here.places, l))) > 0']
|
|
188
|
-
: []),
|
|
189
|
-
...(shape.reach ? [`len(${felt}) > 0`] : []),
|
|
190
|
-
];
|
|
295
|
+
const felt = chronologyFelt();
|
|
296
|
+
const ownRows = chronologyOwnRows(shape);
|
|
191
297
|
const head = [
|
|
192
298
|
...below,
|
|
193
299
|
"here AS (SELECT list(place) AS places FROM below)",
|
|
@@ -241,7 +347,7 @@ async function corpusShape(db) {
|
|
|
241
347
|
* @param {object} record - The note's index record.
|
|
242
348
|
* @returns {string[]} The forms, the `note` one first.
|
|
243
349
|
*/
|
|
244
|
-
function selfForms(record) {
|
|
350
|
+
export function selfForms(record) {
|
|
245
351
|
const tuple = {
|
|
246
352
|
package: String(record.package),
|
|
247
353
|
type: String(record.type),
|
|
@@ -285,6 +391,21 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
285
391
|
if (!all.some((record) => Array.isArray(record?.data?.events) && record.data.events.length))
|
|
286
392
|
return out;
|
|
287
393
|
const shape = await corpusShape(db);
|
|
394
|
+
// A view that can say which notes have rows in one query is asked once, over
|
|
395
|
+
// every note it will be given, and the per-note count is skipped.
|
|
396
|
+
const owned = new Map();
|
|
397
|
+
for (const view of EVENT_VIEWS) {
|
|
398
|
+
if (!view.owners) continue;
|
|
399
|
+
const selves = [];
|
|
400
|
+
for (const record of all) {
|
|
401
|
+
if (!record?.address || record.documents || !record.shortcode) continue;
|
|
402
|
+
if (only !== undefined && fileOf(record) !== only) continue;
|
|
403
|
+
if (!view.applies(record) || authoredSection(record, view.slug)) continue;
|
|
404
|
+
const self = selfForms(record);
|
|
405
|
+
if (view.query(self, shape)) selves.push(self);
|
|
406
|
+
}
|
|
407
|
+
if (selves.length) owned.set(view.slug, await view.owners(db, selves, shape));
|
|
408
|
+
}
|
|
288
409
|
for (const record of all) {
|
|
289
410
|
if (!record?.address || record.documents || !record.shortcode) continue;
|
|
290
411
|
if (only !== undefined && fileOf(record) !== only) continue;
|
|
@@ -294,10 +415,14 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
294
415
|
if (!view.applies(record) || authoredSection(record, view.slug)) continue;
|
|
295
416
|
const query = view.query(self, shape);
|
|
296
417
|
if (!query) continue;
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
418
|
+
const has =
|
|
419
|
+
owned.has(view.slug) ?
|
|
420
|
+
owned.get(view.slug).has(self[0])
|
|
421
|
+
: Number(
|
|
422
|
+
(await db.query(`SELECT count(*) AS n FROM (${query.own ?? query.fence})`))
|
|
423
|
+
.rows[0]?.n ?? 0,
|
|
424
|
+
) > 0;
|
|
425
|
+
if (has) sections.set(view.slug, section(view, query.fence));
|
|
301
426
|
}
|
|
302
427
|
if (sections.size) out.set(fileOf(record), sections);
|
|
303
428
|
}
|
|
@@ -1,10 +1,21 @@
|
|
|
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
|
+
*
|
|
2
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
12
|
*/
|
|
4
13
|
|
|
5
14
|
/** Preserve an exported Foundry Scene while binding marked pins to note pages. */
|
|
6
15
|
|
|
7
16
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
17
|
+
import { dataFields } from "./note-vocabulary.mjs";
|
|
18
|
+
import { isAddressTuple, renderAddress } from "./address.mjs";
|
|
8
19
|
|
|
9
20
|
const EMBEDDED = Object.freeze([
|
|
10
21
|
"drawings",
|
|
@@ -96,38 +107,90 @@ function fixupTarget(scene, path) {
|
|
|
96
107
|
return { parent: current, key: last.key };
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Every fixup a map note states, each with the key it was authored at.
|
|
112
|
+
*
|
|
113
|
+
* Two sources, applied by one mechanism. `data.fixup` names its Scene paths
|
|
114
|
+
* outright. A map field declaring a `sceneProperty` — `thumbnail`, written into
|
|
115
|
+
* `.thumb` — names the Scene property in the vocabulary instead, so the note
|
|
116
|
+
* states only the Address and the compile adds the entry. A declared field and
|
|
117
|
+
* an authored entry writing one property is refused: either would silently
|
|
118
|
+
* overwrite the other.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} fm - The map note's frontmatter.
|
|
121
|
+
* @returns {Array<{label: string, fixup: unknown}>} The fixups, authored first.
|
|
122
|
+
* @throws {Error} When `data.fixup` is not a list, or two fixups write one path.
|
|
123
|
+
*/
|
|
124
|
+
function noteFixups(fm) {
|
|
125
|
+
const authored = fm.data?.fixup;
|
|
126
|
+
if (authored !== undefined && !Array.isArray(authored)) {
|
|
127
|
+
throw new Error("`data.fixup` must be an array");
|
|
128
|
+
}
|
|
129
|
+
const out = (authored ?? []).map((fixup, index) => ({ label: `data.fixup[${index}]`, fixup }));
|
|
130
|
+
for (const field of dataFields("map") ?? []) {
|
|
131
|
+
if (!field.sceneProperty) continue;
|
|
132
|
+
const value = fm.data?.[field.name];
|
|
133
|
+
if (value === undefined || value === null || value === "") continue;
|
|
134
|
+
const label = `data.${field.name}`;
|
|
135
|
+
const clash = out.find(({ fixup }) => fixup?.path === field.sceneProperty);
|
|
136
|
+
if (clash) {
|
|
112
137
|
throw new Error(
|
|
113
|
-
|
|
138
|
+
`\`${label}\` and \`${clash.label}\` both write \`${field.sceneProperty}\`; ` +
|
|
139
|
+
`keep one of them`,
|
|
114
140
|
);
|
|
115
141
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
out.push({
|
|
143
|
+
label,
|
|
144
|
+
fixup: {
|
|
145
|
+
path: field.sceneProperty,
|
|
146
|
+
type: "address",
|
|
147
|
+
value: isAddressTuple(value) ? renderAddress(value) : value,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Apply a map note's asset address replacements to its exported Scene. */
|
|
155
|
+
function applyFixups(scene, fixups, resolveAddress) {
|
|
156
|
+
for (const { label, fixup } of fixups) {
|
|
157
|
+
try {
|
|
158
|
+
applyFixup(scene, label, fixup, resolveAddress);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// An entry the compile added says which key the note wrote, since
|
|
161
|
+
// the path it failed on appears nowhere in the note.
|
|
162
|
+
if (label.startsWith("data.fixup[")) throw error;
|
|
163
|
+
throw new Error(`\`${label}\`: ${error.message}`, { cause: error });
|
|
126
164
|
}
|
|
127
|
-
parent[key] = resolved;
|
|
128
165
|
}
|
|
129
166
|
}
|
|
130
167
|
|
|
168
|
+
/** Resolve one fixup's Address and write its path into the Scene. */
|
|
169
|
+
function applyFixup(scene, label, fixup, resolveAddress) {
|
|
170
|
+
if (
|
|
171
|
+
!fixup ||
|
|
172
|
+
typeof fixup !== "object" ||
|
|
173
|
+
Array.isArray(fixup) ||
|
|
174
|
+
fixup.type !== "address" ||
|
|
175
|
+
typeof fixup.value !== "string" ||
|
|
176
|
+
!fixup.value
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(`${label} needs path, type: address, and an asset address value`);
|
|
179
|
+
}
|
|
180
|
+
const { parent, key } = fixupTarget(scene, fixup.path);
|
|
181
|
+
if (typeof parent[key] !== "string" && parent[key] !== null) {
|
|
182
|
+
throw new Error(`Scene fixup path "${fixup.path}" must name a string asset field`);
|
|
183
|
+
}
|
|
184
|
+
if (typeof resolveAddress !== "function") {
|
|
185
|
+
throw new Error("Scene address fixups need an asset resolver");
|
|
186
|
+
}
|
|
187
|
+
const resolved = resolveAddress(fixup.value);
|
|
188
|
+
if (typeof resolved !== "string" || !resolved) {
|
|
189
|
+
throw new Error(`Scene fixup address "${fixup.value}" does not resolve to an asset`);
|
|
190
|
+
}
|
|
191
|
+
parent[key] = resolved;
|
|
192
|
+
}
|
|
193
|
+
|
|
131
194
|
/**
|
|
132
195
|
* Add a compendium key to every embedded document that carries an `_id`.
|
|
133
196
|
*
|
|
@@ -156,8 +219,9 @@ function keyDocuments(docs, collection, parentIds, sceneId) {
|
|
|
156
219
|
* Compile a Foundry Scene export into a pack document.
|
|
157
220
|
*
|
|
158
221
|
* The Scene passes through as exported. Only the note-derived Scene ID,
|
|
159
|
-
* compendium metadata, `data.fixup` and
|
|
160
|
-
*
|
|
222
|
+
* compendium metadata, the fixups — `data.fixup` and the map fields that
|
|
223
|
+
* declare a Scene property, such as `data.thumbnail` — and pins marked
|
|
224
|
+
* `#anchor` change; nothing else inside the Scene is read or checked.
|
|
161
225
|
*
|
|
162
226
|
* @param {object} fm - The map note's frontmatter.
|
|
163
227
|
* @param {string} markdown - Its converted Markdown body.
|
|
@@ -175,7 +239,7 @@ export function buildExportedScene(fm, markdown, { journalEntryId, stats, resolv
|
|
|
175
239
|
throw new Error("a map note needs a derived 16-character Scene ID");
|
|
176
240
|
}
|
|
177
241
|
const scene = structuredClone(source);
|
|
178
|
-
applyFixups(scene, fm
|
|
242
|
+
applyFixups(scene, noteFixups(fm), resolveAddress);
|
|
179
243
|
scene._id = fm.id;
|
|
180
244
|
scene._key = `!scenes!${fm.id}`;
|
|
181
245
|
if (stats) scene._stats = stats;
|
|
@@ -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
|
/**
|
|
4
15
|
* A name an id, a class or an attribute key may carry.
|