@heroiclands/package-build 22.13.1 → 22.14.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.md +12 -0
- package/docs/content-format.md +61 -106
- package/engine/field-reference.mjs +4 -3
- package/engine/frontmatter-lint.mjs +19 -0
- package/engine/note-format.mjs +100 -0
- package/engine/note-frontmatter.mjs +33 -0
- package/engine/prose-lint.mjs +3 -2
- package/package.json +1 -1
- package/types/engine/field-reference.d.mts +1 -1
- package/types/engine/note-format.d.mts +8 -0
- package/types/engine/note-frontmatter.d.mts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.14.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
Item-field reference pages generated inside content packages pass note validation and formatting checks.
|
|
8
|
+
|
|
9
|
+
## 22.14.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
Content notes accept the documented top-level fields, with clear errors for other keys. Formatting keeps those fields in order and writes YAML collections as short lines or full blocks.
|
|
14
|
+
|
|
3
15
|
## 22.13.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/docs/content-format.md
CHANGED
|
@@ -287,26 +287,49 @@ infoboxes:
|
|
|
287
287
|
|
|
288
288
|
### Frontmatter has three regions
|
|
289
289
|
|
|
290
|
-
A note
|
|
291
|
-
|
|
290
|
+
A HeroicLands note has opening YAML frontmatter with nonempty `shortcode` and
|
|
291
|
+
`type` values. Other Markdown files are ordinary Markdown. A note's frontmatter
|
|
292
|
+
has three regions:
|
|
292
293
|
|
|
293
|
-
|
|
|
294
|
-
| -------------------- |
|
|
295
|
-
| **
|
|
296
|
-
| **`data:`** |
|
|
297
|
-
| **`
|
|
294
|
+
| Region | Describes | Unknown keys |
|
|
295
|
+
| -------------------- | ------------------------------------------------------- | ------------ |
|
|
296
|
+
| **Top level** | The note's identity, name, description, and publication | An error |
|
|
297
|
+
| **`data:`** | The subject itself, shared by systems | An error |
|
|
298
|
+
| **`hm3:` / `sohl:`** | The subject as one system's documents | An error |
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
document field at all, it is the page's description.
|
|
300
|
+
The complete top-level allowlist, in its required order, is `shortcode`, `name`,
|
|
301
|
+
`type`, `subType`, `description`, `tags`, `data`, `hm3`, `sohl`. Omit fields that
|
|
302
|
+
do not apply. The first fields of a typical note look like this:
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
```yaml
|
|
305
|
+
---
|
|
306
|
+
shortcode: example
|
|
307
|
+
name: { full: Example }
|
|
308
|
+
type: lore
|
|
309
|
+
subType: concept
|
|
310
|
+
description: A short page description.
|
|
311
|
+
tags: [draft]
|
|
312
|
+
data: {}
|
|
313
|
+
sohl: {}
|
|
314
|
+
---
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Any other top-level key is an error. To keep an author-only note beside the
|
|
318
|
+
content, comment out its YAML lines with `#`. Comments remain valid YAML text
|
|
319
|
+
and do not become data in the JSONL index, generated pages, or Foundry documents.
|
|
320
|
+
There is no custom site metadata. `description` supplies the page's short
|
|
321
|
+
description; `tags` carries publication and classification tags.
|
|
322
|
+
|
|
323
|
+
Arrays and objects use one-line YAML flow form when the complete line is under
|
|
324
|
+
100 characters. At 100 characters or more, they use full block form. A multiline
|
|
325
|
+
flow collection is formatted into one of those forms; folded collections are
|
|
326
|
+
not used. `content-build format --write` applies this rule and the top-level
|
|
327
|
+
order without changing the Markdown body.
|
|
328
|
+
|
|
329
|
+
**`data:` is closed.** It holds the type-specific facts about the
|
|
305
330
|
subject — a weapon's weight, an affliction's transmission, a being's species —
|
|
306
|
-
and every note type declares which keys it may carry. A misspelled key
|
|
307
|
-
finding
|
|
308
|
-
silently become a theme parameter, which is exactly why these cannot live
|
|
309
|
-
together.
|
|
331
|
+
and every note type declares which keys it may carry. A misspelled key is a
|
|
332
|
+
finding in either region.
|
|
310
333
|
|
|
311
334
|
Every note type may put a pinned document id in `data.id`, a shared compendium
|
|
312
335
|
route in `data.pack`, a shared folder Address in `data.packFolder`, and source
|
|
@@ -314,8 +337,7 @@ details in `data.harnworld`. A system block's `pack` or `packFolder` overrides
|
|
|
314
337
|
the shared value for that system. A `being` may also describe its social profile
|
|
315
338
|
in `data.social`.
|
|
316
339
|
|
|
317
|
-
**Tags are open, except the ones that classify.**
|
|
318
|
-
openness: a tag naming a theme or a region — `underworld`,
|
|
340
|
+
**Tags are open, except the ones that classify.** A tag naming a theme or a region — `underworld`,
|
|
319
341
|
`byzaria`, `riverlands` — is the author's own and this build has no opinion about it.
|
|
320
342
|
A tag that classifies the subject is different, because something queries it. A
|
|
321
343
|
settlement tagged `village` appears in the list of villages and an untagged one
|
|
@@ -465,14 +487,11 @@ in-block spelling, and `hm3.system.gender` is the native destination.
|
|
|
465
487
|
The native destination wins, followed by the in-block value and the shared
|
|
466
488
|
source. Field declarations name both source and destination explicitly.
|
|
467
489
|
|
|
468
|
-
**A field
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
top-level key means instead, and the top level stops being read for it — leaving
|
|
474
|
-
`<system>.system.<field>` and the legacy in-block position, which describe the
|
|
475
|
-
document rather than the note. Native species is independent of the shared lore Address in `data.species`.
|
|
490
|
+
**A field without a shared source reads its system declaration.** An
|
|
491
|
+
`affiliation` item's `system.title` is the style of address an office carries;
|
|
492
|
+
the note's heading reads `name.full`. The field reads
|
|
493
|
+
`<system>.system.<field>` and the in-block position, which describe the
|
|
494
|
+
document. Native species is independent of the shared lore Address in `data.species`.
|
|
476
495
|
`subType` has the same meaning at both levels.
|
|
477
496
|
|
|
478
497
|
**Some of a system's fields are runtime state, and a note may not write any of
|
|
@@ -1080,78 +1099,20 @@ identity across one pins its `id`, which is what the pin is for. One diagnostic
|
|
|
1080
1099
|
narrows — `content-build` tells a **rename** from a **withdrawal** by matching
|
|
1081
1100
|
document ids across releases, and for an unpinned note both sides now move
|
|
1082
1101
|
together. It never reports a _wrong_ successor, and it stays exact for a pinned
|
|
1083
|
-
note; for the rest, the
|
|
1102
|
+
note; for the rest, the diff reports the vacated address.
|
|
1084
1103
|
|
|
1085
1104
|
**A note with no address gets no id, and no document.** `type` and `shortcode`
|
|
1086
1105
|
are what a note is addressed by, so a note missing either cannot be filed and
|
|
1087
1106
|
the build refuses it by that name rather than by a missing `id:`.
|
|
1088
1107
|
|
|
1089
|
-
####
|
|
1090
|
-
|
|
1091
|
-
A note names the shortcode it used to be published under:
|
|
1092
|
-
|
|
1093
|
-
```yaml
|
|
1094
|
-
type: weapongear
|
|
1095
|
-
shortcode: Taburi
|
|
1096
|
-
renamedFrom: Tabri
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
`(type, shortcode)` is a **published interface** — every satellite that declares
|
|
1100
|
-
`itemCatalog: true` assembles its beings out of those addresses — so renaming a
|
|
1101
|
-
shortcode breaks other repositories, and `content-build addresses diff` exists to
|
|
1102
|
-
say so before a release does. To be useful it has to name where the address
|
|
1103
|
-
_went_, and since ids are derived it can no longer work that out for an unpinned
|
|
1104
|
-
note: both sides of the match move together, and the rename reads as a
|
|
1105
|
-
withdrawal.
|
|
1106
|
-
|
|
1107
|
-
**Pinning an `id` needs foresight; a declaration needs only hindsight.** A pin
|
|
1108
|
-
has to be written _before_ the rename, by an author who does not yet know they
|
|
1109
|
-
will make one. An author who has just renamed a shortcode knows exactly what the
|
|
1110
|
-
old one was, and that is the only moment anyone does — so this is the key to
|
|
1111
|
-
reach for, and `id:` stays what it is for: keeping a document's identity across
|
|
1112
|
-
the rename, which is a different question from explaining it.
|
|
1113
|
-
|
|
1114
|
-
**It takes one shortcode or a list**, because renames chain: the diff runs
|
|
1115
|
-
against a released baseline, and a shortcode may have been renamed more than once
|
|
1116
|
-
since. List every name the baseline might still know it by.
|
|
1117
|
-
|
|
1118
|
-
```yaml
|
|
1119
|
-
shortcode: Taburin
|
|
1120
|
-
renamedFrom:
|
|
1121
|
-
- Tabri
|
|
1122
|
-
- Taburi
|
|
1123
|
-
```
|
|
1124
|
-
|
|
1125
|
-
**It is transient.** Once every baseline a build is compared against post-dates
|
|
1126
|
-
the rename, the declaration has nothing left to say and should be deleted. That
|
|
1127
|
-
is what separates it from an `id:` pin, which is permanent.
|
|
1128
|
-
|
|
1129
|
-
**One key per note, at the top level**, however many systems the note compiles
|
|
1130
|
-
into: a shortcode is the note's rather than a system block's, so a note carrying
|
|
1131
|
-
`sohl:` and `hm3:` blocks compiles two documents that share one shortcode, and
|
|
1132
|
-
one declaration covers both.
|
|
1133
|
-
|
|
1134
|
-
**What a declaration changes is what the diagnostic can say, not what it says
|
|
1135
|
-
about you.** A finding reports which of the two joins it had, because they are
|
|
1136
|
-
not equally checkable — a matched id is a fact a reader can verify in both
|
|
1137
|
-
artefacts, while a declaration is the author's word:
|
|
1138
|
-
|
|
1139
|
-
```text
|
|
1140
|
-
since sohl@0.8.2, weapongear:Tabri is no longer published; the note now
|
|
1141
|
-
published as weapongear:Taburi declares it was renamed from Tabri. Every
|
|
1142
|
-
package that resolves weapongear:Tabri breaks when it moves past sohl@0.8.2
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
A rename that is neither pinned nor declared is still reported as a
|
|
1146
|
-
**withdrawal**. Nothing infers a successor from a similar-looking string: a wrong
|
|
1147
|
-
one sends the reader to the wrong fix, which is worse than saying nothing.
|
|
1108
|
+
#### Address changes
|
|
1148
1109
|
|
|
1149
|
-
`
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1110
|
+
`(type, shortcode)` is a published interface. Other packages can link to it or
|
|
1111
|
+
use it as an Item model, so changing either part requires those references to
|
|
1112
|
+
change too. `content-build addresses diff` compares the package with a released
|
|
1113
|
+
baseline and reports addresses whose published documents are absent. A pinned
|
|
1114
|
+
`data.id` preserves document identity across a shortcode change; it does not
|
|
1115
|
+
keep the old address available for links.
|
|
1155
1116
|
|
|
1156
1117
|
#### The compendium folder
|
|
1157
1118
|
|
|
@@ -2760,9 +2721,8 @@ It declares a `shortcode` — conventionally `root` — because that is what a
|
|
|
2760
2721
|
link is written with: `[[homepage-root|Text]]` is an ordinary wikilink, and it
|
|
2761
2722
|
resolves to `/<package>/`. The shortcode names the page in links; its address
|
|
2762
2723
|
is the package root. The page heading reads `name.full` and defaults to
|
|
2763
|
-
`packageBuild.manifest.title
|
|
2764
|
-
|
|
2765
|
-
top-level key is passed through to the page as a theme parameter.
|
|
2724
|
+
`packageBuild.manifest.title`. `description` supplies the page's short
|
|
2725
|
+
description.
|
|
2766
2726
|
|
|
2767
2727
|
The page is its body, published verbatim: no wikilink is resolved on it and
|
|
2768
2728
|
no table expanded, so its links are markdown links, package-relative
|
|
@@ -2770,9 +2730,8 @@ no table expanded, so its links are markdown links, package-relative
|
|
|
2770
2730
|
written here and is not generated anywhere — it is a `doc` note carrying a
|
|
2771
2731
|
content table, linked from this page like any other.
|
|
2772
2732
|
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
a card block — the homepage is a page with a body, rendered as one.
|
|
2733
|
+
The homepage uses the same top-level allowlist as every addressed note. It has
|
|
2734
|
+
no Foundry document id; a pinned `data.id` has no role on this type.
|
|
2776
2735
|
|
|
2777
2736
|
### type: vehicle
|
|
2778
2737
|
|
|
@@ -2998,14 +2957,10 @@ mapping for a field no schema declares is the drift these tables exist to catch.
|
|
|
2998
2957
|
likewise absent here: they are filled on an embedded membership, never from a
|
|
2999
2958
|
catalogue note's `data:`.
|
|
3000
2959
|
|
|
3001
|
-
**`system.title` is
|
|
3002
|
-
|
|
3003
|
-
the
|
|
3004
|
-
|
|
3005
|
-
virtue of its rank. So the top-level key is **not** a shared source for this
|
|
3006
|
-
field, and a note that writes one is stating its own heading and nothing else
|
|
3007
|
-
. Author the style of address on the membership — the `system.title` of the
|
|
3008
|
-
entry in a being's `sohl.items` — or, on a catalogue note that genuinely carries
|
|
2960
|
+
**`system.title` is the style of address the office carries**, Ajaw or Warden,
|
|
2961
|
+
which a being holds by virtue of its rank. The note's page heading reads
|
|
2962
|
+
`name.full`. Author the style of address on the membership — the `system.title`
|
|
2963
|
+
of the entry in a being's `sohl.items` — or, on a catalogue note that carries
|
|
3009
2964
|
one, at `sohl.system.title`.
|
|
3010
2965
|
|
|
3011
2966
|
### type: affliction
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import path from "node:path";
|
|
38
38
|
import matter from "gray-matter";
|
|
39
|
+
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
39
40
|
|
|
40
41
|
import { authoredFields, runtimeOnlyFields } from "./field-spec.mjs";
|
|
41
42
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -389,7 +390,7 @@ export function shortcodeFromBasename(destination) {
|
|
|
389
390
|
* The universal keys every note in the format carries: `type: doc`,
|
|
390
391
|
* `subType: reference` — this page is out-of-world lookup material, the same
|
|
391
392
|
* genre as every other generated reference — a `shortcode`, `name.full` from
|
|
392
|
-
* the page's title, and `pack: none`, since the page publishes to the website
|
|
393
|
+
* the page's title, and `data.pack: none`, since the page publishes to the website
|
|
393
394
|
* and compiles into no compendium document. A consumer's own
|
|
394
395
|
* `docs.itemFields.frontmatter` is deep-merged over it, so it may add keys or
|
|
395
396
|
* override any of the derived ones, `shortcode` included.
|
|
@@ -409,7 +410,7 @@ export function itemFieldsEnvelope({ title, shortcode, frontmatter }) {
|
|
|
409
410
|
subType: "reference",
|
|
410
411
|
shortcode,
|
|
411
412
|
name: { full: title },
|
|
412
|
-
pack: "none",
|
|
413
|
+
data: { pack: "none" },
|
|
413
414
|
},
|
|
414
415
|
frontmatter,
|
|
415
416
|
);
|
|
@@ -448,5 +449,5 @@ export function renderItemFieldsPage(body, { title, destination, contentRoot, fr
|
|
|
448
449
|
// between them, so a page written without one fails a consumer's
|
|
449
450
|
// `lint:format` the moment it is committed. Insert it here rather than
|
|
450
451
|
// let the generator and the formatter rewrite the file back and forth.
|
|
451
|
-
return page.replace(/^(---\n[\s\S]*?\n---\n)/, "$1\n");
|
|
452
|
+
return formatNoteFrontmatter(page.replace(/^(---\n[\s\S]*?\n---\n)/, "$1\n"));
|
|
452
453
|
}
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import { isAddressTuple } from "./address.mjs";
|
|
58
|
+
import { authoredNoteKeys, NOTE_TOP_LEVEL_KEY_SET } from "./note-frontmatter.mjs";
|
|
58
59
|
import { AddressEntries } from "./address-values.mjs";
|
|
59
60
|
import { authoredFields, readsLegacyKey } from "./field-spec.mjs";
|
|
60
61
|
import {
|
|
@@ -1443,6 +1444,24 @@ export function lintNote(
|
|
|
1443
1444
|
});
|
|
1444
1445
|
}
|
|
1445
1446
|
|
|
1447
|
+
for (const key of authoredNoteKeys(raw())) {
|
|
1448
|
+
if (NOTE_TOP_LEVEL_KEY_SET.has(key)) continue;
|
|
1449
|
+
const position = positionInFrontmatter(raw(), key, undefined, { topLevel: true });
|
|
1450
|
+
if (
|
|
1451
|
+
findings.some(
|
|
1452
|
+
(finding) => finding.severity === "error" && finding.line === position.line,
|
|
1453
|
+
)
|
|
1454
|
+
) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
findings.push({
|
|
1458
|
+
file: note.file,
|
|
1459
|
+
...position,
|
|
1460
|
+
severity: "error",
|
|
1461
|
+
message: `unknown top-level frontmatter key ${JSON.stringify(key)}; use shortcode, name, type, subType, description, tags, data, hm3, or sohl`,
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1446
1465
|
// The type's charset, before anything that looks the type up. A
|
|
1447
1466
|
// hyphenated type is unaddressable, and every lookup below would report it
|
|
1448
1467
|
// as a type nobody declared — true, but not the reason, and it would send
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
import YAML from "yaml";
|
|
15
|
+
import { NOTE_TOP_LEVEL_KEYS } from "./note-frontmatter.mjs";
|
|
16
|
+
|
|
17
|
+
const MAX_FLOW_LINE = 99;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format one addressed note's YAML without changing its Markdown body.
|
|
21
|
+
* Invalid YAML remains the YAML linter's finding.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} markdown - The complete Markdown source.
|
|
24
|
+
* @returns {string} The source with canonical frontmatter.
|
|
25
|
+
*/
|
|
26
|
+
export function formatNoteFrontmatter(markdown) {
|
|
27
|
+
const match = markdown.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)([\s\S]*)$/);
|
|
28
|
+
if (!match) return markdown;
|
|
29
|
+
const [, opening, source, closing, body] = match;
|
|
30
|
+
const document = YAML.parseDocument(source, { keepSourceTokens: true });
|
|
31
|
+
if (document.errors.length || !YAML.isMap(document.contents)) return markdown;
|
|
32
|
+
const root = document.contents;
|
|
33
|
+
const values = Object.fromEntries(
|
|
34
|
+
root.items.map((pair) => [String(pair.key?.value), pair.value]),
|
|
35
|
+
);
|
|
36
|
+
if (!nonempty(values.shortcode) || !nonempty(values.type)) return markdown;
|
|
37
|
+
|
|
38
|
+
const order = new Map(NOTE_TOP_LEVEL_KEYS.map((key, index) => [key, index]));
|
|
39
|
+
const rank = (key) => order.get(key) ?? NOTE_TOP_LEVEL_KEYS.length;
|
|
40
|
+
root.items.sort((left, right) => {
|
|
41
|
+
const a = String(left.key?.value);
|
|
42
|
+
const b = String(right.key?.value);
|
|
43
|
+
const difference = rank(a) - rank(b);
|
|
44
|
+
return difference;
|
|
45
|
+
});
|
|
46
|
+
for (const pair of root.items) {
|
|
47
|
+
const key = String(pair.key?.value);
|
|
48
|
+
formatCollection(pair.value, key.length + 2, 0);
|
|
49
|
+
}
|
|
50
|
+
const formatted = document.toString({ lineWidth: 0, flowCollectionPadding: false }).trimEnd();
|
|
51
|
+
return `${opening}${formatted}${closing}${body}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nonempty(node) {
|
|
55
|
+
return YAML.isScalar(node) && typeof node.value === "string" && node.value.trim() !== "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Format nested collections before testing the enclosing line. */
|
|
59
|
+
function formatCollection(node, prefix, depth) {
|
|
60
|
+
if (!YAML.isMap(node) && !YAML.isSeq(node)) return;
|
|
61
|
+
if (YAML.isMap(node)) {
|
|
62
|
+
for (const pair of node.items) {
|
|
63
|
+
formatCollection(
|
|
64
|
+
pair.value,
|
|
65
|
+
2 * (depth + 1) + String(pair.key?.value).length + 2,
|
|
66
|
+
depth + 1,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
for (const item of node.items) formatCollection(item, 2 * (depth + 1) + 2, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
if (hasBlockScalar(node) || hasAlias(node)) {
|
|
73
|
+
node.flow = false;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
node.flow = true;
|
|
77
|
+
const candidate = new YAML.Document(node)
|
|
78
|
+
.toString({
|
|
79
|
+
lineWidth: 0,
|
|
80
|
+
flowCollectionPadding: false,
|
|
81
|
+
})
|
|
82
|
+
.trimEnd();
|
|
83
|
+
if (candidate.includes("\n") || Array.from(candidate).length + prefix > MAX_FLOW_LINE) {
|
|
84
|
+
node.flow = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasBlockScalar(node) {
|
|
89
|
+
if (YAML.isScalar(node)) return node.type === "BLOCK_LITERAL" || node.type === "BLOCK_FOLDED";
|
|
90
|
+
if (YAML.isMap(node)) return node.items.some((pair) => hasBlockScalar(pair.value));
|
|
91
|
+
if (YAML.isSeq(node)) return node.items.some(hasBlockScalar);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasAlias(node) {
|
|
96
|
+
if (YAML.isAlias(node)) return true;
|
|
97
|
+
if (YAML.isMap(node)) return node.items.some((pair) => hasAlias(pair.value));
|
|
98
|
+
if (YAML.isSeq(node)) return node.items.some(hasAlias);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
|
|
10
|
+
/** The complete ordered top-level vocabulary for addressed content notes. */
|
|
11
|
+
export const NOTE_TOP_LEVEL_KEYS = Object.freeze([
|
|
12
|
+
"shortcode",
|
|
13
|
+
"name",
|
|
14
|
+
"type",
|
|
15
|
+
"subType",
|
|
16
|
+
"description",
|
|
17
|
+
"tags",
|
|
18
|
+
"data",
|
|
19
|
+
"hm3",
|
|
20
|
+
"sohl",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** Membership test for the top-level vocabulary. */
|
|
24
|
+
export const NOTE_TOP_LEVEL_KEY_SET = new Set(NOTE_TOP_LEVEL_KEYS);
|
|
25
|
+
|
|
26
|
+
/** Return authored keys from an opening YAML frontmatter map. */
|
|
27
|
+
export function authoredNoteKeys(markdown) {
|
|
28
|
+
const source = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
29
|
+
if (source === undefined) return [];
|
|
30
|
+
const document = YAML.parseDocument(source);
|
|
31
|
+
if (document.errors.length || !YAML.isMap(document.contents)) return [];
|
|
32
|
+
return document.contents.items.map((pair) => String(pair.key?.value));
|
|
33
|
+
}
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import fs from "node:fs";
|
|
35
35
|
import path from "node:path";
|
|
36
|
+
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
36
37
|
|
|
37
38
|
import {
|
|
38
39
|
MARKDOWNLINT_CONFIG,
|
|
@@ -202,7 +203,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
202
203
|
let formatted = source;
|
|
203
204
|
let converged = false;
|
|
204
205
|
for (let pass = 0; pass < MAX_FORMAT_PASSES; pass += 1) {
|
|
205
|
-
const next = await prettier.format(formatted, options);
|
|
206
|
+
const next = formatNoteFrontmatter(await prettier.format(formatted, options));
|
|
206
207
|
if (next === formatted) {
|
|
207
208
|
converged = true;
|
|
208
209
|
break;
|
|
@@ -236,7 +237,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
236
237
|
continue;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
if (
|
|
240
|
+
if (formatNoteFrontmatter(await prettier.format(source, options)) !== source) {
|
|
240
241
|
findings.push({
|
|
241
242
|
file,
|
|
242
243
|
severity: "error",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.14.1",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@ export function shortcodeFromBasename(destination: string): string;
|
|
|
52
52
|
* The universal keys every note in the format carries: `type: doc`,
|
|
53
53
|
* `subType: reference` — this page is out-of-world lookup material, the same
|
|
54
54
|
* genre as every other generated reference — a `shortcode`, `name.full` from
|
|
55
|
-
* the page's title, and `pack: none`, since the page publishes to the website
|
|
55
|
+
* the page's title, and `data.pack: none`, since the page publishes to the website
|
|
56
56
|
* and compiles into no compendium document. A consumer's own
|
|
57
57
|
* `docs.itemFields.frontmatter` is deep-merged over it, so it may add keys or
|
|
58
58
|
* override any of the derived ones, `shortcode` included.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format one addressed note's YAML without changing its Markdown body.
|
|
3
|
+
* Invalid YAML remains the YAML linter's finding.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} markdown - The complete Markdown source.
|
|
6
|
+
* @returns {string} The source with canonical frontmatter.
|
|
7
|
+
*/
|
|
8
|
+
export function formatNoteFrontmatter(markdown: string): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Return authored keys from an opening YAML frontmatter map. */
|
|
2
|
+
export function authoredNoteKeys(markdown: any): string[];
|
|
3
|
+
/** The complete ordered top-level vocabulary for addressed content notes. */
|
|
4
|
+
export const NOTE_TOP_LEVEL_KEYS: readonly string[];
|
|
5
|
+
/** Membership test for the top-level vocabulary. */
|
|
6
|
+
export const NOTE_TOP_LEVEL_KEY_SET: Set<string>;
|