@heroiclands/package-build 22.4.7 → 22.5.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 +23 -0
- package/CONTENT.md +83 -1
- package/bin/content-build.mjs +16 -2
- package/docs/api.md +49 -12
- package/docs/content-format.md +167 -0
- package/docs/diagnostics.md +44 -0
- package/engine/content-index.mjs +63 -21
- package/engine/content-links.mjs +60 -6
- package/engine/content-tables.mjs +29 -2
- package/engine/holdings.mjs +42 -17
- package/engine/index-records.mjs +25 -0
- package/engine/index.mjs +7 -0
- package/engine/note-state.mjs +213 -0
- package/engine/note-vocabulary.mjs +40 -2
- package/engine/pdf-build.mjs +4 -2
- package/engine/populations.mjs +557 -0
- package/engine/site-build.mjs +8 -0
- package/engine/sql-tables.mjs +157 -4
- package/engine/stub-lint.mjs +237 -0
- package/engine/wikilink-syntax.mjs +27 -1
- package/package.json +1 -1
- package/types/engine/content-index.d.mts +19 -5
- package/types/engine/content-links.d.mts +3 -3
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/holdings.d.mts +17 -6
- package/types/engine/index-records.d.mts +22 -0
- package/types/engine/index.d.mts +3 -0
- package/types/engine/note-state.d.mts +110 -0
- package/types/engine/note-vocabulary.d.mts +15 -0
- package/types/engine/populations.d.mts +121 -0
- package/types/engine/stub-lint.d.mts +51 -0
- package/types/engine/wikilink-syntax.d.mts +19 -1
package/types/engine/index.d.mts
CHANGED
|
@@ -15,8 +15,11 @@ export * as derivedFields from "./derived-fields.mjs";
|
|
|
15
15
|
export * as homepage from "./homepage.mjs";
|
|
16
16
|
export * as noteSchemas from "./note-schemas.mjs";
|
|
17
17
|
export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
18
|
+
export * as noteState from "./note-state.mjs";
|
|
19
|
+
export * as stubLint from "./stub-lint.mjs";
|
|
18
20
|
export * as placeRelations from "./place-relations.mjs";
|
|
19
21
|
export * as holdings from "./holdings.mjs";
|
|
22
|
+
export * as populations from "./populations.mjs";
|
|
20
23
|
export * as marketClass from "./market-class.mjs";
|
|
21
24
|
export * as mapPlaces from "./map-places.mjs";
|
|
22
25
|
export * as mapLayout from "./map-layout.mjs";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a note's body is empty.
|
|
3
|
+
*
|
|
4
|
+
* > **The body is empty when everything after the closing frontmatter fence is
|
|
5
|
+
* > whitespace.** Nothing else is empty.
|
|
6
|
+
*
|
|
7
|
+
* The severity is the point: **the test must be one a person can apply by
|
|
8
|
+
* looking at the file.** Every softer rule makes two files that look different
|
|
9
|
+
* behave the same, and turns "why did my page disappear?" into an
|
|
10
|
+
* investigation. So an HTML comment, a horizontal rule, a lone heading and
|
|
11
|
+
* `_To be written._` are all bodies — each renders to little or nothing, and
|
|
12
|
+
* {@link module:engine/stub-lint} reports them rather than treating them as
|
|
13
|
+
* either state.
|
|
14
|
+
*
|
|
15
|
+
* An HTML comment is deliberately not empty. An invisible marker deciding
|
|
16
|
+
* whether a page exists is the failure the retired `draft:` field was removed
|
|
17
|
+
* for, and reintroducing it in comment form would be worse, because nothing
|
|
18
|
+
* would report it.
|
|
19
|
+
*
|
|
20
|
+
* @param {string|null|undefined} body - The note body, frontmatter stripped.
|
|
21
|
+
* @returns {boolean} Whether the body is empty.
|
|
22
|
+
*/
|
|
23
|
+
export function isEmptyBody(body: string | null | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether an empty body makes a note of this type a stub.
|
|
26
|
+
*
|
|
27
|
+
* Two types are **structural** and exempt, and the measurement forced the
|
|
28
|
+
* question: a `folder` note's page is a generated section index and the
|
|
29
|
+
* `homepage`'s is the site's front door, so both have no body by design. If an
|
|
30
|
+
* empty body meant "no page" for them, a tree would lose a page per folder plus
|
|
31
|
+
* its own front door, and the build would report nothing.
|
|
32
|
+
*
|
|
33
|
+
* Read from {@link module:engine/note-vocabulary.NOTE_VOCABULARY}, where every
|
|
34
|
+
* type declares `stubbable` — so a type added without answering the question
|
|
35
|
+
* fails the guard rather than being classified by a default nobody chose. A
|
|
36
|
+
* type the registry does not declare at all is stubbable: the rule is about the
|
|
37
|
+
* body, and an unknown type has no exemption to claim.
|
|
38
|
+
*
|
|
39
|
+
* @param {string|null|undefined} type - The note's `type`.
|
|
40
|
+
* @param {Readonly<Record<string, {stubbable?: boolean}>>} [vocabulary] - The
|
|
41
|
+
* registry to read.
|
|
42
|
+
* @returns {boolean} Whether an empty body suppresses this type's page.
|
|
43
|
+
*/
|
|
44
|
+
export function isStubbableType(type: string | null | undefined, vocabulary?: Readonly<Record<string, {
|
|
45
|
+
stubbable?: boolean;
|
|
46
|
+
}>>): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a note is a stub: an empty body on a type an empty body suppresses.
|
|
49
|
+
*
|
|
50
|
+
* The one place the two halves are put together, so the index, the lint and the
|
|
51
|
+
* guard cannot answer it differently.
|
|
52
|
+
*
|
|
53
|
+
* @param {Record<string, any>|null|undefined} frontmatter - Parsed frontmatter.
|
|
54
|
+
* @param {string|null|undefined} body - The note body.
|
|
55
|
+
* @param {Readonly<Record<string, {stubbable?: boolean}>>} [vocabulary] - The
|
|
56
|
+
* registry to read.
|
|
57
|
+
* @returns {boolean} Whether the note is a stub.
|
|
58
|
+
*/
|
|
59
|
+
export function isStubNote(frontmatter: Record<string, any> | null | undefined, body: string | null | undefined, vocabulary?: Readonly<Record<string, {
|
|
60
|
+
stubbable?: boolean;
|
|
61
|
+
}>>): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The placeholder phrases a body reduces to, or an empty list.
|
|
64
|
+
*
|
|
65
|
+
* A body is reduced by dropping every line that carries no content of its own —
|
|
66
|
+
* a heading, a thematic break, an HTML comment, a blank line — and then asking
|
|
67
|
+
* whether every line that is left is a placeholder. A body with some real prose
|
|
68
|
+
* beside its unwritten sections is **not** reduced: a `TBD` under a heading in
|
|
69
|
+
* a note that is honestly a draft is not a defect, and this fires only when a
|
|
70
|
+
* body reduces to nothing but such phrases.
|
|
71
|
+
*
|
|
72
|
+
* The phrases are returned with the 1-based offset of the line each sat on
|
|
73
|
+
* within the body, so a finding can be located rather than merely stated.
|
|
74
|
+
*
|
|
75
|
+
* @param {string|null|undefined} body - The note body.
|
|
76
|
+
* @returns {Array<{phrase: string, line: number}>} The phrases, in body order.
|
|
77
|
+
* Empty for a body that is empty, or that carries prose of its own.
|
|
78
|
+
*/
|
|
79
|
+
export function placeholderBody(body: string | null | undefined): Array<{
|
|
80
|
+
phrase: string;
|
|
81
|
+
line: number;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* How many words a body holds, counting only the prose a reader meets.
|
|
85
|
+
*
|
|
86
|
+
* Fence blocks, headings, thematic breaks and HTML comments are dropped, and a
|
|
87
|
+
* wikilink counts as the one word it renders — `[[affiliation-meivor|Mëivōr]]`
|
|
88
|
+
* is a word, not three. What is left is split on whitespace.
|
|
89
|
+
*
|
|
90
|
+
* @param {string|null|undefined} body - The note body.
|
|
91
|
+
* @returns {number} The word count.
|
|
92
|
+
*/
|
|
93
|
+
export function bodyWordCount(body: string | null | undefined): number;
|
|
94
|
+
/**
|
|
95
|
+
* The three states, in the order they read as a ladder.
|
|
96
|
+
*
|
|
97
|
+
* @type {ReadonlyArray<string>}
|
|
98
|
+
*/
|
|
99
|
+
export const NOTE_STATES: ReadonlyArray<string>;
|
|
100
|
+
/**
|
|
101
|
+
* The phrases that stand in for prose nobody has written.
|
|
102
|
+
*
|
|
103
|
+
* A body consisting of nothing but these is an abandoned draft, not a stub:
|
|
104
|
+
* it looks written, publishes a page, and says nothing. The list is matched
|
|
105
|
+
* whole-line, case-insensitively, after trailing punctuation is dropped — a
|
|
106
|
+
* line reading `TBD.` and one reading `tbd` are the same line.
|
|
107
|
+
*
|
|
108
|
+
* @type {ReadonlyArray<string>}
|
|
109
|
+
*/
|
|
110
|
+
export const PLACEHOLDER_PHRASES: ReadonlyArray<string>;
|
|
@@ -333,6 +333,16 @@ export type DataFieldSpec = {
|
|
|
333
333
|
* - **`null`** — the type has a `subType` whose values the specification does
|
|
334
334
|
* not yet enumerate. Presence is permitted and the value is unchecked.
|
|
335
335
|
* - **a list** — the closed set of values, and anything else is a finding.
|
|
336
|
+
*
|
|
337
|
+
* `stubbable` says whether an empty body suppresses this type's page. Every
|
|
338
|
+
* type declares it, so adding one means answering the question rather than
|
|
339
|
+
* inheriting a default nobody chose — and a guard derives the check from this
|
|
340
|
+
* registry's own key list, so an omission is a red test.
|
|
341
|
+
*
|
|
342
|
+
* Two types answer **false**, and both are structural: a `folder` note's page
|
|
343
|
+
* is a generated section index and a `homepage`'s is the site's front door, so
|
|
344
|
+
* each is complete with no body at all. An empty body on one of them keeps its
|
|
345
|
+
* address and resolves as `full`.
|
|
336
346
|
*/
|
|
337
347
|
export type TypeVocabulary = {
|
|
338
348
|
/**
|
|
@@ -344,6 +354,11 @@ export type TypeVocabulary = {
|
|
|
344
354
|
* values, as above.
|
|
345
355
|
*/
|
|
346
356
|
subTypes?: readonly string[] | null | undefined;
|
|
357
|
+
/**
|
|
358
|
+
* - Whether an empty body makes a note of this
|
|
359
|
+
* type a stub — see {@link module :engine/note-state.isStubbableType}.
|
|
360
|
+
*/
|
|
361
|
+
stubbable: boolean;
|
|
347
362
|
/**
|
|
348
363
|
* - A
|
|
349
364
|
* check of the whole note, run by the frontmatter lint beside the field
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check a place's population against the corpus around it.
|
|
3
|
+
*
|
|
4
|
+
* Declared on the `place` vocabulary beside `data.population`, so the lint
|
|
5
|
+
* runs it with the index every reference check resolves through. Without an
|
|
6
|
+
* index the questions cannot be asked and nothing is reported.
|
|
7
|
+
*
|
|
8
|
+
* Three rules, each reported on the note being linted: `over-held land` and
|
|
9
|
+
* `over-full region` on the place whose figure is exceeded, and
|
|
10
|
+
* `oversized settlement` on the settlement that exceeds one.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} note - The note, as the link index hands it over.
|
|
13
|
+
* @param {object} [opts]
|
|
14
|
+
* @param {object} [opts.index] - The link index, holding the whole corpus.
|
|
15
|
+
* @returns {object[]} Findings.
|
|
16
|
+
*/
|
|
17
|
+
export function checkPopulation(note: object, { index }?: {
|
|
18
|
+
index?: object | undefined;
|
|
19
|
+
}): object[];
|
|
20
|
+
/**
|
|
21
|
+
* Check the populations a `doc` note cites against the notes they belong to.
|
|
22
|
+
*
|
|
23
|
+
* A reference page restates figures the place and affiliation notes own, and
|
|
24
|
+
* restated figures drift. Each is located at the figure it wrote, counting
|
|
25
|
+
* repeats so two rows carrying one number land on their own lines.
|
|
26
|
+
*
|
|
27
|
+
* A citation naming a note this package does not hold is skipped: a fetched
|
|
28
|
+
* index carries no figure, so there is nothing to disagree with.
|
|
29
|
+
*
|
|
30
|
+
* Declared as the `doc` vocabulary's type-level check.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} note - The note, as the link index hands it over.
|
|
33
|
+
* @param {object} [opts]
|
|
34
|
+
* @param {object} [opts.index] - The link index, holding the whole corpus.
|
|
35
|
+
* @returns {object[]} Findings.
|
|
36
|
+
*/
|
|
37
|
+
export function checkCitedPopulations(note: object, { index }?: {
|
|
38
|
+
index?: object | undefined;
|
|
39
|
+
}): object[];
|
|
40
|
+
/**
|
|
41
|
+
* How far a parent's figure may fall below the sum of what it contains before
|
|
42
|
+
* that is a finding.
|
|
43
|
+
*
|
|
44
|
+
* A figure written to two significant digits carries a half-unit of error in
|
|
45
|
+
* the second digit, and that error is widest at the bottom of a decade — half
|
|
46
|
+
* a unit on `1.0` is five per cent, against half a unit on `9.9` at a twentieth
|
|
47
|
+
* of that. The widest case is the one a tolerance has to admit, so the whole
|
|
48
|
+
* scale is held to five per cent.
|
|
49
|
+
*
|
|
50
|
+
* @type {number}
|
|
51
|
+
*/
|
|
52
|
+
export const POPULATION_TOLERANCE: number;
|
|
53
|
+
/**
|
|
54
|
+
* The rules this module enforces, each `name` the prefix its findings carry.
|
|
55
|
+
*
|
|
56
|
+
* The list is the module's own statement of what it checks: a reader learns
|
|
57
|
+
* the whole surface from it, and the suite derives a case per entry from it, so
|
|
58
|
+
* a rule cannot be added without evidence that it fires.
|
|
59
|
+
*
|
|
60
|
+
* @type {ReadonlyArray<{name: string, describe: string}>}
|
|
61
|
+
*/
|
|
62
|
+
export const POPULATION_RULES: ReadonlyArray<{
|
|
63
|
+
name: string;
|
|
64
|
+
describe: string;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* One place or affiliation as the rules read it.
|
|
68
|
+
*/
|
|
69
|
+
export type PopulationNode = {
|
|
70
|
+
/**
|
|
71
|
+
* Lower case.
|
|
72
|
+
*/
|
|
73
|
+
shortcode: string;
|
|
74
|
+
type: "place" | "affiliation";
|
|
75
|
+
subType: string;
|
|
76
|
+
/**
|
|
77
|
+
* What a finding calls it.
|
|
78
|
+
*/
|
|
79
|
+
title: string;
|
|
80
|
+
/**
|
|
81
|
+
* The stated figure, absent where unstated.
|
|
82
|
+
*/
|
|
83
|
+
population?: number | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* A place's enclosing places, an affiliation's
|
|
86
|
+
* superiors.
|
|
87
|
+
*/
|
|
88
|
+
parents: string[];
|
|
89
|
+
/**
|
|
90
|
+
* What an affiliation holds.
|
|
91
|
+
*/
|
|
92
|
+
domains: string[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The whole tree, indexed the three ways the rules read it.
|
|
96
|
+
*/
|
|
97
|
+
export type Corpus = {
|
|
98
|
+
/**
|
|
99
|
+
* By shortcode.
|
|
100
|
+
*/
|
|
101
|
+
places: Map<string, PopulationNode>;
|
|
102
|
+
/**
|
|
103
|
+
* By shortcode.
|
|
104
|
+
*/
|
|
105
|
+
affiliations: Map<string, PopulationNode>;
|
|
106
|
+
/**
|
|
107
|
+
* Place shortcode → the places
|
|
108
|
+
* whose `parents` name it.
|
|
109
|
+
*/
|
|
110
|
+
childrenOf: Map<string, string[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Place shortcode → the polities
|
|
113
|
+
* whose `domains` name it.
|
|
114
|
+
*/
|
|
115
|
+
holdersOf: Map<string, string[]>;
|
|
116
|
+
/**
|
|
117
|
+
* `type-shortcode` → the node,
|
|
118
|
+
* for the citation rule.
|
|
119
|
+
*/
|
|
120
|
+
byAddress: Map<string, PopulationNode>;
|
|
121
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state of every note in a tree, checked and counted.
|
|
3
|
+
*
|
|
4
|
+
* Read from the content index rather than from a walk of this pass's own, like
|
|
5
|
+
* every other lint: the command derives the corpus once and hands it to each
|
|
6
|
+
* pass, so no two findings are drawn from different ideas of which files the
|
|
7
|
+
* content is. The **body** is read from the file, because the index carries
|
|
8
|
+
* what is *about* a note and never the note's text.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} contentBase - Root of the content tree.
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {readonly object[]} opts.records - Index records the caller derived.
|
|
13
|
+
* @param {string} [opts.contentPackage] - Named in the counts line.
|
|
14
|
+
* @returns {{findings: object[], counts: {full: number, draft: number,
|
|
15
|
+
* stub: number}, byType: Array<{type: string, full: number, draft: number,
|
|
16
|
+
* stub: number}>, oldestDrafts: Array<{file: string, modified: Date}>,
|
|
17
|
+
* summary: string[]}} The findings, the ladder's counts, and the lines a
|
|
18
|
+
* reader is shown as prose rather than as findings.
|
|
19
|
+
*/
|
|
20
|
+
export function lintNoteStates(contentBase: string, { records, contentPackage }?: {
|
|
21
|
+
records: readonly object[];
|
|
22
|
+
contentPackage?: string | undefined;
|
|
23
|
+
}): {
|
|
24
|
+
findings: object[];
|
|
25
|
+
counts: {
|
|
26
|
+
full: number;
|
|
27
|
+
draft: number;
|
|
28
|
+
stub: number;
|
|
29
|
+
};
|
|
30
|
+
byType: Array<{
|
|
31
|
+
type: string;
|
|
32
|
+
full: number;
|
|
33
|
+
draft: number;
|
|
34
|
+
stub: number;
|
|
35
|
+
}>;
|
|
36
|
+
oldestDrafts: Array<{
|
|
37
|
+
file: string;
|
|
38
|
+
modified: Date;
|
|
39
|
+
}>;
|
|
40
|
+
summary: string[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The word count below which an unmarked body is asked whether it is a draft.
|
|
44
|
+
*
|
|
45
|
+
* A threshold, so it is a value the rule *is* rather than a census: twenty-five
|
|
46
|
+
* words is about two sentences, which is where a note stops reading as a
|
|
47
|
+
* statement and starts reading as a beginning.
|
|
48
|
+
*
|
|
49
|
+
* @type {number}
|
|
50
|
+
*/
|
|
51
|
+
export const SHORT_BODY_WORDS: number;
|
|
@@ -92,6 +92,20 @@ export function unlabelledLinkMessage(target: string): string;
|
|
|
92
92
|
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
93
93
|
*/
|
|
94
94
|
export function unresolvedAddressMessage(target: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* What an author linking to a **stub** is told.
|
|
97
|
+
*
|
|
98
|
+
* Strictly better than "resolves to no note", and available only because a stub
|
|
99
|
+
* is in the index: the note exists, its file can be named, and the reason the
|
|
100
|
+
* link cannot land is a fact about that file rather than a guess about a
|
|
101
|
+
* shortcode. So the message names the file to open and offers both corrections
|
|
102
|
+
* — the link was either premature or the note is owed a body.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} target - The address as authored.
|
|
105
|
+
* @param {string} [file] - Where the stub is, relative to the content root.
|
|
106
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
107
|
+
*/
|
|
108
|
+
export function stubAddressMessage(target: string, file?: string): string;
|
|
95
109
|
/**
|
|
96
110
|
* What an author writing a short address more than one package publishes is
|
|
97
111
|
* told.
|
|
@@ -130,16 +144,18 @@ export function ambiguousAddressMessage(target: string, packages?: Iterable<stri
|
|
|
130
144
|
* @param {string} [finding.anchor] - For `unknown-anchor`, the section named.
|
|
131
145
|
* @param {string} [finding.type] - For `not-an-asset`, the type the address
|
|
132
146
|
* named.
|
|
147
|
+
* @param {string} [finding.stub] - For `stub`, where the stub is.
|
|
133
148
|
* @returns {string} The message.
|
|
134
149
|
* @throws {Error} On a reason outside the closed set — a resolver inventing one
|
|
135
150
|
* would otherwise report a link with no explanation at all.
|
|
136
151
|
*/
|
|
137
|
-
export function linkFindingMessage({ reason, target, packages, anchor, type }: {
|
|
152
|
+
export function linkFindingMessage({ reason, target, packages, anchor, type, stub }: {
|
|
138
153
|
reason: string;
|
|
139
154
|
target: string;
|
|
140
155
|
packages?: Iterable<string> | undefined;
|
|
141
156
|
anchor?: string | undefined;
|
|
142
157
|
type?: string | undefined;
|
|
158
|
+
stub?: string | undefined;
|
|
143
159
|
}): string;
|
|
144
160
|
/**
|
|
145
161
|
* Whether a parsed link addresses a section of the page it is written on.
|
|
@@ -221,6 +237,8 @@ export const WIKILINK: RegExp;
|
|
|
221
237
|
* - `unknown-type` — definitely qualified, but names no type this build knows.
|
|
222
238
|
* - `not-an-asset` — a real address, embedded where only a file can be drawn.
|
|
223
239
|
* - `unresolved` — parses as an address, and nothing publishes it.
|
|
240
|
+
* - `stub` — the address names a note this tree holds whose body is empty, so
|
|
241
|
+
* it publishes no page for the link to reach.
|
|
224
242
|
* - `ambiguous` — more than one package publishes the short address.
|
|
225
243
|
* - `unknown-anchor` — the address resolved, the `#section` it names did not.
|
|
226
244
|
* - `no-content-index` — the address names a package declared
|