@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/engine/content-index.mjs
CHANGED
|
@@ -86,7 +86,12 @@
|
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
88
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
89
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
formatDiagnostic,
|
|
91
|
+
locateFrontmatterKey,
|
|
92
|
+
positionOfFrontmatterPath,
|
|
93
|
+
positionOfYamlPath,
|
|
94
|
+
} from "./diagnostics.mjs";
|
|
90
95
|
import {
|
|
91
96
|
decodeNoteAddresses,
|
|
92
97
|
decodeIndexAddresses,
|
|
@@ -100,11 +105,11 @@ import path from "node:path";
|
|
|
100
105
|
|
|
101
106
|
import unidecode from "unidecode";
|
|
102
107
|
|
|
103
|
-
import { metadataFileName } from "./metadata-index.mjs";
|
|
108
|
+
import { loadForeignIndexes, metadataFileName } from "./metadata-index.mjs";
|
|
104
109
|
import { collectAssetRecords } from "./asset-index.mjs";
|
|
105
110
|
import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
106
111
|
import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
107
|
-
import { ownDocumentSystem, readCanonicalKey } from "./address.mjs";
|
|
112
|
+
import { isAddressTuple, ownDocumentSystem, readCanonicalKey, renderAddress } from "./address.mjs";
|
|
108
113
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
109
114
|
import { assetAddressIndex } from "./art-fields.mjs";
|
|
110
115
|
import { embedRole } from "./content-embeds.mjs";
|
|
@@ -131,10 +136,9 @@ import { walkMarkdownTree } from "./helpers.mjs";
|
|
|
131
136
|
// SQL view that derives the ladder from the absence this writes.
|
|
132
137
|
import { isStubNote } from "./note-state.mjs";
|
|
133
138
|
import { resolveNoteId } from "./note-ids.mjs";
|
|
134
|
-
// The
|
|
135
|
-
//
|
|
139
|
+
// The `package:` refusal, so a note declaring one is reported where it is
|
|
140
|
+
// rather than as a bare abort.
|
|
136
141
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
137
|
-
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
138
142
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
139
143
|
// The record accessors, which live apart so that a module the compilers load
|
|
140
144
|
// can read a record without importing this one and closing a cycle.
|
|
@@ -392,11 +396,11 @@ function foundryBlock(entry, system) {
|
|
|
392
396
|
* carries `file` and a `position`, and a pass that collects rather than throws
|
|
393
397
|
* can emit `file:line:column: error: …` like any other finding.
|
|
394
398
|
*
|
|
395
|
-
* **`package:` keeps its own words.** It is not a name collision but a
|
|
396
|
-
*
|
|
397
|
-
* what {@link module:engine/note-package.assertNoDeclaredPackage}
|
|
398
|
-
*
|
|
399
|
-
*
|
|
399
|
+
* **`package:` keeps its own words.** It is not a name collision but a field
|
|
400
|
+
* the configuration owns, and the correction is to delete it, not to rename
|
|
401
|
+
* it — which is what {@link module:engine/note-package.assertNoDeclaredPackage}
|
|
402
|
+
* says. Deferring to it means one message for one mistake rather than two that
|
|
403
|
+
* contradict each other about the fix.
|
|
400
404
|
*
|
|
401
405
|
* @param {object} frontmatter - The note's parsed frontmatter.
|
|
402
406
|
* @param {string} relPath - The note's path within the tree, for the message.
|
|
@@ -486,9 +490,15 @@ export function buildIndexRecord({
|
|
|
486
490
|
const yamlText = fs
|
|
487
491
|
.readFileSync(absPath, "utf8")
|
|
488
492
|
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
const locate = (/** @type {any} */ found) => {
|
|
494
|
+
const position = positionOfYamlPath(yamlText, found.keyPath, {
|
|
495
|
+
key: found.addressKey,
|
|
496
|
+
});
|
|
497
|
+
if (position.line) position.line++;
|
|
498
|
+
found.position = position;
|
|
499
|
+
};
|
|
500
|
+
locate(error);
|
|
501
|
+
for (const found of error.findings ?? []) if (found !== error) locate(found);
|
|
492
502
|
}
|
|
493
503
|
throw error;
|
|
494
504
|
}
|
|
@@ -683,6 +693,302 @@ export function indexRecordsForNote({
|
|
|
683
693
|
return records;
|
|
684
694
|
}
|
|
685
695
|
|
|
696
|
+
/** The `lore` subType that is a culture, and so is its own. */
|
|
697
|
+
const CULTURE_SUBTYPE = "culture";
|
|
698
|
+
|
|
699
|
+
/** The `doc` subType whose culture is its own value. */
|
|
700
|
+
const SETTING_GUIDE_SUBTYPE = "settingguide";
|
|
701
|
+
|
|
702
|
+
/** The `affiliation` subType that also takes the culture of the places it holds. */
|
|
703
|
+
const POLITY_SUBTYPE = "polity";
|
|
704
|
+
|
|
705
|
+
/** The types whose culture is their own value and nothing else. */
|
|
706
|
+
const OWN_CULTURE_TYPES = new Set(["lore", "being", "doc"]);
|
|
707
|
+
|
|
708
|
+
/** The types that inherit a culture through `data.parents`. */
|
|
709
|
+
const INHERITING_TYPES = new Set(["place", "affiliation"]);
|
|
710
|
+
|
|
711
|
+
/** A resolution that yields no culture. */
|
|
712
|
+
const NO_CULTURE = Object.freeze({ culture: undefined, errored: false });
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* The canonical key an Address names, without any anchor.
|
|
716
|
+
*
|
|
717
|
+
* @param {unknown} value - A tuple, an {@link AddressLink}, or anything else.
|
|
718
|
+
* @returns {string|undefined} The key, or `undefined` for a value that is not a
|
|
719
|
+
* decoded Address.
|
|
720
|
+
*/
|
|
721
|
+
function addressKey(value) {
|
|
722
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
723
|
+
return isAddressTuple(tuple) ? renderAddress(tuple) : undefined;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The culture Address a value states, as a tuple.
|
|
728
|
+
*
|
|
729
|
+
* @param {unknown} value - A `data.culture` value.
|
|
730
|
+
* @returns {object|undefined} The tuple, or `undefined` when the value is not a
|
|
731
|
+
* decoded Address.
|
|
732
|
+
*/
|
|
733
|
+
function cultureTuple(value) {
|
|
734
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
735
|
+
return isAddressTuple(tuple) ? tuple : undefined;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Write each note's resolved culture into its record, as `data.culture`.
|
|
740
|
+
*
|
|
741
|
+
* A note's culture is:
|
|
742
|
+
*
|
|
743
|
+
* - its own `data.culture`, when it states one;
|
|
744
|
+
* - for a `lore` note of `subType: culture`, its own canonical Address;
|
|
745
|
+
* - for a `place` or an `affiliation`, else the resolved cultures of its
|
|
746
|
+
* `data.parents`;
|
|
747
|
+
* - for a `polity` affiliation, else the resolved cultures of its places: its
|
|
748
|
+
* `data.seat` pooled with every place whose `data.government` names it, as
|
|
749
|
+
* one set;
|
|
750
|
+
* - else nothing. A `being`, a `doc` and any other `lore` note have their own
|
|
751
|
+
* value only; every other type has none.
|
|
752
|
+
*
|
|
753
|
+
* A parent's culture is its own resolved culture, found recursively; a parent
|
|
754
|
+
* resolving to nothing is ignored, as is one in another package that its index
|
|
755
|
+
* gives none. Sources that agree give that culture. Sources that differ, on a
|
|
756
|
+
* note with no value of its own, give it none and one error finding, at
|
|
757
|
+
* `data.parents`, or for a polity's places at `data.seat`, else at its `data:`
|
|
758
|
+
* line. A note whose parent raised that error treats the parent as resolving to
|
|
759
|
+
* nothing, so a disagreement is reported once, where it is. A cycle resolves to
|
|
760
|
+
* nothing.
|
|
761
|
+
*
|
|
762
|
+
* A parent in another package is read through `foreignRecord`, whose published
|
|
763
|
+
* entry already carries that parent's resolved culture, so a foreign chain is
|
|
764
|
+
* never walked.
|
|
765
|
+
*
|
|
766
|
+
* The record's `data` is replaced by a copy carrying the culture, never
|
|
767
|
+
* assigned into, because the note's parsed frontmatter shares that object. The
|
|
768
|
+
* record then carries the resolved culture to every pass that reads it — lint,
|
|
769
|
+
* compile and site alike. A note resolving to nothing gains no `culture` key.
|
|
770
|
+
*
|
|
771
|
+
* @param {Array<Record<string, any>>} records - Index records, every Address a
|
|
772
|
+
* tuple. Records that are not notes are passed over.
|
|
773
|
+
* @param {object} options - Options.
|
|
774
|
+
* @param {string} options.contentPackage - The package the records belong to.
|
|
775
|
+
* @param {string} [options.contentBase] - The content tree, for opening a
|
|
776
|
+
* conflicting note to locate the finding. Without it a finding names the
|
|
777
|
+
* note's path within the tree and no position.
|
|
778
|
+
* @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
|
|
779
|
+
* Another package's entry for a canonical address. Asked only for an
|
|
780
|
+
* immediate parent whose package is not this one.
|
|
781
|
+
* @param {object[]} [options.problems] - Receives each conflict as a finding.
|
|
782
|
+
* Omitted, every conflict is collected and one Error lists them all.
|
|
783
|
+
* @returns {Array<Record<string, any>>} The same records.
|
|
784
|
+
* @throws {Error} When `problems` is omitted and any note's sources disagree.
|
|
785
|
+
*/
|
|
786
|
+
export function resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems }) {
|
|
787
|
+
// Each note under every key a link may name it by: its record's canonical
|
|
788
|
+
// address, and its page address in the `note` system, which is what a
|
|
789
|
+
// `data.parents`, `data.seat` or `data.government` link names.
|
|
790
|
+
const notes = new Map();
|
|
791
|
+
const pageKey = new Map();
|
|
792
|
+
for (const record of records) {
|
|
793
|
+
if (!isNoteRecord(record) || !isAddressSegment(String(record.shortcode ?? ""))) continue;
|
|
794
|
+
const page = canonicalKey(contentPackage, NOTE_SYSTEM, record.type, record.shortcode);
|
|
795
|
+
const own =
|
|
796
|
+
record.address ?
|
|
797
|
+
addressKey(record.address.canonical)
|
|
798
|
+
: noteAddress(record, contentPackage)?.canonical;
|
|
799
|
+
pageKey.set(record, page);
|
|
800
|
+
for (const key of [page, own]) if (key && !notes.has(key)) notes.set(key, record);
|
|
801
|
+
}
|
|
802
|
+
const recordOf = (value) => notes.get(addressKey(value));
|
|
803
|
+
|
|
804
|
+
// The places each polity governs, read once from the places that name it.
|
|
805
|
+
const governed = new Map();
|
|
806
|
+
for (const record of pageKey.keys()) {
|
|
807
|
+
if (record.type !== "place") continue;
|
|
808
|
+
const polity = recordOf(record.data?.government);
|
|
809
|
+
if (!polity) continue;
|
|
810
|
+
if (!governed.has(polity)) governed.set(polity, []);
|
|
811
|
+
governed.get(polity).push(record);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const memo = new Map();
|
|
815
|
+
const inProgress = new Set();
|
|
816
|
+
const findings = [];
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* A source Address's culture: a local note's resolved one, or the one a
|
|
820
|
+
* foreign entry publishes. A foreign item type is published under its
|
|
821
|
+
* system's address as well as its page's, so both are asked for.
|
|
822
|
+
*
|
|
823
|
+
* @param {unknown} value - The Address.
|
|
824
|
+
* @returns {object|undefined} The culture tuple.
|
|
825
|
+
*/
|
|
826
|
+
const cultureThrough = (value) => {
|
|
827
|
+
const key = addressKey(value);
|
|
828
|
+
if (!key) return undefined;
|
|
829
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
830
|
+
if (tuple.package === contentPackage) {
|
|
831
|
+
const record = recordOf(tuple);
|
|
832
|
+
return record ? resolve(record).culture : undefined;
|
|
833
|
+
}
|
|
834
|
+
if (!foreignRecord) return undefined;
|
|
835
|
+
const entry =
|
|
836
|
+
foreignRecord(key) ??
|
|
837
|
+
foreignRecord(
|
|
838
|
+
canonicalKey(
|
|
839
|
+
tuple.package,
|
|
840
|
+
ownDocumentSystem(tuple.type),
|
|
841
|
+
tuple.type,
|
|
842
|
+
tuple.shortcode,
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
return cultureTuple(entry?.culture);
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Agree the cultures of a set of sources.
|
|
850
|
+
*
|
|
851
|
+
* @param {Array<unknown>} sources - Addresses, in the order they are named.
|
|
852
|
+
* @returns {{culture?: object, distinct: Array<{key: string, via: unknown}>}}
|
|
853
|
+
* The one culture they agree on, or each distinct culture with the first
|
|
854
|
+
* source it came through.
|
|
855
|
+
*/
|
|
856
|
+
const agree = (sources) => {
|
|
857
|
+
const distinct = new Map();
|
|
858
|
+
for (const source of sources) {
|
|
859
|
+
const culture = cultureThrough(source);
|
|
860
|
+
const key = addressKey(culture);
|
|
861
|
+
if (key && !distinct.has(key)) distinct.set(key, { culture, via: source });
|
|
862
|
+
}
|
|
863
|
+
const list = [...distinct].map(([key, { culture, via }]) => ({ key, culture, via }));
|
|
864
|
+
return { culture: list.length === 1 ? list[0].culture : undefined, distinct: list };
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Report a disagreement on one note.
|
|
869
|
+
*
|
|
870
|
+
* @param {Record<string, any>} record - The note.
|
|
871
|
+
* @param {string[]} keyPath - Where the disagreement is written.
|
|
872
|
+
* @param {string} subject - What disagrees, as the message opens.
|
|
873
|
+
* @param {Array<{key: string, via: unknown}>} distinct - The cultures and
|
|
874
|
+
* their sources.
|
|
875
|
+
* @returns {{culture: undefined, errored: true}} The resolution.
|
|
876
|
+
*/
|
|
877
|
+
const conflict = (record, keyPath, subject, distinct) => {
|
|
878
|
+
const file = contentBase ? noteFile(contentBase, record) : record.file?.path;
|
|
879
|
+
const raw = contentBase && fs.existsSync(file) ? fs.readFileSync(file, "utf8") : undefined;
|
|
880
|
+
let position = positionOfFrontmatterPath(raw, keyPath, { key: true });
|
|
881
|
+
if (position.line === undefined && keyPath.length > 1)
|
|
882
|
+
position = positionOfFrontmatterPath(raw, ["data"], { key: true });
|
|
883
|
+
const via = (value) => {
|
|
884
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
885
|
+
return tuple.package === contentPackage ? tuple.shortcode : addressKey(tuple);
|
|
886
|
+
};
|
|
887
|
+
const named = distinct.map(({ key, via: source }) => `${key} via ${via(source)}`);
|
|
888
|
+
findings.push({
|
|
889
|
+
file,
|
|
890
|
+
...position,
|
|
891
|
+
severity: "error",
|
|
892
|
+
message:
|
|
893
|
+
`${subject} resolve to different cultures (${named.join(", ")}); ` +
|
|
894
|
+
`state data.culture`,
|
|
895
|
+
});
|
|
896
|
+
return { culture: undefined, errored: true };
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* One note's resolution, by the rules above.
|
|
901
|
+
*
|
|
902
|
+
* @param {Record<string, any>} record - The note's record.
|
|
903
|
+
* @returns {{culture?: object, errored: boolean}} Its culture, and whether
|
|
904
|
+
* it raised a conflict.
|
|
905
|
+
*/
|
|
906
|
+
const resolveRecord = (record) => {
|
|
907
|
+
const type = String(record.type ?? "");
|
|
908
|
+
const subType = String(record.subType ?? "").toLowerCase();
|
|
909
|
+
if (type === "lore" && subType === CULTURE_SUBTYPE)
|
|
910
|
+
return { culture: readCanonicalKey(pageKey.get(record)), errored: false };
|
|
911
|
+
if (!OWN_CULTURE_TYPES.has(type) && !INHERITING_TYPES.has(type)) return NO_CULTURE;
|
|
912
|
+
if (type === "doc" && subType !== SETTING_GUIDE_SUBTYPE) return NO_CULTURE;
|
|
913
|
+
const own = cultureTuple(record.data?.culture);
|
|
914
|
+
if (own) return { culture: own, errored: false };
|
|
915
|
+
if (!INHERITING_TYPES.has(type)) return NO_CULTURE;
|
|
916
|
+
|
|
917
|
+
const parents = Array.isArray(record.data?.parents) ? record.data.parents : [];
|
|
918
|
+
const fromParents = agree(parents);
|
|
919
|
+
if (fromParents.distinct.length > 1)
|
|
920
|
+
return conflict(record, ["data", "parents"], "data.parents", fromParents.distinct);
|
|
921
|
+
if (fromParents.culture) return { culture: fromParents.culture, errored: false };
|
|
922
|
+
|
|
923
|
+
if (type !== "affiliation" || subType !== POLITY_SUBTYPE) return NO_CULTURE;
|
|
924
|
+
const seat = addressKey(record.data?.seat) ? record.data.seat : undefined;
|
|
925
|
+
const seatRecord = seat ? recordOf(seat) : undefined;
|
|
926
|
+
const places = [
|
|
927
|
+
...(seat ? [seat] : []),
|
|
928
|
+
...(governed.get(record) ?? [])
|
|
929
|
+
.filter((place) => place !== seatRecord)
|
|
930
|
+
.map((place) => pageKey.get(place))
|
|
931
|
+
.sort()
|
|
932
|
+
.map((key) => readCanonicalKey(key)),
|
|
933
|
+
];
|
|
934
|
+
const fromPlaces = agree(places);
|
|
935
|
+
if (fromPlaces.distinct.length > 1)
|
|
936
|
+
return conflict(
|
|
937
|
+
record,
|
|
938
|
+
seat ? ["data", "seat"] : ["data"],
|
|
939
|
+
seat ?
|
|
940
|
+
"data.seat and the places this polity governs"
|
|
941
|
+
: "the places this polity governs",
|
|
942
|
+
fromPlaces.distinct,
|
|
943
|
+
);
|
|
944
|
+
return fromPlaces.culture ? { culture: fromPlaces.culture, errored: false } : NO_CULTURE;
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* A local note's resolution, memoised; a note already being resolved is a
|
|
949
|
+
* cycle and resolves to nothing.
|
|
950
|
+
*
|
|
951
|
+
* @param {Record<string, any>} record - The note's record.
|
|
952
|
+
* @returns {{culture?: object, errored: boolean}} Its resolution.
|
|
953
|
+
*/
|
|
954
|
+
const resolve = (record) => {
|
|
955
|
+
if (memo.has(record)) return memo.get(record);
|
|
956
|
+
if (inProgress.has(record)) return NO_CULTURE;
|
|
957
|
+
inProgress.add(record);
|
|
958
|
+
const result = resolveRecord(record);
|
|
959
|
+
inProgress.delete(record);
|
|
960
|
+
memo.set(record, result);
|
|
961
|
+
return result;
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// Walked in address order, so which note of a cycle is met first is a fact
|
|
965
|
+
// about the content rather than about directory-read order.
|
|
966
|
+
const ordered = [...pageKey].sort(([, a], [, b]) =>
|
|
967
|
+
a < b ? -1
|
|
968
|
+
: a > b ? 1
|
|
969
|
+
: 0,
|
|
970
|
+
);
|
|
971
|
+
for (const [record] of ordered) {
|
|
972
|
+
const { culture } = resolve(record);
|
|
973
|
+
if (culture && addressKey(culture) !== addressKey(record.data?.culture))
|
|
974
|
+
record.data = { ...record.data, culture };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
findings.sort(
|
|
978
|
+
(a, b) =>
|
|
979
|
+
String(a.file).localeCompare(String(b.file), "en") ||
|
|
980
|
+
(a.line ?? 0) - (b.line ?? 0) ||
|
|
981
|
+
(a.column ?? 0) - (b.column ?? 0),
|
|
982
|
+
);
|
|
983
|
+
if (problems) problems.push(...findings);
|
|
984
|
+
else if (findings.length)
|
|
985
|
+
// Each line already starts with its path, so it is printed unprefixed.
|
|
986
|
+
throw Object.assign(new Error(findings.map(formatDiagnostic).join("\n")), {
|
|
987
|
+
located: true,
|
|
988
|
+
});
|
|
989
|
+
return records;
|
|
990
|
+
}
|
|
991
|
+
|
|
686
992
|
/**
|
|
687
993
|
* Read a content tree into index records, in the order they will be written.
|
|
688
994
|
*
|
|
@@ -711,13 +1017,30 @@ export function indexRecordsForNote({
|
|
|
711
1017
|
* @param {object[]} [options.problems] - Supplied by a **reader**: a note that
|
|
712
1018
|
* cannot be recorded is pushed here as a diagnostic and skipped. Omitted, the
|
|
713
1019
|
* note throws — the contract the emitter needs, since an index missing a note
|
|
714
|
-
* asserts that it does not exist.
|
|
1020
|
+
* asserts that it does not exist. The same holds for a culture conflict: a
|
|
1021
|
+
* reader receives each as a finding, and the emitter throws once, listing
|
|
1022
|
+
* every one.
|
|
1023
|
+
* @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
|
|
1024
|
+
* Another package's entry for a canonical address, read for a parent that
|
|
1025
|
+
* package publishes; see {@link resolveCultures}.
|
|
1026
|
+
* @param {Map<string, {body: string, frontmatter: object|null}>} [options.parsed] -
|
|
1027
|
+
* Filled with each note's body and its frontmatter as authored, keyed by the
|
|
1028
|
+
* note's file, so a later pass reads the notes this walk already parsed.
|
|
715
1029
|
* @returns {Array<Record<string, any>>} The records, in a total order that does
|
|
716
1030
|
* not depend on directory-read order.
|
|
717
1031
|
*/
|
|
718
1032
|
export function collectContentIndex(
|
|
719
1033
|
contentBase,
|
|
720
|
-
{
|
|
1034
|
+
{
|
|
1035
|
+
contentPackage,
|
|
1036
|
+
skipDirectories,
|
|
1037
|
+
assetsBase,
|
|
1038
|
+
manifest,
|
|
1039
|
+
problems,
|
|
1040
|
+
addressContext,
|
|
1041
|
+
foreignRecord,
|
|
1042
|
+
parsed,
|
|
1043
|
+
},
|
|
721
1044
|
) {
|
|
722
1045
|
const records = [];
|
|
723
1046
|
// Passed through rather than defaulted away: an absent scope is the
|
|
@@ -781,8 +1104,16 @@ export function collectContentIndex(
|
|
|
781
1104
|
assetsBase ? collectAssetRecords(assetsBase, { contentPackage, problems }) : [];
|
|
782
1105
|
const assetIndex = assetAddressIndex(assetRecords, { config: { contentPackage } });
|
|
783
1106
|
const resolveRole = (address) => embedRole(assetIndex, address);
|
|
1107
|
+
const decodeContext = withDeclaredAddresses(
|
|
1108
|
+
addressContext ?? { package: contentPackage },
|
|
1109
|
+
parsedNotes,
|
|
1110
|
+
contentPackage,
|
|
1111
|
+
);
|
|
784
1112
|
|
|
785
1113
|
for (const { frontmatter, body, bodyLine, absPath } of parsedNotes) {
|
|
1114
|
+
// Before the index derives anything onto the frontmatter, so a later
|
|
1115
|
+
// pass reads the note as authored.
|
|
1116
|
+
parsed?.set(absPath, { body, frontmatter: structuredClone(frontmatter) });
|
|
786
1117
|
const fm = frontmatter ?? {};
|
|
787
1118
|
applyComputedBeingAge(fm, present, dates);
|
|
788
1119
|
const relPath = path.relative(contentBase, absPath);
|
|
@@ -796,7 +1127,7 @@ export function collectContentIndex(
|
|
|
796
1127
|
body,
|
|
797
1128
|
bodyLine,
|
|
798
1129
|
manifest,
|
|
799
|
-
addressContext,
|
|
1130
|
+
addressContext: decodeContext,
|
|
800
1131
|
dateContext: dates,
|
|
801
1132
|
resolveRole,
|
|
802
1133
|
}),
|
|
@@ -819,7 +1150,19 @@ export function collectContentIndex(
|
|
|
819
1150
|
// would state that the note does not exist.
|
|
820
1151
|
if (!problems) throw err;
|
|
821
1152
|
// A reader, by contrast, reports it and carries on: one malformed
|
|
822
|
-
// note must not take every other finding in the tree with it.
|
|
1153
|
+
// note must not take every other finding in the tree with it. A
|
|
1154
|
+
// decode that refused several values reports each at its own
|
|
1155
|
+
// position.
|
|
1156
|
+
if (err.findings?.length) {
|
|
1157
|
+
for (const found of err.findings)
|
|
1158
|
+
problems.push({
|
|
1159
|
+
file: absPath,
|
|
1160
|
+
...(found.position ?? {}),
|
|
1161
|
+
severity: "error",
|
|
1162
|
+
message: String(found.message),
|
|
1163
|
+
});
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
823
1166
|
problems.push({
|
|
824
1167
|
file: absPath,
|
|
825
1168
|
...(err.position ?? {}),
|
|
@@ -831,6 +1174,11 @@ export function collectContentIndex(
|
|
|
831
1174
|
}
|
|
832
1175
|
}
|
|
833
1176
|
|
|
1177
|
+
// Every note record exists and every Address in it is a tuple, so a parent
|
|
1178
|
+
// is found by its canonical address. Before the sort, which reads nothing
|
|
1179
|
+
// the pass writes.
|
|
1180
|
+
resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems });
|
|
1181
|
+
|
|
834
1182
|
// Sorted at every depth like a note's record, and for the same reason: the
|
|
835
1183
|
// declaration order of the `asset` fields is a fact about the emitter, not
|
|
836
1184
|
// about the content, and the artifact is meant to be byte-identical across
|
|
@@ -893,6 +1241,61 @@ function needsContentTree(resolved) {
|
|
|
893
1241
|
return (resolved.packs ?? []).length > 0;
|
|
894
1242
|
}
|
|
895
1243
|
|
|
1244
|
+
/**
|
|
1245
|
+
* An Address context that answers whether a complete Address is declared.
|
|
1246
|
+
*
|
|
1247
|
+
* A refused shortcode is reported with the lowercase spelling it means when
|
|
1248
|
+
* that spelling names a note in this tree, or something the context's own
|
|
1249
|
+
* `declaresAddress` answers for — a dependency's index. The package, type and
|
|
1250
|
+
* shortcode decide it; the system does not, since one note answers under
|
|
1251
|
+
* several.
|
|
1252
|
+
*
|
|
1253
|
+
* @param {object} context - The Address context the tree is decoded with.
|
|
1254
|
+
* @param {Array<{frontmatter?: object}>} notes - The tree's parsed notes.
|
|
1255
|
+
* @param {string} contentPackage - The package the tree compiles as.
|
|
1256
|
+
* @returns {object} The context, with `declaresAddress`.
|
|
1257
|
+
*/
|
|
1258
|
+
function withDeclaredAddresses(context, notes, contentPackage) {
|
|
1259
|
+
const local = new Set(
|
|
1260
|
+
notes
|
|
1261
|
+
.map(({ frontmatter }) => frontmatter)
|
|
1262
|
+
.filter((fm) => typeof fm?.type === "string" && typeof fm?.shortcode === "string")
|
|
1263
|
+
.map((fm) => `${fm.type}\0${fm.shortcode}`),
|
|
1264
|
+
);
|
|
1265
|
+
const beyond = context.declaresAddress;
|
|
1266
|
+
return {
|
|
1267
|
+
...context,
|
|
1268
|
+
declaresAddress: (tuple) =>
|
|
1269
|
+
(tuple.package === contentPackage && local.has(`${tuple.type}\0${tuple.shortcode}`)) ||
|
|
1270
|
+
Boolean(beyond?.(tuple)),
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Whether a complete Address names something a fetched dependency index
|
|
1276
|
+
* declares.
|
|
1277
|
+
*
|
|
1278
|
+
* Read on first use only: it answers for the message of a refused shortcode,
|
|
1279
|
+
* and a tree with none never reads a dependency's index for it.
|
|
1280
|
+
*
|
|
1281
|
+
* @param {object} config - The resolved configuration.
|
|
1282
|
+
* @returns {(tuple: {package: string, type: string, shortcode: string}) => boolean}
|
|
1283
|
+
* The predicate.
|
|
1284
|
+
*/
|
|
1285
|
+
function dependencyAddresses(config) {
|
|
1286
|
+
/** @type {Set<string>|undefined} */
|
|
1287
|
+
let known;
|
|
1288
|
+
return (tuple) => {
|
|
1289
|
+
known ??= new Set(
|
|
1290
|
+
[...loadForeignIndexes(config, [config.contentPackage]).index.keys()]
|
|
1291
|
+
.map((key) => readCanonicalKey(key))
|
|
1292
|
+
.filter(Boolean)
|
|
1293
|
+
.map((t) => `${t.package}\0${t.type}\0${t.shortcode}`),
|
|
1294
|
+
);
|
|
1295
|
+
return known.has(`${tuple.package}\0${tuple.type}\0${tuple.shortcode}`);
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
896
1299
|
/**
|
|
897
1300
|
* The index records for a content tree, without writing anything.
|
|
898
1301
|
*
|
|
@@ -919,6 +1322,10 @@ function needsContentTree(resolved) {
|
|
|
919
1322
|
* aborting the derivation. Omitted, the note throws — which is the contract
|
|
920
1323
|
* the emitter needs, since an index missing a note asserts that it does not
|
|
921
1324
|
* exist.
|
|
1325
|
+
* @param {Map<string, {body: string, frontmatter: object|null}>} [opts.parsed] -
|
|
1326
|
+
* Filled with each note's body and authored frontmatter, keyed by the note's
|
|
1327
|
+
* file. Handed to {@link module:engine/sql-tables.prepareTreeSqlTables}, it
|
|
1328
|
+
* spares that pass reading and parsing every note again.
|
|
922
1329
|
* @returns {object[]} One record per note, plus one per documentation entry.
|
|
923
1330
|
*/
|
|
924
1331
|
export function indexRecordsFor({
|
|
@@ -927,20 +1334,33 @@ export function indexRecordsFor({
|
|
|
927
1334
|
config,
|
|
928
1335
|
skipDirectories,
|
|
929
1336
|
problems,
|
|
1337
|
+
parsed,
|
|
930
1338
|
} = {}) {
|
|
931
1339
|
const resolved = config ?? loadPackConfig();
|
|
932
1340
|
const tree = contentBase ?? resolved.paths.content;
|
|
933
1341
|
if (!fs.existsSync(tree) && needsContentTree(resolved)) {
|
|
934
1342
|
throw new Error(`no content tree at ${tree}`);
|
|
935
1343
|
}
|
|
1344
|
+
// Read on the first parent another package publishes, and not before, so
|
|
1345
|
+
// a package whose parents are all its own reads no dependency index.
|
|
1346
|
+
let foreign;
|
|
1347
|
+
const foreignRecord = (key) => {
|
|
1348
|
+
foreign ??= loadForeignIndexes(resolved, [resolved.contentPackage]).index;
|
|
1349
|
+
return foreign.get(key);
|
|
1350
|
+
};
|
|
936
1351
|
return collectContentIndex(tree, {
|
|
937
1352
|
contentPackage: resolved.contentPackage,
|
|
938
|
-
addressContext:
|
|
1353
|
+
addressContext: {
|
|
1354
|
+
...noteAddressContext(resolved),
|
|
1355
|
+
declaresAddress: dependencyAddresses(resolved),
|
|
1356
|
+
},
|
|
939
1357
|
skipDirectories: skipDirectories ?? resolved.skipDirectories,
|
|
940
1358
|
assetsBase: assetsBase ?? resolved.paths.assets,
|
|
941
1359
|
// Only the identities a UUID is a function of — see emitContentIndex.
|
|
942
1360
|
manifest: foundryIdentities(resolved),
|
|
943
1361
|
problems,
|
|
1362
|
+
foreignRecord,
|
|
1363
|
+
parsed,
|
|
944
1364
|
});
|
|
945
1365
|
}
|
|
946
1366
|
|
|
@@ -985,7 +1405,10 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
985
1405
|
// state one.
|
|
986
1406
|
const records = indexRecordsFor({ contentBase: tree, config: resolved });
|
|
987
1407
|
const assetBindings = checkForeignAssetBindings(resolved);
|
|
988
|
-
if (assetBindings.length)
|
|
1408
|
+
if (assetBindings.length)
|
|
1409
|
+
throw Object.assign(new Error(assetBindings.map(formatDiagnostic).join("\n")), {
|
|
1410
|
+
located: true,
|
|
1411
|
+
});
|
|
989
1412
|
if (records.length === 0) {
|
|
990
1413
|
throw new Error(
|
|
991
1414
|
`${tree} yielded no notes, so the index would state that this ` +
|