@heroiclands/package-build 20.3.1 → 20.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 +351 -0
- package/CONTENT.md +322 -110
- package/README.md +21 -10
- package/bin/content-build.mjs +204 -92
- package/bin/package-build.mjs +48 -15
- package/config.mjs +6 -8
- package/content-config.mjs +518 -151
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +73 -74
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +64 -61
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +57 -52
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +42 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +17 -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 +24 -24
- 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 +196 -165
- package/engine/content-index.mjs +61 -40
- package/engine/content-links.mjs +74 -41
- package/engine/content-lint.mjs +64 -34
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +38 -34
- 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 +18 -16
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +95 -103
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +44 -39
- package/engine/helpers.mjs +147 -84
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +26 -10
- 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 +24 -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 +51 -41
- 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 -8
- package/engine/note-vocabulary.mjs +56 -54
- package/engine/pack-config.mjs +39 -25
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +798 -0
- package/engine/pdf-toc.mjs +525 -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 +33 -25
- package/engine/schema-check.mjs +21 -21
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +66 -50
- 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 +41 -38
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +40 -42
- package/hm3/actors.mjs +30 -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 +28 -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 +66 -6
- package/sohl/actors.mjs +36 -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 +28 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +12 -12
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +91 -39
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +45 -40
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +60 -6
- package/types/engine/base-compiler.d.mts +27 -21
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +10 -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 +20 -20
- 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 +139 -92
- package/types/engine/content-index.d.mts +59 -15
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +8 -6
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/content-tables.d.mts +49 -18
- 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 +10 -9
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +128 -56
- 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 +6 -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 +12 -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 +30 -22
- 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 +31 -203
- package/types/engine/pack-config.d.mts +7 -7
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +144 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- 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/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +13 -13
- package/types/engine/site-build.d.mts +52 -25
- 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 +14 -14
- package/types/hm3/actors.d.mts +9 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +9 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +12 -4
- 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 +9 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
|
@@ -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
|
+
}
|