@heroiclands/package-build 20.3.0 → 20.4.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 +97 -0
- package/CONTENT.md +111 -92
- package/README.md +2 -9
- package/bin/content-build.mjs +70 -61
- package/bin/package-build.mjs +2 -2
- package/config.mjs +6 -8
- package/content-config.mjs +187 -64
- package/docs/content-format.md +57 -68
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +36 -36
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +12 -11
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +36 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +8 -8
- package/engine/code-fences.mjs +3 -3
- package/engine/compendiums.mjs +8 -8
- package/engine/compile-corpus.mjs +4 -5
- package/engine/content-address.mjs +15 -15
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format-check.mjs +24 -25
- package/engine/content-format.mjs +8 -8
- package/engine/content-html.mjs +202 -0
- package/engine/content-icons.mjs +225 -98
- package/engine/content-index.mjs +17 -17
- package/engine/content-links.mjs +30 -30
- package/engine/content-lint.mjs +20 -24
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +7 -8
- package/engine/content-tree.mjs +1 -1
- package/engine/derived-fields.mjs +174 -0
- package/engine/diagnostics.mjs +3 -4
- package/engine/document-subtypes.mjs +6 -7
- package/engine/field-reference.mjs +8 -8
- package/engine/field-spec.mjs +29 -30
- package/engine/folder-notes.mjs +14 -14
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +64 -69
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +39 -39
- package/engine/helpers.mjs +63 -54
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +14 -8
- package/engine/item-compiler.mjs +54 -29
- package/engine/item-docs.mjs +11 -13
- package/engine/item-registry.mjs +13 -13
- package/engine/journals.mjs +15 -15
- package/engine/macros.mjs +3 -3
- package/engine/map-notes.mjs +6 -6
- package/engine/metadata-index.mjs +5 -5
- package/engine/note-claims.mjs +36 -34
- package/engine/note-ids.mjs +5 -5
- package/engine/note-package.mjs +4 -4
- package/engine/note-renames.mjs +3 -3
- package/engine/note-schemas.mjs +3 -3
- package/engine/note-vocabulary.mjs +27 -26
- package/engine/pack-config.mjs +13 -13
- package/engine/pack-router.mjs +0 -0
- package/engine/prose-config.mjs +9 -9
- package/engine/prose-lint.mjs +12 -13
- package/engine/region-events.mjs +2 -2
- package/engine/retired-fields.mjs +24 -22
- package/engine/runtime-only-fields.mjs +1 -1
- package/engine/scene-levels.mjs +1 -1
- package/engine/scenes.mjs +19 -20
- package/engine/schema-check.mjs +20 -20
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +45 -47
- package/engine/site-index.mjs +17 -18
- package/engine/sql-tables.mjs +21 -13
- package/engine/subtype-registry.mjs +5 -4
- package/engine/system-block.mjs +18 -19
- package/engine/systems.mjs +2 -2
- package/engine/web-wikilinks.mjs +35 -35
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +39 -39
- package/hm3/actors.mjs +22 -22
- package/hm3/document-subtypes.mjs +5 -5
- package/hm3/index.mjs +2 -2
- package/hm3/item-builders.mjs +1 -1
- package/hm3/item-fields.mjs +3 -3
- package/hm3/items.mjs +20 -8
- package/hm3/template-priority.mjs +2 -2
- package/lang.mjs +3 -3
- package/manifest.mjs +17 -20
- package/package.json +1 -2
- package/release.mjs +3 -3
- package/sohl/actors.mjs +28 -28
- package/sohl/affiliation-standings.mjs +2 -2
- package/sohl/being-info.mjs +5 -5
- package/sohl/default-item-art.mjs +5 -5
- package/sohl/document-subtypes.mjs +5 -5
- package/sohl/index.mjs +3 -3
- package/sohl/item-builders.mjs +7 -7
- package/sohl/item-fields.mjs +8 -9
- package/sohl/items.mjs +20 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +7 -7
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +25 -24
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +14 -13
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +3 -3
- package/types/engine/base-compiler.d.mts +17 -19
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +1 -1
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/compendiums.d.mts +3 -3
- package/types/engine/compile-corpus.d.mts +1 -1
- package/types/engine/content-address.d.mts +11 -11
- package/types/engine/content-format-check.d.mts +6 -6
- package/types/engine/content-format.d.mts +2 -2
- package/types/engine/content-html.d.mts +78 -0
- package/types/engine/content-icons.d.mts +150 -62
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +2 -2
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/derived-fields.d.mts +101 -0
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/document-subtypes.d.mts +3 -3
- package/types/engine/field-spec.d.mts +37 -39
- package/types/engine/folder-notes.d.mts +4 -5
- package/types/engine/foreign-catalog.d.mts +3 -3
- package/types/engine/foundry-entries.d.mts +6 -7
- package/types/engine/frontmatter-lint.d.mts +7 -7
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +25 -27
- package/types/engine/homepage.d.mts +16 -18
- package/types/engine/ids.d.mts +13 -13
- package/types/engine/index-records.d.mts +3 -3
- package/types/engine/index.d.mts +2 -0
- package/types/engine/item-compiler.d.mts +21 -5
- package/types/engine/item-docs.d.mts +2 -2
- package/types/engine/item-registry.d.mts +6 -6
- package/types/engine/journals.d.mts +3 -3
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/metadata-index.d.mts +4 -4
- package/types/engine/note-claims.d.mts +16 -15
- package/types/engine/note-ids.d.mts +4 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-renames.d.mts +3 -3
- package/types/engine/note-vocabulary.d.mts +9 -8
- package/types/engine/pack-config.d.mts +3 -3
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/prose-config.d.mts +9 -9
- package/types/engine/prose-lint.d.mts +3 -4
- package/types/engine/region-events.d.mts +2 -2
- package/types/engine/retired-fields.d.mts +10 -9
- package/types/engine/schema-check.d.mts +11 -11
- package/types/engine/site-build.d.mts +18 -19
- package/types/engine/site-index.d.mts +3 -4
- package/types/engine/sql-tables.d.mts +11 -5
- package/types/engine/subtype-registry.d.mts +3 -3
- package/types/engine/system-block.d.mts +3 -3
- package/types/engine/web-wikilinks.d.mts +7 -7
- package/types/engine/wikilink-syntax.d.mts +17 -17
- package/types/engine/wikilinks.d.mts +13 -12
- package/types/hm3/actors.d.mts +1 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +1 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/affiliation-standings.d.mts +2 -2
- package/types/sohl/being-info.d.mts +2 -2
- package/types/sohl/document-subtypes.d.mts +1 -1
- package/types/sohl/items.d.mts +1 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
* address other packages link to. Stating it twice is how a manifest comes to
|
|
20
20
|
* assert a URL that resolves at build time and 404s for the reader.
|
|
21
21
|
*
|
|
22
|
-
* **A page's URL is its address** — `<package>/<type>-<shortcode
|
|
23
|
-
* it carries **no `<system>` segment** even though the canonical address does
|
|
24
|
-
*
|
|
22
|
+
* **A page's URL is its address** — `<package>/<type>-<shortcode>/`, and
|
|
23
|
+
* it carries **no `<system>` segment** even though the canonical address does.
|
|
24
|
+
* That is not an omission: a note publishes one page however many
|
|
25
25
|
* systems' documents it compiles into, so there is nothing for the segment to
|
|
26
26
|
* distinguish, and adding it would split one page's URL in two. The canonical
|
|
27
27
|
* address names a *document*; this names a *page*. It
|
|
28
|
-
*
|
|
28
|
+
* is not derived from `name.full`, which would make a display string load-bearing
|
|
29
29
|
* in three separate ways: a rename moved the URL and nothing redirected, two
|
|
30
30
|
* notes in one section could derive the same URL so a uniqueness check had to
|
|
31
31
|
* run, and a long name had to be abbreviated through a word table to keep the
|
|
@@ -62,7 +62,7 @@ export { DEFAULT_ADDRESS_SCHEME };
|
|
|
62
62
|
*/
|
|
63
63
|
export { NO_SYSTEM };
|
|
64
64
|
|
|
65
|
-
/** The knowledgebase's mount within this package's site
|
|
65
|
+
/** The knowledgebase's mount within this package's site. */
|
|
66
66
|
export const KB_PREFIX = "kb/";
|
|
67
67
|
|
|
68
68
|
/**
|
|
@@ -73,7 +73,7 @@ export const KB_PREFIX = "kb/";
|
|
|
73
73
|
* key's **last two segments** — which is what makes a manifest entry's `path`
|
|
74
74
|
* derivable from the key it is filed under rather than transported beside it.
|
|
75
75
|
*
|
|
76
|
-
* It
|
|
76
|
+
* It is not the key's whole tail: the key carries a
|
|
77
77
|
* `<system>` segment, so its tail is now `system-type-shortcode` and a slug is
|
|
78
78
|
* the tail with that segment dropped. The behaviour here is unchanged, and
|
|
79
79
|
* deliberately — a page has no system to name (see the module note above), so
|
|
@@ -131,9 +131,9 @@ export function addressSlug(fm) {
|
|
|
131
131
|
*
|
|
132
132
|
* **It is a pure function of the frontmatter**, and takes no options. Nothing
|
|
133
133
|
* about the file the note was read from reaches it: the `README.md` convention
|
|
134
|
-
* that made one note address a whole section is retired with the section itself
|
|
135
|
-
*
|
|
136
|
-
* It
|
|
134
|
+
* that made one note address a whole section is retired with the section itself,
|
|
135
|
+
* so every note is addressed alike and there is one rule and no branch.
|
|
136
|
+
* It takes no address scheme; the `landing` rule it once validated is
|
|
137
137
|
* discarded; with that key retired, `prefix` was the only thing left in the
|
|
138
138
|
* scheme and the paragraph above is the reason it never applied.
|
|
139
139
|
*
|
|
@@ -169,9 +169,9 @@ export function packageAddress(fm) {
|
|
|
169
169
|
* merge is a real conflict rather than an artefact of two packages sharing a
|
|
170
170
|
* namespace. `(type, shortcode)` alone is unique only *within* a package, and
|
|
171
171
|
* two independently authored packages reaching for the same short string is a
|
|
172
|
-
* matter of time
|
|
172
|
+
* matter of time.
|
|
173
173
|
*
|
|
174
|
-
* **The system segment
|
|
174
|
+
* **The system segment.** A package may ship content for more than one
|
|
175
175
|
* system, and one note then compiles into a document per system — an actor in
|
|
176
176
|
* `actors-sohl` *and* an actor in `actors-hm3`. Without a system segment both
|
|
177
177
|
* land on one key, so the address cannot name either of them. `harn-ensemble`
|
|
@@ -234,7 +234,7 @@ export function blockSystem(keyPath) {
|
|
|
234
234
|
/**
|
|
235
235
|
* Expand a written address to the one canonical address it names.
|
|
236
236
|
*
|
|
237
|
-
* **An omitted segment defaults from where the link is written**
|
|
237
|
+
* **An omitted segment defaults from where the link is written** — it is
|
|
238
238
|
* not a wildcard, and resolution is not a search. Package omitted means the
|
|
239
239
|
* citing note's own; system omitted means {@link blockSystem} of the key path it
|
|
240
240
|
* was written under. So every short form has exactly one expansion, computed
|
|
@@ -315,8 +315,8 @@ export const CANONICAL_KEY_SEGMENTS = 4;
|
|
|
315
315
|
* `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
|
|
316
316
|
* so the hyphen is purely a separator and the count alone determines every
|
|
317
317
|
* field. That is enforced at each of the three sources rather than assumed of
|
|
318
|
-
* the data: shortcodes by `content-lint.mjs
|
|
319
|
-
* `defineConfig
|
|
318
|
+
* the data: shortcodes by `content-lint.mjs`, `contentPackage` by
|
|
319
|
+
* `defineConfig`, and types are bare words. Were any of them free to
|
|
320
320
|
* carry a hyphen, no amount of counting would recover the fields and the reader
|
|
321
321
|
* would need a vocabulary to match against instead.
|
|
322
322
|
*
|
|
@@ -440,7 +440,7 @@ export const DOCUMENT_ID_NAMESPACE = "document";
|
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
442
|
* The Foundry `_id` of the document a note compiles into, derived from its
|
|
443
|
-
* canonical address
|
|
443
|
+
* canonical address.
|
|
444
444
|
*
|
|
445
445
|
* A note used to author this — an opaque 16-character string, one per note,
|
|
446
446
|
* that said nothing its address did not and that no check guaranteed. The
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The charset authored content is held to, so a book can choose its face
|
|
15
|
+
* The charset authored content is held to, so a book can choose its face.
|
|
16
16
|
*
|
|
17
17
|
* The packs and the website render in whatever font the reader's browser or
|
|
18
18
|
* Foundry supplies, and a glyph nobody has is somebody else's problem. **A book
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The things the content format asserts that can be checked
|
|
15
|
+
* The things the content format asserts that can be checked.
|
|
16
16
|
*
|
|
17
17
|
* `content-format.mjs` reads the specification as data; this compares it
|
|
18
18
|
* against the three worlds it makes claims about — the systems it maps onto,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* **The format does not define the `sohl:` or `hm3:` schemas.** Each system
|
|
24
24
|
* defines its own, and its published `schema.json` is the authoritative
|
|
25
|
-
* statement of it
|
|
25
|
+
* statement of it. So a mapping row is a *claim*: `data.weight` reaches
|
|
26
26
|
* `system.weightBase` in SoHL. If SoHL declares no such field the two disagree,
|
|
27
27
|
* and which of them is wrong is a question for a person — but that they
|
|
28
28
|
* disagree is a fact a build can establish.
|
|
@@ -34,17 +34,16 @@
|
|
|
34
34
|
*
|
|
35
35
|
* **A target is resolved against the union of the system's subtypes.** The
|
|
36
36
|
* mapping tables say which system field a shared source reaches; *which
|
|
37
|
-
* document subtype receives it* is the note-type → subtype map, which
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* narrows to the subtype when #79 lands.
|
|
37
|
+
* document subtype receives it* is the note-type → subtype map, which does not
|
|
38
|
+
* exist yet. Resolving per subtype before that map exists would mean inferring
|
|
39
|
+
* it from the prose around each table, which is precisely the transcription
|
|
40
|
+
* this whole module avoids. So the question asked here is "does any schema
|
|
41
|
+
* declare this field?", and it narrows to the subtype once the map lands.
|
|
43
42
|
*
|
|
44
43
|
* ## The corpus against the declared vocabulary
|
|
45
44
|
*
|
|
46
45
|
* Every authored note is measured against the per-type `data` tables. Three
|
|
47
|
-
* classes of finding come out of them,
|
|
46
|
+
* classes of finding come out of them, one per slice of the migration:
|
|
48
47
|
*
|
|
49
48
|
* | class | what it means |
|
|
50
49
|
* | --- | --- |
|
|
@@ -57,8 +56,8 @@
|
|
|
57
56
|
* format, so a failing check would be red on day one in every repository and
|
|
58
57
|
* would stay red for the length of the epic — which is a check nobody can act
|
|
59
58
|
* on and everybody learns to skip. The counts are the migration's progress bar
|
|
60
|
-
* instead, and `--strict` turns them fatal
|
|
61
|
-
*
|
|
59
|
+
* instead, and `--strict` turns them fatal — turned on slice by slice, as each
|
|
60
|
+
* class reaches zero.
|
|
62
61
|
*
|
|
63
62
|
* **What it deliberately does not check.** A key inside a `sohl:` or `hm3:`
|
|
64
63
|
* block that the format says nothing about is left alone: those regions are
|
|
@@ -71,7 +70,7 @@
|
|
|
71
70
|
* The specification hand-writes a `data` table under most of its type sections,
|
|
72
71
|
* which is the ground {@link module:engine/field-reference} already generates
|
|
73
72
|
* from the `fields` on each `itemBuilders` entry — the duplication that module's
|
|
74
|
-
* docstring exists to prevent, one document over
|
|
73
|
+
* docstring exists to prevent, one document over.
|
|
75
74
|
*
|
|
76
75
|
* **Checked rather than generated, because a merge is not available.** The
|
|
77
76
|
* document's vocabulary spans note types that produce Scenes, Macros and
|
|
@@ -89,7 +88,7 @@
|
|
|
89
88
|
* set and are not meant to be: the document names the *shared* source a field is
|
|
90
89
|
* written as, while a declaration names every key the system's own block accepts
|
|
91
90
|
* — including the system-specific ones (`heft`, `strikeModes`) that the document
|
|
92
|
-
* correctly never maps. Until
|
|
91
|
+
* correctly never maps. Until the corpus has moved into `data:`, holding the
|
|
93
92
|
* sets equal would report the migration itself as a defect on every run. So the
|
|
94
93
|
* fields only one side names come back as *coverage*, and the types only one side
|
|
95
94
|
* describes come back **named** rather than skipped in silence — a check that
|
|
@@ -138,7 +137,7 @@ export function declaredPaths(artifact) {
|
|
|
138
137
|
*
|
|
139
138
|
* A shared row names no type, so it is placed rather than typed: "in the shared
|
|
140
139
|
* mappings" instead of "on a `weapongear`". Reading `on a \`the shared
|
|
141
|
-
* mappings\`` would be the alternative, and a diagnostic is prose
|
|
140
|
+
* mappings\`` would be the alternative, and a diagnostic is prose.
|
|
142
141
|
*
|
|
143
142
|
* @param {object} finding - `{system, systemVersion, noteType, source, target, shared}`.
|
|
144
143
|
* @returns {string} The message.
|
|
@@ -342,8 +341,8 @@ export function measureNote(note, format, { severity = "warning" } = {}) {
|
|
|
342
341
|
|
|
343
342
|
// The specification's sections are headed by the current spelling of a note
|
|
344
343
|
// type, so a note still on a renamed one is measured against the section it
|
|
345
|
-
// will move to rather than reported as a type the format does not declare
|
|
346
|
-
//
|
|
344
|
+
// will move to rather than reported as a type the format does not declare.
|
|
345
|
+
// The rename itself is the frontmatter lint's finding.
|
|
347
346
|
const spec = format.types.get(currentType(type));
|
|
348
347
|
if (!spec) {
|
|
349
348
|
add(
|
|
@@ -403,16 +402,16 @@ export function measureNote(note, format, { severity = "warning" } = {}) {
|
|
|
403
402
|
/**
|
|
404
403
|
* Measure a corpus, and count what it finds by class.
|
|
405
404
|
*
|
|
406
|
-
* The counts are the point as much as the findings:
|
|
407
|
-
* fatal when its count reaches zero, so a run that prints them is the
|
|
408
|
-
* progress bar.
|
|
405
|
+
* The counts are the point as much as the findings: a class is promoted to
|
|
406
|
+
* fatal when its count reaches zero, so a run that prints them is the
|
|
407
|
+
* migration's progress bar.
|
|
409
408
|
*
|
|
410
409
|
* @param {Iterable<object>} notes - `{file, raw, fm}` for each authored note.
|
|
411
410
|
* @param {import("./content-format.mjs").ContentFormat} format - The parsed
|
|
412
411
|
* specification.
|
|
413
412
|
* @param {object} [opts]
|
|
414
413
|
* @param {boolean} [opts.strict=false] - Report the findings as errors rather
|
|
415
|
-
* than warnings.
|
|
414
|
+
* than warnings. Turned on one slice at a time.
|
|
416
415
|
* @returns {{findings: object[], notes: number, byClass: Record<string, number>}}
|
|
417
416
|
*/
|
|
418
417
|
export function measureCorpus(notes, format, { strict = false } = {}) {
|
|
@@ -475,9 +474,9 @@ export function fieldDriftMessage({ noteType, source, target, name, to }) {
|
|
|
475
474
|
* A shared source, as the two sides spell it.
|
|
476
475
|
*
|
|
477
476
|
* The specification writes every type-specific row `data.<key>` — the container
|
|
478
|
-
* `data:`
|
|
477
|
+
* `data:` put those facts in — while a declaration writes either: the
|
|
479
478
|
* bare key it has always named, or the same dotted path now that a field can
|
|
480
|
-
* declare its shared source and its legacy in-block key separately
|
|
479
|
+
* declare its shared source and its legacy in-block key separately.
|
|
481
480
|
* Both are the same source, so both are normalized before they are compared;
|
|
482
481
|
* matching one spelling against the other would report every moved field as
|
|
483
482
|
* unmapped, which is the opposite of what this check is for.
|
|
@@ -507,7 +506,7 @@ function claimsFor(format, noteType, system) {
|
|
|
507
506
|
|
|
508
507
|
/**
|
|
509
508
|
* Check the specification's per-type tables against the field declarations that
|
|
510
|
-
* compile them
|
|
509
|
+
* compile them.
|
|
511
510
|
*
|
|
512
511
|
* @param {object} opts
|
|
513
512
|
* @param {import("./content-format.mjs").ContentFormat} opts.format - The
|
|
@@ -563,7 +562,7 @@ export function checkDeclaredFields({ format, itemFields, system, severity = "er
|
|
|
563
562
|
}
|
|
564
563
|
// No declaration names it. That is coverage, not a contradiction —
|
|
565
564
|
// the specification maps fields no builder emits yet, which is the
|
|
566
|
-
// ordinary mid-migration state
|
|
565
|
+
// ordinary mid-migration state and what `schema-check.mjs`
|
|
567
566
|
// already reports as unemitted.
|
|
568
567
|
if (!match) continue;
|
|
569
568
|
fields += 1;
|
|
@@ -587,7 +586,7 @@ export function checkDeclaredFields({ format, itemFields, system, severity = "er
|
|
|
587
586
|
// The two vocabularies, side by side. Reported rather than asserted
|
|
588
587
|
// equal: the document names the *shared* source a field is written as,
|
|
589
588
|
// and a declaration names every key the system's own block accepts, so
|
|
590
|
-
// the sets legitimately differ until
|
|
589
|
+
// the sets legitimately differ until the corpus has moved.
|
|
591
590
|
const registryKeys = new Set(
|
|
592
591
|
authored.map((field) => sharedSource(field.name).split(".")[0]),
|
|
593
592
|
);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Reading `docs/content-format.md` as data
|
|
15
|
+
* Reading `docs/content-format.md` as data.
|
|
16
16
|
*
|
|
17
17
|
* The content format — three frontmatter regions, a note vocabulary with its
|
|
18
18
|
* own `type` and `subType`, and a declared map from each note type onto each
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - **every `system.*` target it names** must exist in the naming system's
|
|
24
24
|
* published `schema.json`, or the specification and the system disagree; and
|
|
25
25
|
* - **every authored note** should carry only the keys the format declares for
|
|
26
|
-
* its type, which during the migration
|
|
26
|
+
* its type, which during the migration is a progress bar as much as a
|
|
27
27
|
* check.
|
|
28
28
|
*
|
|
29
29
|
* Both need the document as data, and this is the module that supplies it.
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* type's `subType` values are stated as `**subType**:` followed by one bullet
|
|
51
51
|
* per value, `- <value>` or `- <value>: <definition>`, and that is read here
|
|
52
52
|
* for the same reason the tables are: so the specification and
|
|
53
|
-
* `note-vocabulary.mjs` cannot disagree about which genres exist
|
|
53
|
+
* `note-vocabulary.mjs` cannot disagree about which genres exist. The
|
|
54
54
|
* one shape is enforced rather than guessed at — the document wrote them five
|
|
55
55
|
* ways, and a reader that accepted every spelling would accept the sixth by
|
|
56
56
|
* reading the section as declaring nothing, which is the drift it exists to
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* The document states the rows every type maps identically once, at the top,
|
|
61
61
|
* and omits them from all sixteen per-type tables — so a parser that only ever
|
|
62
62
|
* looked inside a type's section could not see them, and the eight rows they
|
|
63
|
-
* cover were checked by nothing
|
|
63
|
+
* cover were checked by nothing. Position is the whole distinction:
|
|
64
64
|
* there is no marker to read and none is wanted, since the document's own
|
|
65
65
|
* argument for stating them once is that they belong to no type in particular. A cell that
|
|
66
66
|
* names no field — `NA`, `**see above**`, a `flags.*` path — is not a claim,
|
|
@@ -115,7 +115,7 @@ export const CONTENT_FORMAT_PATH = path.join(
|
|
|
115
115
|
* `the shared mappings` for a row of the shared tables — see `shared`.
|
|
116
116
|
* @property {boolean} [shared] - Whether the row came from a **shared** mapping
|
|
117
117
|
* table, which stands before the first `### type:` heading and states what
|
|
118
|
-
* every type maps identically
|
|
118
|
+
* every type maps identically. Absent on a per-type row, so the two
|
|
119
119
|
* never mix: only a per-type row has a field declaration to be checked
|
|
120
120
|
* against.
|
|
121
121
|
* @property {string} system - The system column it sits under, from the header.
|
|
@@ -205,8 +205,8 @@ const SUBTYPE_BULLET = /^-\s+(\S+?)\s*(?::|$)/;
|
|
|
205
205
|
*
|
|
206
206
|
* Thrown rather than collected, because there is nothing partial to report: a
|
|
207
207
|
* marker the reader does not understand yields a section that appears to
|
|
208
|
-
* declare no subTypes, and every comparison against it then passes vacuously
|
|
209
|
-
*
|
|
208
|
+
* declare no subTypes, and every comparison against it then passes vacuously.
|
|
209
|
+
* The message carries the compiler-parseable position the rest of the
|
|
210
210
|
* toolchain's diagnostics use.
|
|
211
211
|
*
|
|
212
212
|
* @param {string} file - The document being read.
|
|
@@ -338,7 +338,7 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
|
|
|
338
338
|
table = {
|
|
339
339
|
kind: "mapping",
|
|
340
340
|
systems: cells.slice(1).map((cell) => cell.replace(/^→\s*/, "").trim()),
|
|
341
|
-
// Before any type section, so the rows are every type's
|
|
341
|
+
// Before any type section, so the rows are every type's.
|
|
342
342
|
...(current ? {} : { shared: true }),
|
|
343
343
|
};
|
|
344
344
|
continue;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Raw HTML in a note's prose, reported.
|
|
16
|
+
*
|
|
17
|
+
* **A note is markdown.** What markdown cannot say, a note does not say — it
|
|
18
|
+
* gets a construct every surface can render, the way `:icon-…:` replaces a
|
|
19
|
+
* pasted glyph.
|
|
20
|
+
*
|
|
21
|
+
* ## There is no route from `<p>` to a book
|
|
22
|
+
*
|
|
23
|
+
* The packs and the website both pass raw HTML through, so a tag written in a
|
|
24
|
+
* note reaches them intact and looks correct. Typst is handed markdown and
|
|
25
|
+
* knows nothing of HTML, so the same tag reaches the book as nothing at all, or
|
|
26
|
+
* as literal angle brackets. Closing that gap would mean writing an
|
|
27
|
+
* HTML-to-Typst translator — a renderer nobody wants to own for the sake of a
|
|
28
|
+
* `<strong>` that markdown already spells.
|
|
29
|
+
*
|
|
30
|
+
* So this is the character rule one level up: the charset check refuses a glyph
|
|
31
|
+
* no book face can set, and this refuses markup no book renderer can read.
|
|
32
|
+
*
|
|
33
|
+
* ## What it does not look at
|
|
34
|
+
*
|
|
35
|
+
* - **Fenced blocks and code spans.** HTML shown as an example is prose *about*
|
|
36
|
+
* HTML, and a rule that could not tell the difference would make it
|
|
37
|
+
* impossible to document any of this — including this module. Which runs
|
|
38
|
+
* count as code is {@link module:engine/code-fences.codeRegions}' rule rather
|
|
39
|
+
* than a second copy of it.
|
|
40
|
+
* - **Frontmatter.** A structured value is not prose, and a field the compiler
|
|
41
|
+
* derives is its own question — see {@link module:engine/derived-fields}.
|
|
42
|
+
*
|
|
43
|
+
* ## A warning, like the checks around it
|
|
44
|
+
*
|
|
45
|
+
* A note that renders correctly on two of three surfaces today should not fail
|
|
46
|
+
* a build while the third is still being built. The finding is what makes the
|
|
47
|
+
* work visible; the refusal follows when there is somewhere for the content to
|
|
48
|
+
* go.
|
|
49
|
+
*
|
|
50
|
+
* @module
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import fs from "node:fs";
|
|
54
|
+
import path from "node:path";
|
|
55
|
+
|
|
56
|
+
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
57
|
+
import { positionInBody } from "./diagnostics.mjs";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A raw HTML tag, opening, closing or self-closing.
|
|
61
|
+
*
|
|
62
|
+
* The name must start a tag for the match to begin, which is what keeps
|
|
63
|
+
* markdown's own angle brackets out of it: an autolink is `<https://…>`, and
|
|
64
|
+
* `https` is followed by `:` rather than whitespace or `>`, so the pattern
|
|
65
|
+
* stops. A comparison written in prose — `a < b` — has no name after the
|
|
66
|
+
* bracket at all.
|
|
67
|
+
*
|
|
68
|
+
* Attributes are consumed as "anything but a bracket", deliberately loosely: a
|
|
69
|
+
* finding names the tag, and a pattern that tried to parse attribute syntax
|
|
70
|
+
* would be a second HTML parser with its own bugs, in a module whose whole
|
|
71
|
+
* point is that nothing here should be parsing HTML.
|
|
72
|
+
*
|
|
73
|
+
* @type {RegExp}
|
|
74
|
+
*/
|
|
75
|
+
export const HTML_TAG = /<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*)?\/?>/g;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What a note carrying raw HTML is told.
|
|
79
|
+
*
|
|
80
|
+
* It names the tag, because a file with several is fixed one at a time, and it
|
|
81
|
+
* says why rather than only what: an author who does not know the book cannot
|
|
82
|
+
* render it will read the finding as pedantry about a tag that plainly works.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} tag - The matched markup, as written.
|
|
85
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
86
|
+
*/
|
|
87
|
+
export function htmlMessage(tag) {
|
|
88
|
+
return (
|
|
89
|
+
`\`${tag}\` is raw HTML in a note's prose — write it in markdown. ` +
|
|
90
|
+
`The packs and the website pass HTML through, so this renders on both ` +
|
|
91
|
+
`and reaches the book as nothing: Typst is handed markdown and knows ` +
|
|
92
|
+
`no HTML. Inside a fence or a code span it is an example, and not reported`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Every raw HTML tag in one note's body.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} body - The note's markdown, without its frontmatter.
|
|
100
|
+
* @param {string} file - The note's path, for the finding.
|
|
101
|
+
* @param {object} [opts]
|
|
102
|
+
* @param {number} [opts.bodyLine=1] - The 1-based file line the body starts on.
|
|
103
|
+
* @param {number} [opts.bodyColumn=1] - The 1-based file column it starts at.
|
|
104
|
+
* @returns {Array<{file: string, line: number, column: number,
|
|
105
|
+
* severity: "warning", message: string}>} One finding per tag, in source
|
|
106
|
+
* order.
|
|
107
|
+
*/
|
|
108
|
+
export function checkHtml(body, file, { bodyLine = 1, bodyColumn = 1 } = {}) {
|
|
109
|
+
const text = String(body ?? "");
|
|
110
|
+
if (!text) return [];
|
|
111
|
+
|
|
112
|
+
return matchAllOutsideCode(text, HTML_TAG).map((match) => ({
|
|
113
|
+
file,
|
|
114
|
+
...positionInBody(text, /** @type {number} */ (match.index), { bodyLine, bodyColumn }),
|
|
115
|
+
severity: /** @type {"warning"} */ ("warning"),
|
|
116
|
+
message: htmlMessage(match[0]),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Walk a content tree and report raw HTML in every note's prose.
|
|
122
|
+
*
|
|
123
|
+
* The frontmatter fence is taken off first, so what is scanned is the body and
|
|
124
|
+
* the positions are still the file's. A file with no frontmatter is scanned
|
|
125
|
+
* whole: it is not a note, but a stray `.md` in the tree carrying markup is the
|
|
126
|
+
* same problem for the same reason.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} contentBase - Root of the content tree.
|
|
129
|
+
* @param {object} [opts]
|
|
130
|
+
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
|
|
131
|
+
* in addition to the dot-directories always skipped.
|
|
132
|
+
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
133
|
+
* severity: "warning", message: string}>, files: number}} The findings, and
|
|
134
|
+
* how many files were read.
|
|
135
|
+
*/
|
|
136
|
+
export function lintContentHtml(contentBase, { skipDirectories = [] } = {}) {
|
|
137
|
+
const skip = new Set(skipDirectories);
|
|
138
|
+
/** @type {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} */
|
|
139
|
+
const findings = [];
|
|
140
|
+
let files = 0;
|
|
141
|
+
|
|
142
|
+
/** @param {string} dir - Directory to descend into. */
|
|
143
|
+
const walk = (dir) => {
|
|
144
|
+
/** @type {import("node:fs").Dirent[]} */
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (entry.name.startsWith(".") || skip.has(entry.name)) continue;
|
|
153
|
+
const full = path.join(dir, entry.name);
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
walk(full);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!/\.(md|markdown)$/i.test(entry.name)) continue;
|
|
159
|
+
let content;
|
|
160
|
+
try {
|
|
161
|
+
content = fs.readFileSync(full, "utf8");
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
files += 1;
|
|
166
|
+
findings.push(...checkHtml(...bodyOf(content, path.relative(contentBase, full))));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
walk(contentBase);
|
|
171
|
+
return { findings, files };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A file's body, its path, and where the body starts in the file.
|
|
176
|
+
*
|
|
177
|
+
* The same split {@link module:engine/helpers.parseMarkdownFile} makes, without
|
|
178
|
+
* parsing the YAML: this check has no use for the frontmatter's *values*, and
|
|
179
|
+
* reading them would make an unparseable note silently unscanned.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} content - The whole file.
|
|
182
|
+
* @param {string} file - Its path, for the finding.
|
|
183
|
+
* @returns {[string, string, {bodyLine: number, bodyColumn: number}]} The
|
|
184
|
+
* arguments {@link checkHtml} takes.
|
|
185
|
+
*/
|
|
186
|
+
function bodyOf(content, file) {
|
|
187
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
188
|
+
if (!match) return [content, file, { bodyLine: 1, bodyColumn: 1 }];
|
|
189
|
+
|
|
190
|
+
const raw = match[2];
|
|
191
|
+
const body = raw.trim();
|
|
192
|
+
const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
193
|
+
const before = content.slice(0, bodyStart);
|
|
194
|
+
return [
|
|
195
|
+
body,
|
|
196
|
+
file,
|
|
197
|
+
{
|
|
198
|
+
bodyLine: before.split("\n").length,
|
|
199
|
+
bodyColumn: bodyStart - before.lastIndexOf("\n"),
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|