@heroiclands/package-build 0.6.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG-content-build.md +965 -0
  2. package/CHANGELOG.md +58 -0
  3. package/CONTENT.md +824 -0
  4. package/MIGRATING.md +127 -0
  5. package/README.md +29 -17
  6. package/bin/content-build.mjs +1082 -0
  7. package/bin/package-build.mjs +2 -2
  8. package/bin/report.mjs +2 -2
  9. package/config.mjs +21 -18
  10. package/content-config.mjs +1424 -0
  11. package/coverage.mjs +1 -1
  12. package/e2e.mjs +1 -1
  13. package/engine/abbreviations.mjs +0 -0
  14. package/engine/base-compiler.mjs +608 -0
  15. package/engine/code-fences.mjs +311 -0
  16. package/engine/compendiums.mjs +353 -0
  17. package/engine/content-address.mjs +141 -0
  18. package/engine/content-links.mjs +438 -0
  19. package/engine/content-lint.mjs +204 -0
  20. package/engine/content-package.mjs +67 -0
  21. package/engine/content-slug.mjs +169 -0
  22. package/engine/content-tables.mjs +1216 -0
  23. package/engine/content-tree.mjs +56 -0
  24. package/engine/diagnostics.mjs +270 -0
  25. package/engine/field-reference.mjs +242 -0
  26. package/engine/field-spec.mjs +218 -0
  27. package/engine/foreign-catalog.mjs +450 -0
  28. package/engine/foreign-manifests.mjs +129 -0
  29. package/engine/frontmatter-lint.mjs +343 -0
  30. package/engine/frontmatter.mjs +258 -0
  31. package/engine/generate.mjs +305 -0
  32. package/engine/helpers.mjs +809 -0
  33. package/engine/ids.mjs +192 -0
  34. package/engine/index.mjs +142 -0
  35. package/engine/item-docs.mjs +153 -0
  36. package/engine/item-registry.mjs +129 -0
  37. package/engine/journals.mjs +374 -0
  38. package/engine/kb-manifest.mjs +473 -0
  39. package/engine/macros.mjs +333 -0
  40. package/engine/manifest-emit.mjs +339 -0
  41. package/engine/map-notes.mjs +1302 -0
  42. package/engine/pack-config.mjs +479 -0
  43. package/engine/pack-router.mjs +254 -0
  44. package/engine/prose-config.mjs +238 -0
  45. package/engine/prose-lint.mjs +294 -0
  46. package/engine/region-events.mjs +79 -0
  47. package/engine/scene-levels.mjs +420 -0
  48. package/engine/scenes.mjs +528 -0
  49. package/engine/site-build.mjs +778 -0
  50. package/engine/site-index.mjs +332 -0
  51. package/engine/web-wikilinks.mjs +330 -0
  52. package/engine/wikilink-syntax.mjs +105 -0
  53. package/engine/wikilinks.mjs +645 -0
  54. package/index.mjs +42 -25
  55. package/lang.mjs +1 -1
  56. package/manifest.mjs +79 -5
  57. package/markdownlint-config.mjs +45 -0
  58. package/package.json +78 -35
  59. package/prettier-config.mjs +34 -0
  60. package/sohl/actors.mjs +581 -0
  61. package/sohl/affiliation-standings.mjs +44 -0
  62. package/sohl/being-info.mjs +173 -0
  63. package/sohl/default-item-art.mjs +82 -0
  64. package/sohl/index.mjs +55 -0
  65. package/sohl/item-builders.mjs +114 -0
  66. package/sohl/item-fields.mjs +867 -0
  67. package/sohl/items.mjs +197 -0
  68. package/sohl/kb-passes.mjs +207 -0
  69. package/sohl/note-schemas.mjs +322 -0
  70. package/templates.mjs +1 -1
  71. package/types/config.d.mts +1 -1
  72. package/types/content-config.d.mts +571 -0
  73. package/types/engine/abbreviations.d.mts +44 -0
  74. package/types/engine/base-compiler.d.mts +320 -0
  75. package/types/engine/code-fences.d.mts +102 -0
  76. package/types/engine/compendiums.d.mts +77 -0
  77. package/types/engine/content-address.d.mts +61 -0
  78. package/types/engine/content-links.d.mts +87 -0
  79. package/types/engine/content-lint.d.mts +47 -0
  80. package/types/engine/content-package.d.mts +36 -0
  81. package/types/engine/content-slug.d.mts +59 -0
  82. package/types/engine/content-tables.d.mts +131 -0
  83. package/types/engine/content-tree.d.mts +12 -0
  84. package/types/engine/diagnostics.d.mts +152 -0
  85. package/types/engine/field-reference.d.mts +20 -0
  86. package/types/engine/field-spec.d.mts +181 -0
  87. package/types/engine/foreign-catalog.d.mts +103 -0
  88. package/types/engine/foreign-manifests.d.mts +43 -0
  89. package/types/engine/frontmatter-lint.d.mts +74 -0
  90. package/types/engine/frontmatter.d.mts +99 -0
  91. package/types/engine/generate.d.mts +56 -0
  92. package/types/engine/helpers.d.mts +348 -0
  93. package/types/engine/ids.d.mts +126 -0
  94. package/types/engine/index.d.mts +33 -0
  95. package/types/engine/item-docs.d.mts +73 -0
  96. package/types/engine/item-registry.d.mts +57 -0
  97. package/types/engine/journals.d.mts +121 -0
  98. package/types/engine/kb-manifest.d.mts +214 -0
  99. package/types/engine/macros.d.mts +201 -0
  100. package/types/engine/manifest-emit.d.mts +137 -0
  101. package/types/engine/map-notes.d.mts +283 -0
  102. package/types/engine/pack-config.d.mts +77 -0
  103. package/types/engine/pack-router.d.mts +53 -0
  104. package/types/engine/prose-config.d.mts +121 -0
  105. package/types/engine/prose-lint.d.mts +59 -0
  106. package/types/engine/region-events.d.mts +43 -0
  107. package/types/engine/scene-levels.d.mts +36 -0
  108. package/types/engine/scenes.d.mts +48 -0
  109. package/types/engine/site-build.d.mts +214 -0
  110. package/types/engine/site-index.d.mts +141 -0
  111. package/types/engine/web-wikilinks.d.mts +67 -0
  112. package/types/engine/wikilink-syntax.d.mts +101 -0
  113. package/types/engine/wikilinks.d.mts +172 -0
  114. package/types/index.d.mts +3 -0
  115. package/types/manifest.d.mts +52 -4
  116. package/types/markdownlint-config.d.mts +8 -0
  117. package/types/prettier-config.d.mts +1 -0
  118. package/types/sohl/actors.d.mts +65 -0
  119. package/types/sohl/affiliation-standings.d.mts +22 -0
  120. package/types/sohl/being-info.d.mts +78 -0
  121. package/types/sohl/default-item-art.d.mts +28 -0
  122. package/types/sohl/index.d.mts +7 -0
  123. package/types/sohl/item-builders.d.mts +11 -0
  124. package/types/sohl/item-fields.d.mts +20 -0
  125. package/types/sohl/items.d.mts +19 -0
  126. package/types/sohl/kb-passes.d.mts +71 -0
  127. package/types/sohl/note-schemas.d.mts +7 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Whether a value is a well-formed shortcode.
