@heroiclands/package-build 0.6.1 → 3.0.1
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-content-build.md +965 -0
- package/CHANGELOG.md +68 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -35
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- 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
|
+
};
|