@heroiclands/package-build 14.0.0 → 15.0.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 +184 -0
- package/CONTENT.md +128 -19
- package/MIGRATING.md +185 -0
- package/docs/content-format.md +21 -0
- package/engine/field-reference.mjs +29 -0
- package/engine/field-spec.mjs +25 -0
- package/engine/frontmatter-lint.mjs +85 -0
- package/engine/helpers.mjs +38 -12
- package/engine/homepage.mjs +9 -4
- package/engine/item-registry.mjs +4 -1
- package/engine/macros.mjs +3 -1
- package/engine/site-build.mjs +20 -14
- package/engine/system-block.mjs +29 -3
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -3
- package/sohl/item-fields.mjs +6 -0
- package/sohl/items.mjs +4 -1
- package/types/engine/field-spec.d.mts +53 -0
- package/types/engine/helpers.d.mts +34 -12
- package/types/engine/homepage.d.mts +8 -4
- package/types/engine/site-build.d.mts +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,189 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 15.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 5c16c02: Let a note's art path say "unset" and "blank on purpose" with two different values.
|
|
8
|
+
|
|
9
|
+
`resolveImg` opened with `if (!raw) return ""`, and every caller then applied its
|
|
10
|
+
own default to the result with `||` — `resolveImg(fm.img) || itemArt(type)`. So
|
|
11
|
+
`""`, `null` and an absent key were one case: all three compiled to the type's
|
|
12
|
+
default art, and a note had no way to say _ship no image_ at all.
|
|
13
|
+
|
|
14
|
+
They are now three values with two meanings, the convention the project already
|
|
15
|
+
holds for an optional "not specified" DataModel string (`nullable, initial: null`,
|
|
16
|
+
so "unset" is one honest value rather than two):
|
|
17
|
+
|
|
18
|
+
| a note writes | it means | it compiles with |
|
|
19
|
+
| -------------- | ------------------------------------ | ---------------- |
|
|
20
|
+
| nothing at all | _unset_ — name me no art | the type default |
|
|
21
|
+
| `img: null` | the same thing, said out loud | the type default |
|
|
22
|
+
| `img: ""` | _blank on purpose_ — I want no image | no image |
|
|
23
|
+
|
|
24
|
+
**`resolveImg` returns `string | null`.** `null` for an unset path, `""` for a
|
|
25
|
+
deliberate blank, the translation half unchanged. Every caller pairs its default
|
|
26
|
+
with **nullish** coalescing: `sohl/items.mjs`, three in `sohl/actors.mjs` (`img`,
|
|
27
|
+
`portrait`, and the prototype token's `texture.src`), and `engine/macros.mjs`.
|
|
28
|
+
Not `||` — that collapses a deliberate blank back into the default and takes the
|
|
29
|
+
distinction away again, and it does so silently, because `""` is falsy.
|
|
30
|
+
`itemArt()` is unaffected: a registry entry with no art throws before the
|
|
31
|
+
translation, so its result is never the unset case.
|
|
32
|
+
|
|
33
|
+
**The default-art seam is a documented extension point, so this is the substance
|
|
34
|
+
of the change, not a detail.** A consuming repository that pairs art with its own
|
|
35
|
+
`itemBuilders` entry, or calls `resolveImg` from a builder of its own, gets the
|
|
36
|
+
new reading of `""` whether or not it asked for it — which is why the sweeps have
|
|
37
|
+
to come first. `sohl-thalorna` swept forty-five `img: ""` notes ahead of this
|
|
38
|
+
release (HeroicLands/sohl-thalorna#134) and `sohl-kethira-basic` eleven
|
|
39
|
+
`portrait: ""` beings (HeroicLands/sohl-kethira-basic#81); `sohl` authors neither,
|
|
40
|
+
and its 3,125 compiled documents are byte-identical across the change.
|
|
41
|
+
|
|
42
|
+
**Both art fields, because both go through `resolveImg`.** A being carries `img`
|
|
43
|
+
(its token art) and `portrait` (its sheet portrait) independently; `portrait` is
|
|
44
|
+
not a variant spelling of `img`, and the rule belongs to the translator rather
|
|
45
|
+
than to one of the keys reaching it. This is not theoretical: `sohl-kethira-basic`
|
|
46
|
+
writes `portrait: ""` on eleven beings and `img: ""` on none, so a change — or a
|
|
47
|
+
guard — keyed on `img` alone would have called that tree clean and dropped every
|
|
48
|
+
one of those portraits.
|
|
49
|
+
|
|
50
|
+
**A warning for the old spelling.** The frontmatter lint reports `img: ""` and
|
|
51
|
+
`portrait: ""` — in either authoring position — as the meaning-change they are, on
|
|
52
|
+
the pattern the `package:` and retired-alias sweeps set: a warning, because the
|
|
53
|
+
note still compiles, to a document that is merely iconless.
|
|
54
|
+
|
|
55
|
+
**`title` is deliberately not on this rule.** It reads as a general rule about
|
|
56
|
+
optional strings and it is not: on a `type: affiliation` note `title` is
|
|
57
|
+
_simultaneously_ a declared item field whose default is `""`
|
|
58
|
+
(`sohl/item-fields.mjs`), resolved from the very same shared top-level key the
|
|
59
|
+
site emitter reads as the page title. `title: null` therefore does not fall back —
|
|
60
|
+
it stringifies, and the compiled document ships the literal `"null"`. A `title` a
|
|
61
|
+
note does not want is written by omitting the key; the emitter's `fm.title ?? name`
|
|
62
|
+
is already correct and is untouched. The lint guard is `img`'s alone for the same
|
|
63
|
+
reason.
|
|
64
|
+
|
|
65
|
+
Delivers the `img` half of #218. The `title` half — the collision above — stays
|
|
66
|
+
open.
|
|
67
|
+
- 63dfcae: **A note's own `title` no longer fills an affiliation's `system.title`** (#218).
|
|
68
|
+
|
|
69
|
+
The two were never the same quantity. A note's top-level `title` is _the title
|
|
70
|
+
of the note_ — the heading its page is published under, which the site emitter
|
|
71
|
+
reads. An `affiliation` item's `system.title` is _the style of address the office
|
|
72
|
+
carries_ — Ajaw, Warden, a person's style within the body. They collided only in
|
|
73
|
+
spelling, and field resolution's third step, the shared top-level property, fed
|
|
74
|
+
the second from the first.
|
|
75
|
+
|
|
76
|
+
That step also answers **without** applying the field's default — only the
|
|
77
|
+
in-block step does — so an authored `title: null` reached the `String()` coercion
|
|
78
|
+
unguarded and compiled to the literal string `"null"`. Fifteen `sohl-thalorna`
|
|
79
|
+
notes shipped `"system": { "title": "null" }` that way.
|
|
80
|
+
|
|
81
|
+
**Nothing in any content tree relied on the fallback.** Across all three
|
|
82
|
+
consumers — 295 `affiliation` notes in `sohl-thalorna`, 28 in
|
|
83
|
+
`sohl-kethira-basic`, none in `sohl` — not one carries a non-empty top-level
|
|
84
|
+
`title`, and `content-build package compile` emits byte-identical
|
|
85
|
+
`build/packs-json` for all three. It is a major because the rule that decides a
|
|
86
|
+
consumer's compiled documents changed with no configuration to restore it, and
|
|
87
|
+
because the generated item-field reference moves (below).
|
|
88
|
+
|
|
89
|
+
**The field is still authorable**, at the two positions that describe the
|
|
90
|
+
document rather than the note: `sohl.system.title`, and the legacy in-block
|
|
91
|
+
`sohl.title` that most trees already write. A membership's title belongs on the
|
|
92
|
+
entry in a being's `sohl.items`, as its `system.title`. `data.title` is neither —
|
|
93
|
+
`title` is not a `data:` property any note type declares, so `content-build lint`
|
|
94
|
+
refuses it.
|
|
95
|
+
|
|
96
|
+
**Declaring the exemption:** a field in an `itemBuilders` `fields:` declaration
|
|
97
|
+
may now carry `topLevelMeans`, whose value is _what the note's top-level key of
|
|
98
|
+
that name means instead_. Declaring it removes the shared top-level position from
|
|
99
|
+
that field's resolution order, and the generated item-field reference prints the
|
|
100
|
+
reason beneath the type's table, so an author reading it learns that the
|
|
101
|
+
top-level key will not fill the field. A repository that commits that page should
|
|
102
|
+
regenerate it.
|
|
103
|
+
|
|
104
|
+
The value is the reason rather than a bare flag deliberately: a boolean would
|
|
105
|
+
record the decision and lose the case for it, and the next person adding a field
|
|
106
|
+
needs to know the question exists.
|
|
107
|
+
- a41b066: **A page states its address relative to the site root.** `site.base` is no
|
|
108
|
+
longer written into a page's Hugo `url:` front matter — only into the `href`s
|
|
109
|
+
this build renders and the base a link-manifest `path` is measured against:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
# emitted page
|
|
113
|
+
url: /doc-rulesintro/ # was: /sohl/doc-rulesintro/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`site.base` was two quantities wearing one name. It is correctly _where the
|
|
117
|
+
package is served_ — the prefix on every rendered `href`, and the base a
|
|
118
|
+
manifest `path` is stripped against — and `/<contentPackage>/` is the right
|
|
119
|
+
default for that. It was also written verbatim into each page's `url:`, and that
|
|
120
|
+
is a different quantity: Hugo resolves `url` against `baseURL`, whose path for
|
|
121
|
+
every consumer that exists **already is** the package base. So the prefix was
|
|
122
|
+
written twice, and every content page — plus the homepage — published one
|
|
123
|
+
package segment too deep:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
/sohl/doc-rulesintro/ 404 /sohl/sohl/doc-rulesintro/ 200
|
|
127
|
+
/thalorna/being-afzndhprnzr/ 404 /thalorna/thalorna/being-afzndhprnzr/ 200
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Not one address a link manifest advertised resolved: **0 of `sohl`'s 2,988
|
|
131
|
+
entries**, and 0 of `thalorna`'s 2,585.
|
|
132
|
+
|
|
133
|
+
**One value fed two readers that need opposite framings**, which is why no
|
|
134
|
+
setting could fix it from a consumer: `site.base: "/"` bought the addresses and
|
|
135
|
+
short-changed the hrefs, and the default did the reverse. They are now separate,
|
|
136
|
+
and each reader gets the form it needs:
|
|
137
|
+
|
|
138
|
+
| Reader | Gets | Because |
|
|
139
|
+
| --------------------------------------------------- | --------------------------- | ------------------------------------------------------------------- |
|
|
140
|
+
| A page's own `url:` front matter | `/<type>-<shortcode>/` | Hugo prefixes the site's `baseURL` path to it |
|
|
141
|
+
| The address index a `[[wikilink]]` resolves through | `<base><type>-<shortcode>/` | A browser resolves the rendered `href` against nothing |
|
|
142
|
+
| A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; unchanged, byte for byte |
|
|
143
|
+
|
|
144
|
+
The homepage moves with them: `homepageFrontmatter` states `/homepage-root/`,
|
|
145
|
+
and no longer takes a `base` (nor does `writeHomepages`' fourth argument, which
|
|
146
|
+
existed only to supply it). `trees` pages and section landings never stated a
|
|
147
|
+
`url:` and are untouched — they take their address from their path.
|
|
148
|
+
|
|
149
|
+
**Take this release and drop your `site.base`.** Both publishing consumers set
|
|
150
|
+
`site.base: "/"` as a stopgap
|
|
151
|
+
(HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1813, HeroicLands/sohl-thalorna#131),
|
|
152
|
+
which bought the correct addresses at the cost of same-package body links
|
|
153
|
+
rendering `/doc-x/` rather than `/sohl/doc-x/` — dead either way, so nothing
|
|
154
|
+
regressed. With this release the stopgap is no longer needed and no longer
|
|
155
|
+
harmless: keeping it leaves those hrefs short. Delete the `base:` line and the
|
|
156
|
+
default is right for both halves.
|
|
157
|
+
|
|
158
|
+
**Verified against pristine `git archive origin/main` extractions of all three
|
|
159
|
+
consumers**, in both configurations, through `content-build site` **and** Hugo
|
|
160
|
+
0.165:
|
|
161
|
+
|
|
162
|
+
| Consumer | `site.base` | Page `url:` | Rendered path | Manifest entries resolving |
|
|
163
|
+
| ---------- | ----------- | --------------------- | ------------------------------ | -------------------------- |
|
|
164
|
+
| `sohl` | unset | `/doc-rulesintro/` | `/sohl/doc-rulesintro/` | 2,988 of 2,988 (was 0) |
|
|
165
|
+
| `sohl` | `"/"` | `/doc-rulesintro/` | `/sohl/doc-rulesintro/` | 2,988 of 2,988 |
|
|
166
|
+
| `thalorna` | unset | `/being-afzndhprnzr/` | `/thalorna/being-afzndhprnzr/` | 2,585 of 2,585 (was 0) |
|
|
167
|
+
| `thalorna` | `"/"` | `/being-afzndhprnzr/` | `/thalorna/being-afzndhprnzr/` | 2,585 of 2,585 |
|
|
168
|
+
|
|
169
|
+
No `/sohl/sohl/` or `/thalorna/thalorna/` path exists in either built tree, the
|
|
170
|
+
section landings, the content mount and the `dev-docs` tree pages all still
|
|
171
|
+
resolve, and with the stopgap dropped a same-package body link renders
|
|
172
|
+
`<a href=/sohl/doc-skills/>` again. `lint` and `links` are identical line for
|
|
173
|
+
line in every combination — `sohl` green, `sohl-thalorna` exactly as red as its
|
|
174
|
+
own content gap leaves it (1,983 lint findings, 122 link findings).
|
|
175
|
+
|
|
176
|
+
**Exactly one line per page changes, and nothing else does.** Of `sohl`'s 1,671
|
|
177
|
+
emitted files, 1,606 differ — the 1,605 content pages and the homepage — each by
|
|
178
|
+
its `url:` alone; the 46 tree pages and 19 landings are byte-identical, and
|
|
179
|
+
`build/manifests/sohl.json` is byte-identical. `sohl-kethira-basic` publishes a
|
|
180
|
+
homepage and nothing else (`publish.site: homepage`) and declares no
|
|
181
|
+
`site.base`: its console output is identical, its one emitted page moves from
|
|
182
|
+
`/kethira/kethira/homepage-root/` to `/kethira/homepage-root/`, and its landing
|
|
183
|
+
at `/kethira/` is unaffected.
|
|
184
|
+
|
|
185
|
+
Closes #217
|
|
186
|
+
|
|
3
187
|
## 14.0.0
|
|
4
188
|
|
|
5
189
|
### Major Changes
|
package/CONTENT.md
CHANGED
|
@@ -455,9 +455,9 @@ error: pack "characters" (Actor) reads the compiled output of the Item pack
|
|
|
455
455
|
|
|
456
456
|
### An item type's default art
|
|
457
457
|
|
|
458
|
-
A note that
|
|
459
|
-
|
|
460
|
-
|
|
458
|
+
A note that names no art gets its type's **default art**, and a type declares
|
|
459
|
+
that art in the same place it declares its builder. An `itemBuilders` entry may
|
|
460
|
+
be written two ways:
|
|
461
461
|
|
|
462
462
|
```js
|
|
463
463
|
itemBuilders: {
|
|
@@ -478,8 +478,60 @@ same `resolveImg` rule as a note's `img:`, so `icons/relic.svg` means _this_
|
|
|
478
478
|
repository's asset root — `modules/sohl-relics/assets/icons/relic.svg` — and an
|
|
479
479
|
already-served path (`systems/sohl/assets/icons/…`) passes through untouched.
|
|
480
480
|
|
|
481
|
-
|
|
482
|
-
|
|
481
|
+
#### "Names no art" and "wants no art" are different (#218)
|
|
482
|
+
|
|
483
|
+
A note has two ways to leave `img:` empty, and they mean opposite things:
|
|
484
|
+
|
|
485
|
+
| a note writes | it means | it compiles with |
|
|
486
|
+
| -------------- | ------------------------------------ | ---------------- |
|
|
487
|
+
| nothing at all | _unset_ — name me no art | the type default |
|
|
488
|
+
| `img: null` | the same thing, said out loud | the type default |
|
|
489
|
+
| `img: ""` | _blank on purpose_ — I want no image | no image |
|
|
490
|
+
| `img: <path>` | this art | that path |
|
|
491
|
+
|
|
492
|
+
`resolveImg` returns `null` for the first two and `""` for the third, and every
|
|
493
|
+
caller pairs its default with **nullish** coalescing — `resolveImg(fm.img) ?? itemArt(type)`.
|
|
494
|
+
Never `||`: that collapses a deliberate blank back into the default and takes the
|
|
495
|
+
distinction away again, which is exactly what the function used to do.
|
|
496
|
+
|
|
497
|
+
**`portrait` is the same field twice over.** A being carries `img` (its token
|
|
498
|
+
art) and `portrait` (its sheet portrait) independently, and both resolve through
|
|
499
|
+
`resolveImg`, so the rule above is the rule for both.
|
|
500
|
+
|
|
501
|
+
This is the convention the project already holds for an optional "not specified"
|
|
502
|
+
DataModel string — `nullable, initial: null`, so "unset" is one honest value
|
|
503
|
+
rather than two.
|
|
504
|
+
|
|
505
|
+
> **The rule is `img`'s, and does not extend to `title`.** `title` is not art
|
|
506
|
+
> and never reaches `resolveImg`, so nothing here applies to it.
|
|
507
|
+
>
|
|
508
|
+
> The reason used to be sharper, and is no longer true: a note's top-level
|
|
509
|
+
> `title` was _also_ the shared source for an `affiliation` item's `system.title`
|
|
510
|
+
> (`sohl/item-fields.mjs`, "the style of address the office carries"), so one
|
|
511
|
+
> authored key fed two unrelated destinations that disagreed about what empty
|
|
512
|
+
> means — and `title: null` stringified into the compiled document as the literal
|
|
513
|
+
> `"null"`. That collision is gone: the field declares `topLevelMeans`, and the
|
|
514
|
+
> top-level key is no longer a source for it (#218).
|
|
515
|
+
>
|
|
516
|
+
> So `title: null` is now a note declining to state a heading, and the site
|
|
517
|
+
> emitter's `fm.title ?? name` falls back to `name.full`. `title: ""` still
|
|
518
|
+
> publishes a deliberately blank heading, and nothing warns about that yet.
|
|
519
|
+
|
|
520
|
+
Because `""` used to mean "unset", a note still carrying that spelling has
|
|
521
|
+
quietly changed meaning, and the frontmatter lint says so — for either art
|
|
522
|
+
field:
|
|
523
|
+
|
|
524
|
+
```
|
|
525
|
+
Note.md:9:1: warning: `img: ""` means "ship no art at all" — it no longer falls
|
|
526
|
+
back to this type's default. Write `img: null` for a note that simply names
|
|
527
|
+
none; keep `""` only where the document is meant to have no image
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
A warning, not an error: the note still compiles, to a document that is merely
|
|
531
|
+
iconless.
|
|
532
|
+
|
|
533
|
+
**A type with neither is a build error, deliberately.** When a note names no
|
|
534
|
+
art and its type pairs none, the pack build aborts rather than shipping an
|
|
483
535
|
item with a mismatched icon:
|
|
484
536
|
|
|
485
537
|
```
|
|
@@ -570,6 +622,47 @@ system `S` is:
|
|
|
570
622
|
`sohl:`", which is the degenerate case where source and destination happen to
|
|
571
623
|
share a name.
|
|
572
624
|
|
|
625
|
+
**A spelling that means two different things skips step 3.** Because a field's
|
|
626
|
+
`name` doubles as its identity and as the shared property it draws from, the two
|
|
627
|
+
coincide only while the note vocabulary and the system vocabulary agree about
|
|
628
|
+
what a spelling means. `title` is where they do not. A note's top-level `title`
|
|
629
|
+
is _the title of the note_ — the heading its page publishes under, which the
|
|
630
|
+
site emitter reads. An `affiliation` item's `system.title` is _the style of
|
|
631
|
+
address the office carries_ — Ajaw, Warden, a person's style within the body.
|
|
632
|
+
They are unrelated quantities, and step 3 used to feed the second from the first
|
|
633
|
+
(#218).
|
|
634
|
+
|
|
635
|
+
That was not merely untidy, because **step 3 answers without applying
|
|
636
|
+
`field.default`** — only step 2 does — so an authored `title: null` reached the
|
|
637
|
+
field's `String()` coercion unguarded and shipped as the literal string `"null"`.
|
|
638
|
+
|
|
639
|
+
So a field may declare `topLevelMeans`: what the top-level key of that name means
|
|
640
|
+
_instead_. Declaring it removes step 3 for that field, and the value is the
|
|
641
|
+
reason rather than a bare flag, so the collision is legible where the field is
|
|
642
|
+
declared and the generated field reference can print it. It is a per-field
|
|
643
|
+
opt-out, not a change to the order — step 3 is right wherever the two levels
|
|
644
|
+
state the same quantity, which is nearly everywhere: `subType` is the other
|
|
645
|
+
declared item field spelled like a note-level key, and there the two agree by
|
|
646
|
+
design.
|
|
647
|
+
|
|
648
|
+
**An exempted field is still authorable**, at the two positions that describe the
|
|
649
|
+
document rather than the note:
|
|
650
|
+
|
|
651
|
+
```yaml
|
|
652
|
+
title: The Order of the Silver Hand # the note's own heading — reaches the page
|
|
653
|
+
type: affiliation
|
|
654
|
+
subType: order
|
|
655
|
+
sohl:
|
|
656
|
+
system:
|
|
657
|
+
title: Warden # → document.system.title, the style of address
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
`sohl.title`, the legacy in-block position, works the same way. A membership — a
|
|
661
|
+
`title` a particular being holds — is authored on the entry in that being's
|
|
662
|
+
`sohl.items`, whose `system` is overlaid on the catalogue document directly.
|
|
663
|
+
`data.title` is neither position: `title` is not a `data:` property any note type
|
|
664
|
+
declares, so the frontmatter lint refuses it.
|
|
665
|
+
|
|
573
666
|
**`<system>.system` is written through verbatim**, at the DataModel's own paths,
|
|
574
667
|
with no renaming layer. A key the system's published `schema.json` does not
|
|
575
668
|
declare for the subtype the note compiles into is an **error naming the note**,
|
|
@@ -1234,6 +1327,21 @@ order to satisfy a rendering engine's directory semantics. The file is now
|
|
|
1234
1327
|
`<mount>/<type>-<shortcode>.md` and the front-matter `url:` still publishes it at
|
|
1235
1328
|
the package root, one level above.
|
|
1236
1329
|
|
|
1330
|
+
**A page states its address without the package base; everything pointing _at_
|
|
1331
|
+
it composes one** (#217). They read as one quantity and are two:
|
|
1332
|
+
|
|
1333
|
+
| Written | Form | Because |
|
|
1334
|
+
| ------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
|
|
1335
|
+
| A page's own `url:` front matter | `/<type>-<shortcode>/` | Hugo resolves it against `baseURL`, whose path already _is_ where the package is served |
|
|
1336
|
+
| Every `href` this build renders into a body | `<base><type>-<shortcode>/` | A browser resolves it against nothing |
|
|
1337
|
+
| A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; a consumer prefixes its own |
|
|
1338
|
+
|
|
1339
|
+
`site.base` is the second and third of those and reaches the first not at all.
|
|
1340
|
+
It used to be written into the `url:` as well, so every consumer's Hugo prefixed
|
|
1341
|
+
its own base to a value that already carried one and published every content
|
|
1342
|
+
page a segment too deep — `/sohl/sohl/doc-rulesintro/`, 404 at the address the
|
|
1343
|
+
manifest, the sitemap and every inbound link named.
|
|
1344
|
+
|
|
1237
1345
|
**There is no landing page.** A `README.md` was its section's landing and
|
|
1238
1346
|
addressed the section itself; that is retired with the section. A page that
|
|
1239
1347
|
introduces the notes of a type is an ordinary note addressed `doc-<type>`, with
|
|
@@ -1363,7 +1471,8 @@ The homepage's file is written at the root of `site.out` — the package's own
|
|
|
1363
1471
|
site root, one level above the content mount, which is where
|
|
1364
1472
|
`publish.address.prefix` puts everything else — under the name its address gives
|
|
1365
1473
|
it, `homepage-root.md`. As with every other page, the front matter's `url`
|
|
1366
|
-
decides where it publishes.
|
|
1474
|
+
decides where it publishes, and states it relative to the site root — `site.base`
|
|
1475
|
+
does not reach it (#217).
|
|
1367
1476
|
|
|
1368
1477
|
**What it does not do is decide addresses.** Those come from `publish.address`,
|
|
1369
1478
|
the same setting the link manifest reads, so a page and its manifest entry cannot
|
|
@@ -1374,7 +1483,7 @@ published beside the content:
|
|
|
1374
1483
|
```yaml
|
|
1375
1484
|
site:
|
|
1376
1485
|
out: kb/content # required; wiped on every run
|
|
1377
|
-
base: /sohl/ # default: /<contentPackage>/
|
|
1486
|
+
base: /sohl/ # default: /<contentPackage>/ — hrefs only, never a page's `url:`
|
|
1378
1487
|
packages: [sohl, thalorna] # default: just contentPackage
|
|
1379
1488
|
backfillSections: true
|
|
1380
1489
|
landing: { title: Knowledgebase, type: knowledgebase }
|
|
@@ -1394,18 +1503,18 @@ site:
|
|
|
1394
1503
|
dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
|
|
1395
1504
|
```
|
|
1396
1505
|
|
|
1397
|
-
| Key | What it decides
|
|
1398
|
-
| ------------------ |
|
|
1399
|
-
| `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below.
|
|
1400
|
-
| `base` | Where the package is served. Defaults to `/<contentPackage>/`.
|
|
1401
|
-
| `packages` | Which content packages this site renders. Defaults to its own.
|
|
1402
|
-
| `sections` | The Hugo sections this site declares, and what each says about itself — see below.
|
|
1403
|
-
| `readmeSections` | The same, for a `trees` entry, whose landing comes from its own `README`.
|
|
1404
|
-
| `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's.
|
|
1405
|
-
| `backfillSections` | Write a bare `_index.md` for any other directory directly under the mount.
|
|
1406
|
-
| `trees` | Extra source trees published beside the content, preserving their source layout below a section.
|
|
1407
|
-
| `pass` | A named bundle of this repository's own body rewrites.
|
|
1408
|
-
| `passOptions` | That bundle's options.
|
|
1506
|
+
| Key | What it decides |
|
|
1507
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1508
|
+
| `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
|
|
1509
|
+
| `base` | Where the package is served: the prefix on every rendered `href`, and what a manifest `path` is measured against. It reaches no page's own `url:` — see [A page's URL is its address](#a-pages-url-is-its-address). Defaults to `/<contentPackage>/`. |
|
|
1510
|
+
| `packages` | Which content packages this site renders. Defaults to its own. |
|
|
1511
|
+
| `sections` | The Hugo sections this site declares, and what each says about itself — see below. |
|
|
1512
|
+
| `readmeSections` | The same, for a `trees` entry, whose landing comes from its own `README`. |
|
|
1513
|
+
| `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's. |
|
|
1514
|
+
| `backfillSections` | Write a bare `_index.md` for any other directory directly under the mount. |
|
|
1515
|
+
| `trees` | Extra source trees published beside the content, preserving their source layout below a section. |
|
|
1516
|
+
| `pass` | A named bundle of this repository's own body rewrites. |
|
|
1517
|
+
| `passOptions` | That bundle's options. |
|
|
1409
1518
|
|
|
1410
1519
|
### What a section may declare
|
|
1411
1520
|
|
package/MIGRATING.md
CHANGED
|
@@ -1,3 +1,188 @@
|
|
|
1
|
+
# Migrating to `@heroiclands/package-build` 15.0.0
|
|
2
|
+
|
|
3
|
+
**Three unrelated changes ship in this major, and they ask different
|
|
4
|
+
repositories for different things.** A page's `url:` front matter is now stated
|
|
5
|
+
relative to the **site root** (#217), which a site-publishing repository answers
|
|
6
|
+
by deleting one line; an empty art path now means the opposite of an absent one
|
|
7
|
+
(#218), which a content tree answers by sweeping `img: ""` and `portrait: ""` to
|
|
8
|
+
`null`; and a note's own `title` no longer reaches an `affiliation` item's
|
|
9
|
+
`system.title` (#218), which asks nothing of any repository that exists today.
|
|
10
|
+
|
|
11
|
+
Route yourself by what you have. A repository that publishes no site skips
|
|
12
|
+
§1–§2. One that authors no empty art path skips §3–§4. One with no
|
|
13
|
+
`type: affiliation` notes skips §6 — and §5, which is only the seam between the
|
|
14
|
+
two halves of #218.
|
|
15
|
+
|
|
16
|
+
## 1. Drop the `site.base: "/"` stopgap (#217)
|
|
17
|
+
|
|
18
|
+
If your repository set it to stop every page publishing at
|
|
19
|
+
`/<package>/<package>/<address>/`, delete the line — the default,
|
|
20
|
+
`/<contentPackage>/`, is now right for both halves:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
site:
|
|
24
|
+
out: kb/content
|
|
25
|
+
# base: "/" ← delete this
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keeping it is no longer harmless: the addresses stay correct either way, but
|
|
29
|
+
every same-package link this build renders into a page body stays short
|
|
30
|
+
(`/doc-skills/` rather than `/sohl/doc-skills/`) and 404s.
|
|
31
|
+
|
|
32
|
+
A repository that is genuinely served somewhere other than
|
|
33
|
+
`/<contentPackage>/` still says so here, and that value still reaches every
|
|
34
|
+
`href`.
|
|
35
|
+
|
|
36
|
+
## 2. What the `url:` change does _not_ touch
|
|
37
|
+
|
|
38
|
+
- No note edits, and no configuration key added or removed.
|
|
39
|
+
- Every link-manifest entry, including each entry's `path`, is byte-identical.
|
|
40
|
+
- Every compiled compendium document is unchanged.
|
|
41
|
+
- `trees` pages and section landings state no `url:` and do not move.
|
|
42
|
+
|
|
43
|
+
The published address of every content page **does** move — from
|
|
44
|
+
`/<package>/<package>/<address>/`, where nothing linked, to
|
|
45
|
+
`/<package>/<address>/`, which is what the link manifest, the sitemap and every
|
|
46
|
+
inbound link already named.
|
|
47
|
+
|
|
48
|
+
## 3. Sweep `img: ""` and `portrait: ""` to `null` (#218)
|
|
49
|
+
|
|
50
|
+
`resolveImg` opened with `if (!raw) return ""`, and every caller applied its own
|
|
51
|
+
default to the result with `||`. So `""`, `null` and an absent key were one
|
|
52
|
+
case: all three compiled to the type's default art, and a note had no way to say
|
|
53
|
+
"ship no image" at all.
|
|
54
|
+
|
|
55
|
+
They are now three values with two meanings:
|
|
56
|
+
|
|
57
|
+
| a note writes | it means | it compiles with |
|
|
58
|
+
| -------------- | ------------------------------------ | ---------------- |
|
|
59
|
+
| nothing at all | _unset_ — name me no art | the type default |
|
|
60
|
+
| `img: null` | the same thing, said out loud | the type default |
|
|
61
|
+
| `img: ""` | _blank on purpose_ — I want no image | no image |
|
|
62
|
+
|
|
63
|
+
So a note still carrying `img: ""` **loses its default art**. The same holds for
|
|
64
|
+
`portrait:`, which a being carries independently of `img` and which resolves
|
|
65
|
+
through the same function. Find them both:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
grep -rnE '^[[:space:]]*(img|portrait):[[:space:]]*""[[:space:]]*$' assets/content --include='*.md'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
and write `null` in each — unless the document really is meant to have no image,
|
|
72
|
+
which is what `""` now says. Before this release `sohl-thalorna` swept forty-five
|
|
73
|
+
`img: ""` notes and `sohl-kethira-basic` eleven `portrait: ""` beings; `sohl`
|
|
74
|
+
authors neither.
|
|
75
|
+
|
|
76
|
+
The frontmatter lint reports every one that is left, as a warning:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Note.md:9:1: warning: `img: ""` means "ship no art at all" — it no longer falls
|
|
80
|
+
back to this type's default. Write `img: null` for a note that simply names
|
|
81
|
+
none; keep `""` only where the document is meant to have no image
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 4. Custom callers pair their default with `??`, not `||`
|
|
85
|
+
|
|
86
|
+
`resolveImg` returns `string | null` now: `null` for an unset path, `""` for a
|
|
87
|
+
deliberate blank. A consumer that calls it directly — a custom item builder, a
|
|
88
|
+
compiler of its own — must switch:
|
|
89
|
+
|
|
90
|
+
```diff
|
|
91
|
+
-img: resolveImg(fm.img) || MY_DEFAULT,
|
|
92
|
+
+img: resolveImg(fm.img) ?? MY_DEFAULT,
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`||` still compiles and still looks right; it silently reinstates the old
|
|
96
|
+
conflation, because `""` is falsy. Every caller in this package moved:
|
|
97
|
+
`sohl/items.mjs`, three in `sohl/actors.mjs` (`img`, `portrait`, and the
|
|
98
|
+
prototype token's `texture.src`), and `engine/macros.mjs`.
|
|
99
|
+
|
|
100
|
+
`itemArt()` is unaffected — a registry entry with no art throws before the
|
|
101
|
+
translation, so its result is never the unset case.
|
|
102
|
+
|
|
103
|
+
## 5. `title` is **not** on the art rule
|
|
104
|
+
|
|
105
|
+
`resolveImg`'s rule reads as a general one about optional strings, and it is
|
|
106
|
+
not — it belongs to the function, and `title` never goes through it. So do not
|
|
107
|
+
extend §3's sweep to `title`, and do not read the table in §3 as saying anything
|
|
108
|
+
about it.
|
|
109
|
+
|
|
110
|
+
**The reason has changed since this section was first written, and the earlier
|
|
111
|
+
one is no longer true.** It used to be that a note's top-level `title` was
|
|
112
|
+
_simultaneously_ the page's heading and the shared source for an `affiliation`
|
|
113
|
+
item's `system.title`, so `title: null` did not fall back — it stringified, and
|
|
114
|
+
the compiled document shipped the literal string `"null"`. That collision is what
|
|
115
|
+
§6 removes: the top-level key is no longer a source for the item field at all.
|
|
116
|
+
|
|
117
|
+
So `title: null` is now simply a note declining to state a heading, and the site
|
|
118
|
+
emitter's `fm.title ?? name` falls back to `name.full` as it always did. It
|
|
119
|
+
reaches no document field and stringifies nothing.
|
|
120
|
+
|
|
121
|
+
`title: ""` still publishes a **deliberately blank heading** — which is what cost
|
|
122
|
+
fifteen `sohl-thalorna` notes their names (HeroicLands/sohl-thalorna#129) — and
|
|
123
|
+
nothing warns about it yet. Whether the frontmatter lint should is #218's, still
|
|
124
|
+
open, and deliberately not settled by the art-field warning in §3.
|
|
125
|
+
|
|
126
|
+
## 6. A note's own `title` no longer fills `system.title` (#218)
|
|
127
|
+
|
|
128
|
+
**No note edit, no URL change, and no compiled document moves.** An
|
|
129
|
+
`affiliation` item's `system.title` stops falling back to the note's own
|
|
130
|
+
top-level `title`. The two were never the same quantity — a note's `title` is
|
|
131
|
+
the heading its page publishes under, while `system.title` is the style of
|
|
132
|
+
address an office carries, Ajaw or Warden — and no note in any content tree
|
|
133
|
+
relied on the fallback, so `content-build package compile` emits byte-identical
|
|
134
|
+
`build/packs-json` for every consumer.
|
|
135
|
+
|
|
136
|
+
Only a `type: affiliation` note is affected, and only if it carries a top-level
|
|
137
|
+
`title` it meant as the item's field rather than as the page's heading:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
grep -rl '^type: affiliation' assets/content --include='*.md' \
|
|
141
|
+
| xargs grep -l '^title:'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Anything that turns up wanted one of the two positions that describe the
|
|
145
|
+
_document_ rather than the note — `sohl.system.title`, or `sohl.title`, the
|
|
146
|
+
legacy in-block key most trees already write. A membership's title belongs on
|
|
147
|
+
the entry in the being's `sohl.items`, as `system.title`.
|
|
148
|
+
|
|
149
|
+
`data: { title: ... }` is not a position and never was: `title` is not a `data:`
|
|
150
|
+
property any note type declares, so `content-build lint` refuses it.
|
|
151
|
+
|
|
152
|
+
### Regenerate the item field reference
|
|
153
|
+
|
|
154
|
+
The generated page now prints, under each affected type's table, what the
|
|
155
|
+
top-level key of a non-shared field means instead — so an author reading the
|
|
156
|
+
table learns that writing `title:` at the top of a note will not fill this
|
|
157
|
+
field. Re-run the generator and commit the result, or a repository that checks
|
|
158
|
+
the page for staleness reports it stale:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx content-build docs item-fields --out <the path your repo uses>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Declaring your own non-shared field
|
|
165
|
+
|
|
166
|
+
A field in an `itemBuilders` `fields:` declaration may now carry
|
|
167
|
+
`topLevelMeans`, whose value is _what the note's top-level key of that name
|
|
168
|
+
means instead_. Declaring it removes the shared top-level position from that
|
|
169
|
+
field's resolution order:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
{
|
|
173
|
+
name: "title",
|
|
174
|
+
to: "title",
|
|
175
|
+
...STRING,
|
|
176
|
+
default: "",
|
|
177
|
+
topLevelMeans: "the note's own title — the heading its page is published under",
|
|
178
|
+
describe: "The style of address the office carries.",
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The value is the reason rather than a bare flag on purpose: the next person
|
|
183
|
+
adding a field needs to know the question exists, and a boolean with a comment
|
|
184
|
+
beside it is two statements of one rule.
|
|
185
|
+
|
|
1
186
|
# Migrating to `@heroiclands/package-build` 12.0.0
|
|
2
187
|
|
|
3
188
|
**No note edit, no URL change, and one thing to check in the Hugo layer.** A
|
package/docs/content-format.md
CHANGED
|
@@ -138,6 +138,17 @@ it _there_, and a system that disagrees is not in error. A weapon weighs what
|
|
|
138
138
|
This is the same rule as `hm3.type` overriding a derived document type, applied
|
|
139
139
|
to fields: derive from the shared source, and let the system state the exception.
|
|
140
140
|
|
|
141
|
+
**A field whose spelling means something else at the note level has no shared
|
|
142
|
+
source.** The fallback assumes the two vocabularies agree about what a name
|
|
143
|
+
means, and they do not always: a note's top-level `title` is the heading its page
|
|
144
|
+
publishes under, while an `affiliation` item's `system.title` is the style of
|
|
145
|
+
address an office carries. Where they diverge, the field declares what the
|
|
146
|
+
top-level key means instead, and the top level stops being read for it — leaving
|
|
147
|
+
`<system>.system.<field>` and the legacy in-block position, which describe the
|
|
148
|
+
document rather than the note. `title` is the one field this applies to; `subType`
|
|
149
|
+
is the other declared item field spelled like a note-level key, and there the two
|
|
150
|
+
levels mean the same thing by design.
|
|
151
|
+
|
|
141
152
|
**A `WikiLink` becomes a shortcode where the target field expects one.** SoHL
|
|
142
153
|
stores cross-references as shortcode strings, which is what the `Code` suffix
|
|
143
154
|
marks: `data.assocSkill` is a link to a skill note, and `system.assocSkillCode`
|
|
@@ -785,6 +796,16 @@ mapping for a field no schema declares is the drift these tables exist to catch.
|
|
|
785
796
|
likewise absent here: they are filled on an embedded membership, never from a
|
|
786
797
|
catalogue note's `data:`.
|
|
787
798
|
|
|
799
|
+
**`system.title` is not the note's `title`.** The two are unrelated quantities
|
|
800
|
+
that share a spelling. A note's top-level `title` is _the title of the note_ —
|
|
801
|
+
the heading its page is published under; an affiliation's `system.title` is _the
|
|
802
|
+
style of address the office carries_, Ajaw or Warden, which a being holds by
|
|
803
|
+
virtue of its rank. So the top-level key is **not** a shared source for this
|
|
804
|
+
field, and a note that writes one is stating its own heading and nothing else
|
|
805
|
+
(#218). Author the style of address on the membership — the `system.title` of the
|
|
806
|
+
entry in a being's `sohl.items` — or, on a catalogue note that genuinely carries
|
|
807
|
+
one, at `sohl.system.title`.
|
|
808
|
+
|
|
788
809
|
### type: affliction
|
|
789
810
|
|
|
790
811
|
Represents an affliction.
|
|
@@ -111,6 +111,34 @@ function fieldTable(fields) {
|
|
|
111
111
|
return padTable(rows);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The fields of one type that are **not** filled from the note's top level.
|
|
116
|
+
*
|
|
117
|
+
* A field ordinarily falls back to the top-level property spelled like its
|
|
118
|
+
* name, so an author who writes `weight: 3` at the top of a note reasonably
|
|
119
|
+
* expects it to reach the document. Where that spelling means something else at
|
|
120
|
+
* the note level the fallback is off, and an author has no way to tell from the
|
|
121
|
+
* table — the field is there, the value is written, and the document ships the
|
|
122
|
+
* default. So the reason each such field declares is rendered beside its table
|
|
123
|
+
* rather than left in the source (#218).
|
|
124
|
+
*
|
|
125
|
+
* Below the table, not inside it: the reason is a sentence or two, and
|
|
126
|
+
* {@link padTable} pads every column to its widest cell, so a cell holding it
|
|
127
|
+
* would stretch the whole type's table past legibility.
|
|
128
|
+
*
|
|
129
|
+
* @param {readonly object[]} fields - The type's declaration.
|
|
130
|
+
* @returns {string[]} Markdown lines, empty when the type exempts nothing.
|
|
131
|
+
*/
|
|
132
|
+
function sharedExemptions(fields) {
|
|
133
|
+
const exempt = authoredFields(fields).filter((field) => field.topLevelMeans);
|
|
134
|
+
if (!exempt.length) return [];
|
|
135
|
+
return exempt.flatMap((field) => [
|
|
136
|
+
`**\`${field.name}\` is not read from the note's top level.** There it means ` +
|
|
137
|
+
`${field.topLevelMeans}`,
|
|
138
|
+
"",
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
/**
|
|
115
143
|
* A minimal note for one type: the frontmatter envelope every note carries,
|
|
116
144
|
* plus exactly the `sohl:` fields the type requires.
|
|
@@ -204,6 +232,7 @@ export function renderItemFieldReference({
|
|
|
204
232
|
"",
|
|
205
233
|
...fieldTable(declared[type]),
|
|
206
234
|
"",
|
|
235
|
+
...sharedExemptions(declared[type]),
|
|
207
236
|
...workedExample(type, declared[type]),
|
|
208
237
|
"",
|
|
209
238
|
);
|