3
+ *
4
+ * A blank value is **not** valid here. Blank is handled separately wherever a
5
+ * key is derived from a document's name, so this predicate answers only "is
6
+ * this an acceptable key", never "is this key present".
7
+ *
8
+ * @param {unknown} value - The candidate shortcode.
9
+ * @returns {boolean} `true` when it matches {@link SHORTCODE_PATTERN}.
10
+ */
11
+ export function isValidShortcode(value: unknown): boolean;
12
+ /**
13
+ * Lint every address in a content tree.
14
+ *
15
+ * @param {string} contentBase - Root of the content tree.
16
+ * @param {object} [opts]
17
+ * @param {readonly string[]} [opts.skipDirectories] - Directory names the walk
18
+ * ignores. Defaults to the configured list.
19
+ * @returns {{findings: Array<{file: string, line?: number, column?: number,
20
+ * severity: "error"|"warning", message: string}>, notes: number,
21
+ * keys: number}} The findings, and what was inspected to produce them.
22
+ */
23
+ export function lintContentTree(contentBase: string, { skipDirectories }?: {
24
+ skipDirectories?: readonly string[] | undefined;
25
+ }): {
26
+ findings: Array<{
27
+ file: string;
28
+ line?: number;
29
+ column?: number;
30
+ severity: "error" | "warning";
31
+ message: string;
32
+ }>;
33
+ notes: number;
34
+ keys: number;
35
+ };
36
+ /**
37
+ * The shape every `shortcode` must match: ASCII letters and digits only.
38
+ *
39
+ * Case is deliberately **not** constrained: hundreds of authored shortcodes are
40
+ * mixed-case and collide with nothing, so tightening that is a separate
41
+ * decision from this one.
42
+ *
43
+ * A consuming system's *runtime* keeps its own copy of this pattern — it cannot
44
+ * import a build-time dependency into shipped code — and is expected to pin the
45
+ * two together with a test rather than trust that they still agree.
46
+ */
47
+ export const SHORTCODE_PATTERN: RegExp;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The **content** package: the distribution unit a note declares in its
3
+ * `package:` frontmatter. The pack compilers select their entries by it.
4
+ *
5
+ * Stable across compilation targets. If this content were ever compiled for a
6
+ * second game system, its notes would still declare `package: sohl` — only the
7
+ * Foundry package below would differ.
8
+ *
9
+ * An accessor rather than a hoisted constant, so that importing this module
10
+ * needs no configuration (#2).
11
+ *
12
+ * @returns {string} The configured `contentPackage`.
13
+ */
14
+ export function contentPackage(): string;
15
+ /**
16
+ * The **Foundry package** this repository's packs are shipped in — the `id` in
17
+ * `assets/templates/system.template.json`, and the first segment of every
18
+ * compendium UUID the compilers emit.
19
+ *
20
+ * Distinct from {@link contentPackage}, and equal to it only by coincidence
21
+ * here: a note says `package: sohl` and its documents are addressed as
22
+ * `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
23
+ * (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
24
+ * than one — treating them as interchangeable is what #1498 was.
25
+ *
26
+ * Configured rather than read from the manifest so the link resolver stays
27
+ * filesystem-free and unit-testable. `assertPackageIdMatchesManifestFile` in
28
+ * `package-manifest.mjs` — called from `generatePacksJson`, before any entry is
29
+ * written — fails the build if this value and the manifest's `id` ever drift.
30
+ *
31
+ * An accessor rather than a hoisted constant, so that importing this module
32
+ * needs no configuration (#2).
33
+ *
34
+ * @returns {string} The configured `foundryPackage`.
35
+ */
36
+ export function foundryPackageId(): string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The URL segment for one content note.
3
+ *
4
+ * The name is **transliterated** before it is reduced, so an accented character
5
+ * is carried across rather than dropped — dropping is what turned `Nüsvōrroth`
6
+ * into `n-sv-rroth` and forced a hand-written slug. Ligatures expand the way a
7
+ * reader would spell them out: `þ`→`th`, `æ`→`ae`, `œ`→`oe`, `ß`→`ss`, `ij`→`ij`,
8
+ * `fi`→`fi`, and eth (`ð`) follows the Icelandic convention of a bare `d`.
9
+ *
10
+ * Two reductions are ours rather than the transliterator's:
11
+ *
12
+ * - **apostrophes are removed**, not treated as separators (`Armorer's Kit` →
13
+ * `armorers-kit`), matching the URLs these pages already publish at;
14
+ * - **a fraction keeps its digits together** — a vulgar fraction expands to
15
+ * `3/4`, and the solidus would otherwise split it into `3-4`, so a slash
16
+ * *between digits* is closed up (`Kûrbúl ¾-Helm` → `kurbul-34-helm`).
17
+ *
18
+ * @param {string | undefined} name - The note's display name (`name.full`),
19
+ * which a malformed note may not have at all.
20
+ * @returns {string} The URL segment (never empty).
21
+ * @throws {Error} When there is no name, or the name carries no URL-safe
22
+ * characters — either way the note cannot be addressed, which is a content
23
+ * error rather than something to paper over with a fallback.
24
+ */
25
+ export function slugify(text: any): string;
26
+ /**
27
+ * The URL segment a content note publishes at.
28
+ *
29
+ * {@link slugify} with the rule that a document *must* be addressable: a note
30
+ * that yields no slug is a content error, not something to paper over with a
31
+ * fallback, because the alternative is a page nobody can reach.
32
+ *
33
+ * @param {string | undefined} name - The note's display name (`name.full`),
34
+ * which a malformed note may not have at all.
35
+ * @returns {string} The URL segment (never empty).
36
+ * @throws {Error} When there is no name, or the name carries no URL-safe
37
+ * characters.
38
+ */
39
+ export function contentSlug(name: string | undefined): string;
40
+ /**
41
+ * Find pages that would publish to the same URL.
42
+ *
43
+ * Nothing constrains two notes in one section from sharing a name, and a
44
+ * collision silently overwrites one page with the other. This turns it into a
45
+ * build failure that names every claimant, so the fix is a more specific title.
46
+ * (The content tree has no collisions today.)
47
+ *
48
+ * @param {Array<{sec: string, slug: string, src: string}>} pages
49
+ * @returns {Array<{url: string, sources: string[]}>} One entry per collision, in
50
+ * first-claim order; empty when every URL is unique.
51
+ */
52
+ export function findSlugCollisions(pages: Array<{
53
+ sec: string;
54
+ slug: string;
55
+ src: string;
56
+ }>): Array<{
57
+ url: string;
58
+ sources: string[];
59
+ }>;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Parse one `dataview` block's query.
3
+ *
4
+ * @param {string} source - The query text (fences already stripped).
5
+ * @returns {{columns: Array<{header: string, expr: object}>, from: object|null,
6
+ * where: object|null, sort: Array<{expr: object, descending: boolean}>,
7
+ * limit: number|null}}
8
+ * @throws {Error} When the query is not a supported `TABLE` query — the message
9
+ * names the offending clause, column, or token.
10
+ */
11
+ export function parseDataviewQuery(source: string): {
12
+ columns: Array<{
13
+ header: string;
14
+ expr: object;
15
+ }>;
16
+ from: object | null;
17
+ where: object | null;
18
+ sort: Array<{
19
+ expr: object;
20
+ descending: boolean;
21
+ }>;
22
+ limit: number | null;
23
+ };
24
+ /**
25
+ * Resolve a dotted path against a content note. `file.*` names the note's place
26
+ * in the tree; everything else reads frontmatter, preferring a literal dotted
27
+ * key before walking the segments (the same precedence the pack compilers'
28
+ * `getFrontmatter` uses) — so **any** frontmatter property is addressable,
29
+ * however deeply nested.
30
+ *
31
+ * @param {ContentTableDoc} doc
32
+ * @param {string} path
33
+ * @returns {unknown} `null` when the note has no such field.
34
+ * @throws {Error} On an unknown `file.*` field, which would otherwise read as a
35
+ * table that silently matches nothing.
36
+ */
37
+ export function resolveField(doc: ContentTableDoc, path: string): unknown;
38
+ /**
39
+ * Evaluate one parsed expression against a note.
40
+ *
41
+ * @param {object} node - From {@link parseDataviewQuery}.
42
+ * @param {ContentTableDoc} doc - The row being rendered.
43
+ * @param {ContentTableDoc} [self] - The note *containing* the query, which is
44
+ * what `this` reads; absent means every `this.…` resolves to nothing.
45
+ * @returns {unknown}
46
+ */
47
+ export function evaluate(node: object, doc: ContentTableDoc, self?: ContentTableDoc): unknown;
48
+ /**
49
+ * The notes a query selects, in the order its `SORT` keys give. With no `SORT`
50
+ * the notes keep content-path order, and the note path (then its id) breaks any
51
+ * remaining tie, so the emitted table is byte-stable across builds regardless
52
+ * of directory-walk order.
53
+ *
54
+ * @param {object} spec - From {@link parseDataviewQuery}.
55
+ * @param {Array<ContentTableDoc>} docs
56
+ * @param {ContentTableDoc} [self] - The note containing the query (`this`).
57
+ * @returns {Array<ContentTableDoc>} The matching notes, sorted and limited.
58
+ */
59
+ export function selectRows(spec: object, docs: Array<ContentTableDoc>, self?: ContentTableDoc): Array<ContentTableDoc>;
60
+ /**
61
+ * Build the markdown table for one query.
62
+ *
63
+ * A `link(…)` cell — and the implicit `File` column — is emitted as a wikilink
64
+ * to the row's own note, which the caller's wikilink pass resolves; a note the
65
+ * caller reports as unlinkable (no shortcode, or a content directory that
66
+ * compiles into no pack) degrades to plain text. A column is right-aligned when
67
+ * every value it shows is numeric.
68
+ *
69
+ * @param {object} spec - From {@link parseDataviewQuery}.
70
+ * @param {Array<ContentTableDoc>} rows - From {@link selectRows}.
71
+ * @param {(doc: ContentTableDoc) => boolean} linkable - Can this note be linked to?
72
+ * @param {ContentTableDoc} [self] - The note containing the query (`this`).
73
+ * @returns {string} The markdown table (no trailing newline).
74
+ */
75
+ export function renderContentTable(spec: object, rows: Array<ContentTableDoc>, linkable: (doc: ContentTableDoc) => boolean, self?: ContentTableDoc): string;
76
+ /**
77
+ * Expand every fenced `dataview` block in a markdown body.
78
+ *
79
+ * A block that cannot be honoured — malformed or unsupported — is left in the
80
+ * body verbatim and reported in `errors`, so the failure is visible in the
81
+ * output as well as on the console. Every other code fence, and every code
82
+ * span, is left alone (that is how the syntax is documented).
83
+ *
84
+ * A query that matches **no** note is not an error: it renders as an empty
85
+ * table (headers only), which is what the author already sees in Obsidian, and
86
+ * a category with no content yet is a normal state of the corpus rather than a
87
+ * broken build.
88
+ *
89
+ * @param {string} markdown - The note body, frontmatter already stripped.
90
+ * @param {object} ctx
91
+ * @param {Array<ContentTableDoc>} ctx.docs - The searchable universe: every
92
+ * content note the caller considers in scope.
93
+ * @param {(doc: ContentTableDoc) => boolean} [ctx.linkable] - Whether a note can
94
+ * be linked to from a cell; defaults to never.
95
+ * @param {string} [ctx.source] - The note being expanded, for error reports.
96
+ * @param {ContentTableDoc} [ctx.self] - The note being expanded, as a searchable
97
+ * doc: what a query's `this` reads.
98
+ * @returns {{markdown: string, errors: Array<{source: string, directive: string,
99
+ * reason: string, line: number}>, lineMap: Array<{line: number,
100
+ * generated: boolean}>}} `lineMap` is parallel to the emitted lines and says
101
+ * which authored line each came from, so a diagnostic about the expanded
102
+ * body can name an authored position (#17). An `errors` entry carries the
103
+ * 0-based line of the directive that failed, for the same reason.
104
+ */
105
+ export function expandContentTables(markdown: string, { docs, linkable, source, self }?: {
106
+ docs: Array<ContentTableDoc>;
107
+ linkable?: ((doc: ContentTableDoc) => boolean) | undefined;
108
+ source?: string | undefined;
109
+ self?: ContentTableDoc | undefined;
110
+ }): {
111
+ markdown: string;
112
+ errors: Array<{
113
+ source: string;
114
+ directive: string;
115
+ reason: string;
116
+ line: number;
117
+ }>;
118
+ lineMap: Array<{
119
+ line: number;
120
+ generated: boolean;
121
+ }>;
122
+ };
123
+ /**
124
+ * One content note as a content build hands it to the expander: its parsed
125
+ * frontmatter plus its path below the content root, which backs the `file.*`
126
+ * fields.
127
+ */
128
+ export type ContentTableDoc = {
129
+ fm: Record<string, any>;
130
+ path?: string;
131
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Count the Markdown notes below a content tree.
3
+ *
4
+ * Only notes count. The per-pack folder manifests are not content, so a tree
5
+ * holding nothing but manifests counts zero — which is correct, because it
6
+ * compiles zero documents. Dot directories are skipped so stale editor caches
7
+ * cannot make an empty tree look populated.
8
+ *
9
+ * @param {string} root - Absolute path to the content tree.
10
+ * @returns {number} The number of `.md` notes, or 0 when the tree is absent.
11
+ */
12
+ export function countContentNotes(root: string): number;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The `file:line:column` locator, with whatever is known.
3
+ *
4
+ * The path is relativized against the working directory — during a build that
5
+ * is the consuming repository's root, so the result is both shorter to read
6
+ * and what an editor resolves a relative diagnostic against. A path outside
7
+ * the tree stays absolute, since a `../../..` locator helps nobody.
8
+ *
9
+ * @param {object} at
10
+ * @param {string} [at.file] - Absolute or relative path to the source file.
11
+ * @param {number} [at.line] - 1-based line.
12
+ * @param {number} [at.column] - 1-based column. Ignored without a line.
13
+ * @returns {string} The locator, or `""` when not even a file is known.
14
+ */
15
+ export function formatLocator({ file, line, column }?: {
16
+ file?: string | undefined;
17
+ line?: number | undefined;
18
+ column?: number | undefined;
19
+ }): string;
20
+ /**
21
+ * One diagnostic, as a parseable line.
22
+ *
23
+ * @param {object} d
24
+ * @param {string} [d.file] - Source file the diagnostic is about.
25
+ * @param {number} [d.line] - 1-based line.
26
+ * @param {number} [d.column] - 1-based column.
27
+ * @param {"warning"|"error"} d.severity - Which of the two levels this is.
28
+ * @param {string} d.message - What is wrong, in one sentence.
29
+ * @returns {string} `file:line:column: severity: message`, with any unknown
30
+ * leading field omitted.
31
+ */
32
+ export function formatDiagnostic({ file, line, column, severity, message }: {
33
+ file?: string | undefined;
34
+ line?: number | undefined;
35
+ column?: number | undefined;
36
+ severity: "warning" | "error";
37
+ message: string;
38
+ }): string;
39
+ /**
40
+ * Prints one diagnostic on the console, unprefixed.
41
+ *
42
+ * **Both severities go to stderr**, which is what keeps findings clear of the
43
+ * progress and summary prose a build writes to stdout. That is Node's doing,
44
+ * not a choice made here: `console.warn` is an alias for `console.error` and
45
+ * writes to `process.stderr`, so the two branches below differ only in which
46
+ * severity word the line carries, never in the stream.
47
+ *
48
+ * Saying otherwise has already cost something — an earlier version of this
49
+ * comment claimed warnings went to stdout, and a consumer wrote a whole
50
+ * local wrapper to obtain the stderr routing it already had. Anything relying
51
+ * on the separation should split on the `severity` field, not on the stream.
52
+ *
53
+ * This deliberately sidesteps `loglevel`, for the reason given in the module
54
+ * docs.
55
+ *
56
+ * @param {object} d - As {@link formatDiagnostic}.
57
+ * @returns {void}
58
+ */
59
+ export function emitDiagnostic(d: object): void;
60
+ /**
61
+ * Where a character offset within a note's **body** falls in its **file**.
62
+ *
63
+ * Three corrections separate the two, and each is applied only where it is
64
+ * true:
65
+ *
66
+ * 1. _The frontmatter._ A body offset is not a file line until the lines
67
+ * before the body are added — `bodyLine`.
68
+ * 2. _The trimmed first line._ `parseMarkdownFile` trims the body, so its
69
+ * first line may have lost indentation the file still has. `bodyColumn`
70
+ * restores it, and only on that line.
71
+ * 3. _Generated text._ A body is scanned **after** its content tables expand,
72
+ * so an offset may fall in text no one authored. `lineMap` maps each
73
+ * scanned line back to the line it came from; a generated line reports the
74
+ * directive that produced it and **no column**, since there is no authored
75
+ * character to point at.
76
+ *
77
+ * @param {string} body - The text the offset indexes into.
78
+ * @param {number} offset - 0-based character offset within `body`.
79
+ * @param {object} [opts]
80
+ * @param {number} [opts.bodyLine=1] - 1-based file line of the body's line 0.
81
+ * @param {number} [opts.bodyColumn=1] - 1-based file column of the body's
82
+ * first character.
83
+ * @param {Array<{line: number, generated: boolean}>} [opts.lineMap] - Per
84
+ * scanned line, the 0-based body line it came from. From
85
+ * {@link expandContentTables}.
86
+ * @returns {{line: number, column: number|undefined, generated: boolean}}
87
+ */
88
+ export function positionInBody(body: string, offset: number, { bodyLine, bodyColumn, lineMap }?: {
89
+ bodyLine?: number | undefined;
90
+ bodyColumn?: number | undefined;
91
+ lineMap?: {
92
+ line: number;
93
+ generated: boolean;
94
+ }[] | undefined;
95
+ }): {
96
+ line: number;
97
+ column: number | undefined;
98
+ generated: boolean;
99
+ };
100
+ /**
101
+ * Where a **frontmatter key** is declared in a note's file.
102
+ *
103
+ * {@link positionInBody} answers the same question for the body, and the two
104
+ * are separate because the body is what the compilers scan while frontmatter is
105
+ * what the linters read — a key sits *before* the body, so a body offset can
106
+ * never reach it.
107
+ *
108
+ * The search is deliberately scoped to the frontmatter block rather than run
109
+ * over the whole file. A bare search for the key would match the first place
110
+ * the word appears anywhere, which for a key like `name` or `type` is routinely
111
+ * a line of prose — sending the reader to a position that is not the problem,
112
+ * which is the one thing the located form exists to prevent.
113
+ *
114
+ * @param {string} raw - The file's full contents, frontmatter included.
115
+ * @param {string} key - The top-level frontmatter key.
116
+ * @param {string} [value] - When given, prefer the occurrence whose line also
117
+ * carries this text. A list-valued key (`aliases`) is reported at the entry
118
+ * that is wrong, not at the key that introduces it.
119
+ * @returns {{line?: number, column?: number}} Spreadable position fields, empty
120
+ * when the key cannot be located — dropped rather than guessed, as
121
+ * {@link formatDiagnostic} requires.
122
+ */
123
+ export function positionInFrontmatter(raw: string, key: string, value?: string): {
124
+ line?: number;
125
+ column?: number;
126
+ };
127
+ /**
128
+ * Where a literal sits in a text, so a finding about it can be opened.
129
+ *
130
+ * {@link positionInBody} maps an offset within a parsed note body, and
131
+ * {@link positionInFrontmatter} finds a key in the fence. This is the plainer
132
+ * case: a finding about a string the reader can see in a file that is neither —
133
+ * a manifest, a lockfile, a config.
134
+ *
135
+ * `@heroiclands/package-build` carries the same arithmetic for the files *it*
136
+ * reads. That is a duplicate worth naming: unlike the diagnostic *format* or a
137
+ * validation *rule*, "which line and column is this substring on" has exactly
138
+ * one correct answer and cannot drift into disagreement. The tidier arrangement
139
+ * is for that package to re-export this one — the dependency runs that way — and
140
+ * it should, next time either is touched.
141
+ *
142
+ * @param {string} text - The file's contents.
143
+ * @param {string} needle - The literal to locate.
144
+ * @param {number} [occurrence] - Which occurrence, 1-based. Repeats of one
145
+ * literal are otherwise indistinguishable.
146
+ * @returns {{line?: number, column?: number}} Spreadable position fields, empty
147
+ * when the literal is not there — dropped rather than guessed.
148
+ */
149
+ export function positionOfLiteral(text: string, needle: string, occurrence?: number): {
150
+ line?: number;
151
+ column?: number;
152
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Render the per-type item frontmatter reference.
3
+ *
4
+ * @param {object} [options] - Rendering options.
5
+ * @param {string} [options.title] - The page's H1.
6
+ * @param {string[]} [options.preamble] - Lines placed after the H1, before the
7
+ * first type. Written by the consumer, since only it knows what its page
8
+ * should link to.
9
+ * @param {string} [options.generatedBy] - What a reader should re-run to
10
+ * regenerate the page, named in the do-not-edit banner.
11
+ * @param {object} [options.config] - Resolved configuration. Defaults to the
12
+ * consumer's own.
13
+ * @returns {string} The complete markdown page.
14
+ */
15
+ export function renderItemFieldReference({ title, preamble, generatedBy, config, }?: {
16
+ title?: string | undefined;
17
+ preamble?: string[] | undefined;
18
+ generatedBy?: string | undefined;
19
+ config?: object | undefined;
20
+ }): string;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Write `value` at a dotted path, creating the intermediate objects.
3
+ *
4
+ * Insertion order is the emitted JSON's key order, so a declaration's order is
5
+ * the compiled document's order — which is what lets a field list replace a
6
+ * hand-written object literal without changing a single byte of output.
7
+ *
8
+ * @param {object} target - The object to write into (mutated).
9
+ * @param {string} dotted - Path, e.g. `"locations.flexible"`.
10
+ * @param {any} value - The value to set.
11
+ * @returns {object} `target`, for chaining.
12
+ */
13
+ export function setPath(target: object, dotted: string, value: any): object;
14
+ /**
15
+ * Read one declared field out of a note's frontmatter.
16
+ *
17
+ * @param {FieldSpec} field - The declaration.
18
+ * @param {object} fm - The note's frontmatter.
19
+ * @returns {any} The value to emit.
20
+ */
21
+ export function readField(field: FieldSpec, fm: object): any;
22
+ /**
23
+ * Turn a field declaration into the builder it declares.
24
+ *
25
+ * @param {readonly FieldSpec[]} fields - The declaration, in emission order.
26
+ * @returns {(fm: object) => object} A `system`-block builder.
27
+ */
28
+ export function buildFromFields(fields: readonly FieldSpec[]): (fm: object) => object;
29
+ /**
30
+ * The fields of a declaration an author actually writes.
31
+ *
32
+ * Constants and derived values are part of the emitted document but not part of
33
+ * the vocabulary, so every author-facing surface — the reference generator, a
34
+ * frontmatter linter, an unknown-key check — wants this list rather than the
35
+ * whole declaration.
36
+ *
37
+ * @param {readonly FieldSpec[]} fields - The declaration.
38
+ * @returns {FieldSpec[]} Only the fields with a frontmatter `name`.
39
+ */
40
+ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
41
+ /**
42
+ * @typedef {object} FieldSpec
43
+ * @property {string} to - Dotted path in the emitted `system` block.
44
+ * @property {string} [name] - Frontmatter key under `sohl:`, dotted for a
45
+ * nested one (`impact.die`). Absent means the value is not authored — see
46
+ * `value`.
47
+ * @property {string} [shape] - Human-readable shape, for documentation. Comes
48
+ * paired with `read` from one of the coercion constants below.
49
+ * @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
50
+ * How the raw frontmatter value becomes the emitted one. Identity if absent.
51
+ * @property {any} [default] - Emitted when the note does not carry the field.
52
+ * @property {boolean} [required] - Whether a note must carry it. A required
53
+ * field's `read` is expected to throw when it is missing.
54
+ * @property {"string"|"number"|"boolean"|"list"|"map"} [kind] - The value's
55
+ * shape, for the frontmatter linter (#19). Distinct from `shape`, which is
56
+ * prose for a reader, and from `read`, which is what the compiler does: a
57
+ * field may declare `kind` without changing a byte of what it emits, and
58
+ * several do — `weight` is coerced leniently but is still a number, and
59
+ * `weight: heavy` is an authoring mistake worth reporting where it was made.
60
+ * Absent means the lint makes no claim about the value.
61
+ * @property {string} [ref] - The content type a value addresses by shortcode,
62
+ * for the linter's dead-reference check. Only for references to a **note**:
63
+ * `bodyLocationCode` names a part inside a being's own body structure, not a
64
+ * note, so it declares none.
65
+ * @property {any|((fm: object) => any)} [value] - For a field with no `name`:
66
+ * the constant, or a function deriving it from the frontmatter.
67
+ * @property {string} describe - One line, for the author-facing reference.
68
+ */
69
+ /** Whatever the author wrote, unconverted. */
70
+ export const AS_AUTHORED: Readonly<{
71
+ shape: "as authored";
72
+ }>;
73
+ /** Coerced with `String()`. */
74
+ export const STRING: Readonly<{
75
+ shape: "string";
76
+ kind: "string";
77
+ read: (raw: any) => string;
78
+ }>;
79
+ /** Coerced with `Number()`, with a non-numeric or absent value reading `0`. */
80
+ export const NUMBER: Readonly<{
81
+ shape: "number";
82
+ kind: "number";
83
+ read: (raw: any) => number;
84
+ }>;
85
+ /** Coerced with `Boolean()`. */
86
+ export const BOOLEAN: Readonly<{
87
+ shape: "boolean";
88
+ kind: "boolean";
89
+ read: (raw: any) => boolean;
90
+ }>;
91
+ /**
92
+ * A number whose *absence* is meaningful: unset or blank ships `null`, and any
93
+ * other value goes through `Number()` unguarded (so a non-numeric one is
94
+ * `NaN`, not a silent `0` — an authoring mistake worth seeing).
95
+ */
96
+ export const NULLABLE_NUMBER: Readonly<{
97
+ shape: "number or unset";
98
+ kind: "number";
99
+ read: (raw: any) => number | null;
100
+ }>;
101
+ /**
102
+ * A number whose absence is meaningful, but whose *value* is guarded: unset
103
+ * ships `null`, anything else reads as a number defaulting to `0`.
104
+ */
105
+ export const NULLABLE_COUNT: Readonly<{
106
+ shape: "number or unset";
107
+ kind: "number";
108
+ read: (raw: any) => number | null;
109
+ }>;
110
+ /** Anything falsy — including a cleared `""` — ships `null`. */
111
+ export const BLANK_IS_NULL: Readonly<{
112
+ shape: "as authored, blank is unset";
113
+ read: (raw: any) => any;
114
+ }>;
115
+ /** Anything falsy — including a cleared `""` — falls back to the default. */
116
+ export const BLANK_IS_DEFAULT: Readonly<{
117
+ shape: "as authored, blank is the default";
118
+ read: (raw: any, { field }: {
119
+ field: any;
120
+ }) => any;
121
+ }>;
122
+ export type FieldSpec = {
123
+ /**
124
+ * - Dotted path in the emitted `system` block.
125
+ */
126
+ to: string;
127
+ /**
128
+ * - Frontmatter key under `sohl:`, dotted for a
129
+ * nested one (`impact.die`). Absent means the value is not authored — see
130
+ * `value`.
131
+ */
132
+ name?: string | undefined;
133
+ /**
134
+ * - Human-readable shape, for documentation. Comes
135
+ * paired with `read` from one of the coercion constants below.
136
+ */
137
+ shape?: string | undefined;
138
+ /**
139
+ * -
140
+ * How the raw frontmatter value becomes the emitted one. Identity if absent.
141
+ */
142
+ read?: ((raw: any, ctx: {
143
+ fm: object;
144
+ field: FieldSpec;
145
+ }) => any) | undefined;
146
+ /**
147
+ * - Emitted when the note does not carry the field.
148
+ */
149
+ default?: any;
150
+ /**
151
+ * - Whether a note must carry it. A required
152
+ * field's `read` is expected to throw when it is missing.
153
+ */
154
+ required?: boolean | undefined;
155
+ /**
156
+ * - The value's
157
+ * shape, for the frontmatter linter (#19). Distinct from `shape`, which is
158
+ * prose for a reader, and from `read`, which is what the compiler does: a
159
+ * field may declare `kind` without changing a byte of what it emits, and
160
+ * several do — `weight` is coerced leniently but is still a number, and
161
+ * `weight: heavy` is an authoring mistake worth reporting where it was made.
162
+ * Absent means the lint makes no claim about the value.
163
+ */
164
+ kind?: "string" | "number" | "boolean" | "map" | "list" | undefined;
165
+ /**
166
+ * - The content type a value addresses by shortcode,
167
+ * for the linter's dead-reference check. Only for references to a **note**:
168
+ * `bodyLocationCode` names a part inside a being's own body structure, not a
169
+ * note, so it declares none.
170
+ */
171
+ ref?: string | undefined;
172
+ /**
173
+ * - For a field with no `name`:
174
+ * the constant, or a function deriving it from the frontmatter.
175
+ */
176
+ value?: any | ((fm: object) => any);
177
+ /**
178
+ * - One line, for the author-facing reference.
179
+ */
180
+ describe: string;
181
+ };