@heroiclands/package-build 22.6.0 → 22.7.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 +9 -0
- package/CONTENT.md +5 -0
- package/docs/api.md +17 -0
- package/docs/content-format.md +48 -13
- package/engine/draft-notice.mjs +175 -0
- package/engine/index.mjs +3 -0
- package/engine/item-docs.mjs +9 -8
- package/engine/journals.mjs +23 -4
- package/engine/pdf-build.mjs +8 -1
- package/engine/pdf-render.mjs +6 -0
- package/hm3/actors.mjs +5 -1
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -1
- package/types/engine/draft-notice.d.mts +77 -0
- package/types/engine/index.d.mts +1 -0
- package/types/engine/item-docs.d.mts +9 -8
- package/types/engine/journals.d.mts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
**Draft notes say so.** A note tagged `draft` states that it is unfinished and
|
|
8
|
+
that nothing in it is settled, at the head of its compendium entry, on the
|
|
9
|
+
character sheet a referee opens, and on its page in the book. A finished note
|
|
10
|
+
carries nothing.
|
|
11
|
+
|
|
3
12
|
## 22.6.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/CONTENT.md
CHANGED
|
@@ -1113,6 +1113,11 @@ page, no address and no link target. A note with a body and the `draft` tag is a
|
|
|
1113
1113
|
**draft**, and one with a body and no tag is **full**.
|
|
1114
1114
|
`docs/content-format.md` states the rule; this is what the lint does with it.
|
|
1115
1115
|
|
|
1116
|
+
A draft says so on every surface it reaches — its journal entry, the Actor a
|
|
1117
|
+
being compiles into, its page in the book — in one sentence at the head of the
|
|
1118
|
+
entry, above the infobox and the prose. A stub says nothing anywhere, because it
|
|
1119
|
+
publishes nothing to say it on.
|
|
1120
|
+
|
|
1116
1121
|
**A document that would be empty is not created**, and the index then names no
|
|
1117
1122
|
artefact — `uuid`, `path` and `address` are `null`. An unwritten note still
|
|
1118
1123
|
compiles every document derived from `data:`, so an arcane talent is still its
|
package/docs/api.md
CHANGED
|
@@ -427,6 +427,23 @@ The three states of a note — **stub**, **draft**, **full** — and the one obs
|
|
|
427
427
|
| `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub |
|
|
428
428
|
| `bodyWordCount` | `bodyWordCount(body)` | `number` | counting the prose a reader meets, with a wikilink counting as the one word it renders |
|
|
429
429
|
|
|
430
|
+
### `engine.draftNotice`
|
|
431
|
+
|
|
432
|
+
What a draft note says about itself, in one sentence, on every surface it reaches. The words are stated once and each renderer composes its own markup around them, so a reader meeting the note on a page, in a compendium and in the book meets one statement. The mark is a notice rather than a hazard, and the book draws it rather than setting a glyph, because a face missing one is silent in Typst.
|
|
433
|
+
|
|
434
|
+
| Export | Signature | Returns | Use it when |
|
|
435
|
+
| ------------------------- | ------------------------------------ | -------- | ---------------------------------------------------------------------------------- |
|
|
436
|
+
| `DRAFT_NOTICE` | `const DRAFT_NOTICE` | `string` | reading the sentence itself, which is the floor every surface degrades to |
|
|
437
|
+
| `DRAFT_NOTICE_LABEL` | `const DRAFT_NOTICE_LABEL` | `string` | reading the label that opens the notice |
|
|
438
|
+
| `DRAFT_NOTICE_ICON` | `const DRAFT_NOTICE_ICON` | `string` | naming the Font Awesome mark the HTML surfaces draw |
|
|
439
|
+
| `DRAFT_NOTICE_CLASS` | `const DRAFT_NOTICE_CLASS` | `string` | styling the HTML notice from a package that wants to |
|
|
440
|
+
| `BOOK_DRAFT_NOTICE` | `const BOOK_DRAFT_NOTICE` | `string` | naming the Typst function the book's notice calls |
|
|
441
|
+
| `draftNoticeHtml` | `draftNoticeHtml()` | `string` | drawing the notice for a JournalEntry page or an Actor's prose field |
|
|
442
|
+
| `draftNoticeFor` | `draftNoticeFor(frontmatter)` | `string` | asking whether a note has earned a notice, which is the one place the tag is asked |
|
|
443
|
+
| `withDraftNotice` | `withDraftNotice(frontmatter, html)` | `string` | leading a system's own prose field with the notice where one is due |
|
|
444
|
+
| `draftNoticeTypst` | `draftNoticeTypst()` | `string` | drawing the notice for a leaf of the book |
|
|
445
|
+
| `bookDraftNoticePreamble` | `bookDraftNoticePreamble()` | `string` | emitting the Typst definition the book's notice is drawn by |
|
|
446
|
+
|
|
430
447
|
### `engine.stubLint`
|
|
431
448
|
|
|
432
449
|
An empty body must be deliberate. Four rules hold the state honest — a stub states what it is, a stub is not tagged `draft`, a body that renders to nothing is an abandoned draft, and a short unmarked body is asked whether it forgot its marker — and two reports say what the rules cannot decide.
|
package/docs/content-format.md
CHANGED
|
@@ -320,16 +320,51 @@ is declared, so a near miss is a finding that names what you probably meant.
|
|
|
320
320
|
| **being kind** | `being` | `character`, `creature` |
|
|
321
321
|
| **state** | any | `draft` |
|
|
322
322
|
|
|
323
|
-
**`draft` is the one tag either build reads.**
|
|
324
|
-
link
|
|
323
|
+
**`draft` is the one tag either build reads.** It does two things and nothing
|
|
324
|
+
else: a link _into_ a draft note renders marked, and the note itself states that
|
|
325
|
+
it is unfinished wherever a reader meets it.
|
|
326
|
+
|
|
327
|
+
A link into it renders as
|
|
325
328
|
`<span class="sohl-draft-link" title="Draft — not yet written">…</span>` in a
|
|
326
329
|
compiled journal and on the website alike, with the appearance supplied by the
|
|
327
|
-
consuming system's stylesheet or the site theme.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
consuming system's stylesheet or the site theme.
|
|
331
|
+
|
|
332
|
+
The note's own statement is the **draft notice**, and it reads the same on every
|
|
333
|
+
surface:
|
|
334
|
+
|
|
335
|
+
> **Draft.** This entry is unfinished. What it states may change, and nothing in
|
|
336
|
+
> it is settled.
|
|
337
|
+
|
|
338
|
+
It leads the entry — above the infobox and above the prose — because a reader
|
|
339
|
+
deciding whether to rely on an entry has to be told before they read it. Where
|
|
340
|
+
it lands:
|
|
341
|
+
|
|
342
|
+
| Surface | What carries it |
|
|
343
|
+
| ------------ | -------------------------------------------------------------------- |
|
|
344
|
+
| JournalEntry | a `blockquote.sohl-draft-notice` at the head of the first page |
|
|
345
|
+
| Actor | the dossier the sheet draws — SoHL's `dossier`, HM3's `biography` |
|
|
346
|
+
| The book | a drawn mark and a left rule between the entry's plate and its panel |
|
|
347
|
+
| The website | the theme's own notice, above the infobox rail and the prose |
|
|
348
|
+
|
|
349
|
+
**An Actor carries it once.** A being's prose reaches two fields, and a document
|
|
350
|
+
saying the same thing twice teaches a reader to skip it — a printed sheet draws
|
|
351
|
+
both in turn — so the dossier carries the notice and the appearance is
|
|
352
|
+
untouched. The two systems pair by the authored section each field reads rather
|
|
353
|
+
than by its name, so `dossier` and `biography` carry it while `appearance` and
|
|
354
|
+
`description` do not.
|
|
355
|
+
|
|
356
|
+
**The mark is a notice, not a hazard** — `fa-circle-exclamation`, which Foundry
|
|
357
|
+
bundles, so a journal page carries it with no module stylesheet loaded. The book
|
|
358
|
+
draws its own, because a consumer declaring no icon family is the ordinary case
|
|
359
|
+
and a glyph a face lacks is silent in Typst. Stripped of mark and rule the
|
|
360
|
+
sentence still states the whole thing, which is the floor every surface is built
|
|
361
|
+
to.
|
|
362
|
+
|
|
363
|
+
**Nothing else changes**: the note compiles, validates, publishes and resolves
|
|
364
|
+
exactly as any other, and it stays in the packs, in the link manifest and on the
|
|
365
|
+
site. That is what separates the tag from the retired `draft:` field, whose whole
|
|
366
|
+
effect was to move a note from published to unresolvable without saying so — and
|
|
367
|
+
which is refused, by name, if you write it.
|
|
333
368
|
|
|
334
369
|
**The group's scope is what makes the check work.** A group names the types it
|
|
335
370
|
applies to, and a place's kinds are only ever checked on a place. Without that
|
|
@@ -1677,11 +1712,11 @@ convention rather than declared in a field — see
|
|
|
1677
1712
|
A note has three states, and two of them are nothing but conventions the format
|
|
1678
1713
|
already carried.
|
|
1679
1714
|
|
|
1680
|
-
| State | Body | Marker | Page
|
|
1681
|
-
| --------- | ------- | --------------- |
|
|
1682
|
-
| **stub** | empty | none | no
|
|
1683
|
-
| **draft** | written | `tags: [draft]` | yes,
|
|
1684
|
-
| **full** | written | none | yes
|
|
1715
|
+
| State | Body | Marker | Page | Document | Link target |
|
|
1716
|
+
| --------- | ------- | --------------- | ------------ | --------------------------------- | ----------- |
|
|
1717
|
+
| **stub** | empty | none | no | yes, where one would not be empty | no |
|
|
1718
|
+
| **draft** | written | `tags: [draft]` | yes, noticed | yes, noticed | yes |
|
|
1719
|
+
| **full** | written | none | yes | yes | yes |
|
|
1685
1720
|
|
|
1686
1721
|
"Document" here is the document a note's frontmatter earns. A system's Actor or
|
|
1687
1722
|
Item is earned by that system's block, in every state —
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
* **What a draft note says about itself**, in one sentence, on every surface it
|
|
16
|
+
* reaches.
|
|
17
|
+
*
|
|
18
|
+
* A note tagged `draft` compiles, validates, publishes and resolves like any
|
|
19
|
+
* other. What separates it from a finished note is a judgement only its author
|
|
20
|
+
* holds — the content is not settled — and a reader who is not told assumes the
|
|
21
|
+
* opposite. So each surface states it where the reader cannot miss it: at the
|
|
22
|
+
* head of the entry, above the infobox and above the prose.
|
|
23
|
+
*
|
|
24
|
+
* **The words are the same everywhere and are stated once here.** A reader who
|
|
25
|
+
* meets the same note on a page, in a compendium and in the book should meet
|
|
26
|
+
* one statement, not three that agree approximately. {@link DRAFT_NOTICE} is
|
|
27
|
+
* that statement; every renderer below composes its own markup around it and
|
|
28
|
+
* changes none of it.
|
|
29
|
+
*
|
|
30
|
+
* **A label alone will not do.** "Draft" names the writing, so it reads as
|
|
31
|
+
* rough prose; what has to land is that the facts may change. The label and the
|
|
32
|
+
* sentence are one unit, and the sentence carries the meaning by itself — which
|
|
33
|
+
* is the floor each surface is built to, because each of them can lose
|
|
34
|
+
* everything around it.
|
|
35
|
+
*
|
|
36
|
+
* ## What each surface can rely on
|
|
37
|
+
*
|
|
38
|
+
* - **Foundry** renders a JournalEntry's own HTML inside a sheet, so the floor
|
|
39
|
+
* is what a journal page carries with no module and no system stylesheet
|
|
40
|
+
* loaded. Foundry draws a left rule on a `blockquote` in any application
|
|
41
|
+
* window and bundles Font Awesome, so the rule and the mark are the host's
|
|
42
|
+
* and nothing here ships CSS. The class is a hook for a package that wants
|
|
43
|
+
* one, never a requirement.
|
|
44
|
+
* - **The book** sets Typst, where a glyph missing from the face is silent and
|
|
45
|
+
* a consumer declaring no icon family is the normal case — see {@link
|
|
46
|
+
* module:engine/content-icons}. So the mark is **drawn**, from two
|
|
47
|
+
* primitives, and cannot become a box.
|
|
48
|
+
* - **An Actor's prose** lands in a system's own sheet, in a field the system
|
|
49
|
+
* styles. It gets the same HTML the journal does; what it can rely on is the
|
|
50
|
+
* sentence.
|
|
51
|
+
*
|
|
52
|
+
* @module
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
56
|
+
|
|
57
|
+
/** The word that opens the notice, and the only part a reader reads as a label. */
|
|
58
|
+
export const DRAFT_NOTICE_LABEL = "Draft.";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The sentence, which states the fact and stops.
|
|
62
|
+
*
|
|
63
|
+
* It says the content may change rather than that the writing is rough,
|
|
64
|
+
* because that is the difference between a reader who treats the entry as
|
|
65
|
+
* provisional and one who builds on it. It gives no instruction: a notice that
|
|
66
|
+
* reaches one page in seven wears better as a statement than as a direction,
|
|
67
|
+
* and the fact carries the consequence for anyone who can act on it.
|
|
68
|
+
*
|
|
69
|
+
* @type {string}
|
|
70
|
+
*/
|
|
71
|
+
export const DRAFT_NOTICE =
|
|
72
|
+
"This entry is unfinished. What it states may change, and nothing in it is settled.";
|
|
73
|
+
|
|
74
|
+
/** The Font Awesome mark: a notice, not a hazard. */
|
|
75
|
+
export const DRAFT_NOTICE_ICON = "fa-circle-exclamation";
|
|
76
|
+
|
|
77
|
+
/** The class a package may style the HTML notice against. */
|
|
78
|
+
export const DRAFT_NOTICE_CLASS = "sohl-draft-notice";
|
|
79
|
+
|
|
80
|
+
/** The Typst function the book's notice calls. */
|
|
81
|
+
export const BOOK_DRAFT_NOTICE = "book-draft-notice";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The notice as HTML, for a JournalEntry page and for an Actor's prose field.
|
|
85
|
+
*
|
|
86
|
+
* A `blockquote` because that is the element Foundry already sets apart —
|
|
87
|
+
* `body.game .app blockquote` carries a left rule and an indent — so the
|
|
88
|
+
* treatment survives with no stylesheet of this package's own. The mark is a
|
|
89
|
+
* Font Awesome element, which Foundry bundles; `aria-hidden` keeps it out of a
|
|
90
|
+
* screen reader, where the label reads it.
|
|
91
|
+
*
|
|
92
|
+
* @returns {string} The notice's HTML.
|
|
93
|
+
*/
|
|
94
|
+
export function draftNoticeHtml() {
|
|
95
|
+
return (
|
|
96
|
+
`<blockquote class="${DRAFT_NOTICE_CLASS}">` +
|
|
97
|
+
`<p><i class="fa-solid ${DRAFT_NOTICE_ICON}" aria-hidden="true"></i> ` +
|
|
98
|
+
`<strong>${DRAFT_NOTICE_LABEL}</strong> ${DRAFT_NOTICE}</p>` +
|
|
99
|
+
`</blockquote>`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The notice a note has earned, or nothing.
|
|
105
|
+
*
|
|
106
|
+
* The one place the tag is asked about on a compiled document, so the packs and
|
|
107
|
+
* the book cannot answer it differently. It reads {@link
|
|
108
|
+
* module:engine/note-vocabulary.isDraftNote}, which is the single reader of the
|
|
109
|
+
* tag itself.
|
|
110
|
+
*
|
|
111
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
112
|
+
* @returns {string} The notice's HTML, or `""` for a note that is settled.
|
|
113
|
+
*/
|
|
114
|
+
export function draftNoticeFor(fm) {
|
|
115
|
+
return isDraftNote(fm) ? draftNoticeHtml() : "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Prose led by the notice, where the note is a draft.
|
|
120
|
+
*
|
|
121
|
+
* **Which field a system leads is the system's decision, and it makes it
|
|
122
|
+
* once.** A referee at the table opens an actor sheet rather than the
|
|
123
|
+
* documentation beside it, so the prose a sheet draws says it too — on one
|
|
124
|
+
* field, since a document showing the same statement twice teaches a reader to
|
|
125
|
+
* skip it. Both systems put it on the dossier: SoHL's `dossier` and HM3's
|
|
126
|
+
* `biography` read the same authored section, so the notice lands on the same
|
|
127
|
+
* prose whichever system compiles it.
|
|
128
|
+
*
|
|
129
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
130
|
+
* @param {string} html - The rendered prose.
|
|
131
|
+
* @returns {string} The prose, led by the notice where one is due.
|
|
132
|
+
*/
|
|
133
|
+
export function withDraftNotice(fm, html) {
|
|
134
|
+
const notice = draftNoticeFor(fm);
|
|
135
|
+
if (!notice) return html;
|
|
136
|
+
return html ? `${notice}\n${html}` : notice;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The `#let` the book's notice is drawn by.
|
|
141
|
+
*
|
|
142
|
+
* Emitted with the rest of the book's preamble. The mark is a circle with an
|
|
143
|
+
* exclamation set inside it: two primitives and the face already setting the
|
|
144
|
+
* page, so there is no font to resolve and nothing that can come out as a box.
|
|
145
|
+
*
|
|
146
|
+
* @returns {string} The Typst definition.
|
|
147
|
+
*/
|
|
148
|
+
export function bookDraftNoticePreamble() {
|
|
149
|
+
return (
|
|
150
|
+
`#let ${BOOK_DRAFT_NOTICE}(label, body) = block(\n` +
|
|
151
|
+
" width: 100%, above: 0.55em, below: 0.75em, breakable: false,\n" +
|
|
152
|
+
" inset: (left: 0.7em, top: 0.35em, bottom: 0.35em),\n" +
|
|
153
|
+
" stroke: (left: 1.6pt + book-faint))[\n" +
|
|
154
|
+
" #set par(justify: false, first-line-indent: 0em)\n" +
|
|
155
|
+
" #set text(size: 8.6pt, fill: book-faint)\n" +
|
|
156
|
+
" #box(baseline: 0.18em, circle(radius: 0.42em, stroke: 0.6pt + book-faint)[\n" +
|
|
157
|
+
' #align(center + horizon)[#text(size: 6.4pt, weight: "bold")[!]]\n' +
|
|
158
|
+
" ])\n" +
|
|
159
|
+
" #h(0.4em)\n" +
|
|
160
|
+
' #text(weight: "bold")[#label]\n' +
|
|
161
|
+
" #h(0.25em)\n" +
|
|
162
|
+
" #body\n" +
|
|
163
|
+
"]"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The notice as Typst, for a leaf of the book.
|
|
169
|
+
*
|
|
170
|
+
* @returns {string} The call, ready to sit between the entry's plate and its
|
|
171
|
+
* infobox panel.
|
|
172
|
+
*/
|
|
173
|
+
export function draftNoticeTypst() {
|
|
174
|
+
return `#${BOOK_DRAFT_NOTICE}[${DRAFT_NOTICE_LABEL}][${DRAFT_NOTICE}]`;
|
|
175
|
+
}
|
package/engine/index.mjs
CHANGED
|
@@ -87,6 +87,9 @@ export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
|
87
87
|
/** The three states of a note — stub, draft, full — and the empty body they rest on. */
|
|
88
88
|
export * as noteState from "./note-state.mjs";
|
|
89
89
|
|
|
90
|
+
/** What a draft note says about itself, in one sentence, on every surface. */
|
|
91
|
+
export * as draftNotice from "./draft-notice.mjs";
|
|
92
|
+
|
|
90
93
|
/** Whether every empty body is a stub on purpose, and how finished a tree is. */
|
|
91
94
|
export * as stubLint from "./stub-lint.mjs";
|
|
92
95
|
|
package/engine/item-docs.mjs
CHANGED
|
@@ -64,10 +64,10 @@ export { itemTypes };
|
|
|
64
64
|
* Every content type whose **prose compiles into a JournalEntry of its own**,
|
|
65
65
|
* addressed by the virtual `doc<type>` qualifier.
|
|
66
66
|
*
|
|
67
|
-
* Every item type, plus `macro` — a macro
|
|
68
|
-
* note also compiles into a Macro, which
|
|
69
|
-
* and its description: one note, two documents,
|
|
70
|
-
* journals pack.
|
|
67
|
+
* Every item type, every actor type, every map type, plus `macro` — a macro
|
|
68
|
+
* note's body documents the script the note also compiles into a Macro, which
|
|
69
|
+
* is the same shape as an item and its description: one note, two documents,
|
|
70
|
+
* the prose living in the journals pack.
|
|
71
71
|
*
|
|
72
72
|
* **One set, read by the compiler and the emitter alike.** The journals pass
|
|
73
73
|
* decides what to compile from it, and the link manifest decides what to
|
|
@@ -76,8 +76,9 @@ export { itemTypes };
|
|
|
76
76
|
* consumer can address. It is composed exactly once, in `defineConfig`, and
|
|
77
77
|
* read from there — never recomposed at a call site.
|
|
78
78
|
*
|
|
79
|
-
* `doc` notes
|
|
80
|
-
*
|
|
79
|
+
* `doc` notes are absent, for the reason that applies to them alone: a `doc`
|
|
80
|
+
* note's single document *is* its prose, so there is no second document for a
|
|
81
|
+
* `doc<type>` address to name.
|
|
81
82
|
*
|
|
82
83
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
83
84
|
* needs no configuration (#2).
|
|
@@ -93,8 +94,8 @@ export function docEntryTypes() {
|
|
|
93
94
|
* its own.
|
|
94
95
|
*
|
|
95
96
|
* @param {string} type - The note's `type` frontmatter.
|
|
96
|
-
* @returns {boolean} True for an item type,
|
|
97
|
-
* false for `doc
|
|
97
|
+
* @returns {boolean} True for an item type, an actor type, a map type and
|
|
98
|
+
* `macro`; false for `doc`.
|
|
98
99
|
*/
|
|
99
100
|
export function hasDocEntry(type) {
|
|
100
101
|
// Through {@link currentType}, because the set is derived from the item
|
package/engine/journals.mjs
CHANGED
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
* The content root (`contentBase`) is walked recursively; any `.md` file
|
|
19
19
|
* whose frontmatter declares either `type: doc` or a
|
|
20
20
|
* **doc-carrying type** ({@link sohl.utils.packs.docEntryTypes} — every item
|
|
21
|
-
* type, plus `macro`) is compiled into one
|
|
21
|
+
* type, every actor type, every map type, plus `macro`) is compiled into one
|
|
22
|
+
* JournalEntry document. Each note's
|
|
22
23
|
* body is split on top-level H1 headings; the optional content before the
|
|
23
24
|
* first H1 becomes a lead page, and each subsequent H1 starts a new page named
|
|
24
25
|
* after its heading text. All page bodies are rendered to HTML.
|
|
25
26
|
*
|
|
26
27
|
* A doc-carrying note compiles into that document's **documentation** — the
|
|
27
|
-
* same prose and pages, filed in the same folder as the document itself
|
|
28
|
-
* keeps only a pointer to it
|
|
28
|
+
* same prose and pages, filed in the same folder as the document itself. An
|
|
29
|
+
* item keeps only a pointer to it; an actor keeps its own inline prose as
|
|
30
|
+
* well, because the indirection an item pays for is bought by embedding one
|
|
31
|
+
* item across hundreds of beings and an actor is singular. See `item-docs.mjs` for why, and for the ids the
|
|
29
32
|
* two passes agree on. A macro note's `{#script}` page is compiled here like
|
|
30
33
|
* any other: the macro pass reads the same page independently, and withholds
|
|
31
34
|
* nothing from the journal.
|
|
@@ -56,6 +59,7 @@ import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
|
56
59
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
57
60
|
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
58
61
|
import { journalHasContent } from "./note-state.mjs";
|
|
62
|
+
import { draftNoticeFor } from "./draft-notice.mjs";
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
65
|
* Splits a markdown body into pages by top-level H1 headings. Fenced
|
|
@@ -287,6 +291,11 @@ export function buildPages(rawPages, entryId, noteName) {
|
|
|
287
291
|
* that reads. It is **not** a page of its own — a page is what a UUID
|
|
288
292
|
* addresses, and a summary a reader has to navigate to is a summary they do
|
|
289
293
|
* not see.
|
|
294
|
+
* @param {string} [params.notice] - A statement about the entry rather than
|
|
295
|
+
* about its subject, already rendered to HTML — see {@link
|
|
296
|
+
* module:engine/draft-notice}. It leads the first page, ahead of the infobox,
|
|
297
|
+
* because a reader deciding whether to rely on the entry has to be told
|
|
298
|
+
* before they read it rather than after.
|
|
290
299
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
291
300
|
*/
|
|
292
301
|
export function buildJournalEntry({
|
|
@@ -298,12 +307,16 @@ export function buildJournalEntry({
|
|
|
298
307
|
flags,
|
|
299
308
|
stats = defaultStats(),
|
|
300
309
|
infobox = "",
|
|
310
|
+
notice = "",
|
|
301
311
|
}) {
|
|
302
312
|
const rawPages = splitPages(markdown, leadName);
|
|
303
313
|
const pages = buildPages(rawPages, id, name);
|
|
304
314
|
if (infobox.trim() && pages.length) {
|
|
305
315
|
pages[0].text.content = `${infobox}\n${pages[0].text.content}`;
|
|
306
316
|
}
|
|
317
|
+
if (notice.trim() && pages.length) {
|
|
318
|
+
pages[0].text.content = `${notice}\n${pages[0].text.content}`;
|
|
319
|
+
}
|
|
307
320
|
return {
|
|
308
321
|
name,
|
|
309
322
|
pages,
|
|
@@ -407,7 +420,7 @@ export class Journals extends BasePackCompiler {
|
|
|
407
420
|
* Compile one note into a JournalEntry.
|
|
408
421
|
*
|
|
409
422
|
* A `doc` note becomes the entry its frontmatter describes. A
|
|
410
|
-
* **doc-carrying note** — every item
|
|
423
|
+
* **doc-carrying note** — every item, actor, map and macro note — becomes
|
|
411
424
|
* that document's documentation instead: the same prose, the same pages,
|
|
412
425
|
* in the same folder, under an id derived from the note's, so the pointer
|
|
413
426
|
* the items pass wrote resolves to it (see
|
|
@@ -458,6 +471,12 @@ export class Journals extends BasePackCompiler {
|
|
|
458
471
|
name,
|
|
459
472
|
markdown,
|
|
460
473
|
infobox: infoboxesToHtml(boxes, { link: linkToUuid }),
|
|
474
|
+
// Whether the entry is settled, asked of the tag through the one
|
|
475
|
+
// reader of it. This sits here rather than in `buildJournalEntry`
|
|
476
|
+
// for the reason `skipNote` runs first: a note with no prose
|
|
477
|
+
// compiles into nothing, and a notice composed before that test
|
|
478
|
+
// would be content enough to give it an entry.
|
|
479
|
+
notice: draftNoticeFor(fm),
|
|
461
480
|
// A doc-carrying note's lead page is the document itself, not an
|
|
462
481
|
// "Introduction" — see {@link splitPages}.
|
|
463
482
|
leadName: ownsDoc ? name : undefined,
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -112,6 +112,8 @@ import {
|
|
|
112
112
|
renderBook,
|
|
113
113
|
resolveDanglingLabels,
|
|
114
114
|
} from "./pdf-render.mjs";
|
|
115
|
+
import { draftNoticeTypst } from "./draft-notice.mjs";
|
|
116
|
+
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
115
117
|
import { infoboxTypstPreamble, infoboxesToTypst, linkToTypst } from "./infobox-render.mjs";
|
|
116
118
|
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
117
119
|
import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
@@ -540,7 +542,12 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
540
542
|
const panel = infoboxesToTypst(boxes, {
|
|
541
543
|
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
542
544
|
});
|
|
543
|
-
|
|
545
|
+
// Whether the entry is settled comes before anything it says: the
|
|
546
|
+
// notice leads the leaf, ahead of the panel, which is the position it
|
|
547
|
+
// holds on every other surface.
|
|
548
|
+
const notice = isDraftNote(page.fm) ? draftNoticeTypst() : "";
|
|
549
|
+
const parts = [notice, panel, prose].filter((part) => part);
|
|
550
|
+
return parts.join("\n\n");
|
|
544
551
|
};
|
|
545
552
|
|
|
546
553
|
const bodies = new Map();
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
import MarkdownIt from "markdown-it";
|
|
65
65
|
|
|
66
|
+
import { bookDraftNoticePreamble } from "./draft-notice.mjs";
|
|
66
67
|
import { iconPlugin, ICON_PATTERN } from "./content-icons.mjs";
|
|
67
68
|
import { IMAGE_CLASSES, IMAGE_FLOATS, imagePlugin } from "./content-images.mjs";
|
|
68
69
|
import { slugify } from "./content-slug.mjs";
|
|
@@ -882,6 +883,11 @@ export function bookTypstPreamble() {
|
|
|
882
883
|
" ]\n" +
|
|
883
884
|
" ]\n" +
|
|
884
885
|
"}",
|
|
886
|
+
// A statement about the entry rather than about its subject, set in
|
|
887
|
+
// the running face at the head of the leaf. The mark is drawn from two
|
|
888
|
+
// primitives rather than set from a face: a consumer declares its icon
|
|
889
|
+
// families or declares none, and a glyph the face lacks is silent here.
|
|
890
|
+
bookDraftNoticePreamble(),
|
|
885
891
|
"#let book-epigraph(body) = {\n" +
|
|
886
892
|
" v(0.42cm)\n" +
|
|
887
893
|
" align(center)[\n" +
|
package/hm3/actors.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import { resolveName } from "../engine/helpers.mjs";
|
|
|
53
53
|
import { buildFromFields, retiredTopLevelKey, STRING } from "../engine/field-spec.mjs";
|
|
54
54
|
import { SystemActorCompiler } from "../engine/actor-compiler.mjs";
|
|
55
55
|
import { renderSection } from "../engine/anchored-sections.mjs";
|
|
56
|
+
import { withDraftNotice } from "../engine/draft-notice.mjs";
|
|
56
57
|
import { documentSubtype } from "../engine/document-subtypes.mjs";
|
|
57
58
|
import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
58
59
|
import { templateFlags } from "./template-priority.mjs";
|
|
@@ -348,7 +349,10 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
348
349
|
// `system.portrait` is not: a being's portrait is the lead image of
|
|
349
350
|
// its `{#appearance}` section, which is the markup below.
|
|
350
351
|
description: renderSection(body || "", "appearance"),
|
|
351
|
-
biography:
|
|
352
|
+
// `biography` is this system's dossier: the two are paired by the
|
|
353
|
+
// authored section each reads, not by their names, so the notice
|
|
354
|
+
// lands on the same prose here as it does under SoHL.
|
|
355
|
+
biography: withDraftNotice(fm, renderSection(body || "", "dossier")),
|
|
352
356
|
...buildFromFields(ACTOR_FIELDS, reports)(fm),
|
|
353
357
|
// Declared on `character` alone, so written there alone — see the
|
|
354
358
|
// module note.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.7.0",
|
|
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",
|
package/sohl/actors.mjs
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
import { sohlField, resolveName, systemTemplatePriority, folderField } from "../engine/helpers.mjs";
|
|
50
50
|
import { openingMasteryLevel } from "./skill-base.mjs";
|
|
51
51
|
import { SystemActorCompiler, renderSection } from "../engine/actor-compiler.mjs";
|
|
52
|
+
import { withDraftNotice } from "../engine/draft-notice.mjs";
|
|
52
53
|
// Which Foundry Actor subtype a note's `type` compiles into. Looked up in the
|
|
53
54
|
// system's declared map, never inferred from the type itself.
|
|
54
55
|
import { documentSubtype } from "../engine/document-subtypes.mjs";
|
|
@@ -347,7 +348,11 @@ export class Actors extends SystemActorCompiler {
|
|
|
347
348
|
// key that used to declare one is not a key. With no authored
|
|
348
349
|
// source left, the field keeps the schema's own initial.
|
|
349
350
|
appearance: renderSection(body || "", "appearance"),
|
|
350
|
-
dossier
|
|
351
|
+
// One document says it once. `dossier` is the field that carries
|
|
352
|
+
// an unsettled entry's notice, and `appearance` carries none —
|
|
353
|
+
// the print sheet draws both blocks in turn, so a notice on each
|
|
354
|
+
// would be read twice on one page.
|
|
355
|
+
dossier: withDraftNotice(fm, renderSection(body || "", "dossier")),
|
|
351
356
|
};
|
|
352
357
|
|
|
353
358
|
// Fill `system.body` (+ the base-actor movement fields) from the being's
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The notice as HTML, for a JournalEntry page and for an Actor's prose field.
|
|
3
|
+
*
|
|
4
|
+
* A `blockquote` because that is the element Foundry already sets apart —
|
|
5
|
+
* `body.game .app blockquote` carries a left rule and an indent — so the
|
|
6
|
+
* treatment survives with no stylesheet of this package's own. The mark is a
|
|
7
|
+
* Font Awesome element, which Foundry bundles; `aria-hidden` keeps it out of a
|
|
8
|
+
* screen reader, where the label reads it.
|
|
9
|
+
*
|
|
10
|
+
* @returns {string} The notice's HTML.
|
|
11
|
+
*/
|
|
12
|
+
export function draftNoticeHtml(): string;
|
|
13
|
+
/**
|
|
14
|
+
* The notice a note has earned, or nothing.
|
|
15
|
+
*
|
|
16
|
+
* The one place the tag is asked about on a compiled document, so the packs and
|
|
17
|
+
* the book cannot answer it differently. It reads {@link
|
|
18
|
+
* module:engine/note-vocabulary.isDraftNote}, which is the single reader of the
|
|
19
|
+
* tag itself.
|
|
20
|
+
*
|
|
21
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
22
|
+
* @returns {string} The notice's HTML, or `""` for a note that is settled.
|
|
23
|
+
*/
|
|
24
|
+
export function draftNoticeFor(fm: object | null | undefined): string;
|
|
25
|
+
/**
|
|
26
|
+
* Prose led by the notice, where the note is a draft.
|
|
27
|
+
*
|
|
28
|
+
* **Which field a system leads is the system's decision, and it makes it
|
|
29
|
+
* once.** A referee at the table opens an actor sheet rather than the
|
|
30
|
+
* documentation beside it, so the prose a sheet draws says it too — on one
|
|
31
|
+
* field, since a document showing the same statement twice teaches a reader to
|
|
32
|
+
* skip it. Both systems put it on the dossier: SoHL's `dossier` and HM3's
|
|
33
|
+
* `biography` read the same authored section, so the notice lands on the same
|
|
34
|
+
* prose whichever system compiles it.
|
|
35
|
+
*
|
|
36
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
37
|
+
* @param {string} html - The rendered prose.
|
|
38
|
+
* @returns {string} The prose, led by the notice where one is due.
|
|
39
|
+
*/
|
|
40
|
+
export function withDraftNotice(fm: object | null | undefined, html: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* The `#let` the book's notice is drawn by.
|
|
43
|
+
*
|
|
44
|
+
* Emitted with the rest of the book's preamble. The mark is a circle with an
|
|
45
|
+
* exclamation set inside it: two primitives and the face already setting the
|
|
46
|
+
* page, so there is no font to resolve and nothing that can come out as a box.
|
|
47
|
+
*
|
|
48
|
+
* @returns {string} The Typst definition.
|
|
49
|
+
*/
|
|
50
|
+
export function bookDraftNoticePreamble(): string;
|
|
51
|
+
/**
|
|
52
|
+
* The notice as Typst, for a leaf of the book.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string} The call, ready to sit between the entry's plate and its
|
|
55
|
+
* infobox panel.
|
|
56
|
+
*/
|
|
57
|
+
export function draftNoticeTypst(): string;
|
|
58
|
+
/** The word that opens the notice, and the only part a reader reads as a label. */
|
|
59
|
+
export const DRAFT_NOTICE_LABEL: "Draft.";
|
|
60
|
+
/**
|
|
61
|
+
* The sentence, which states the fact and stops.
|
|
62
|
+
*
|
|
63
|
+
* It says the content may change rather than that the writing is rough,
|
|
64
|
+
* because that is the difference between a reader who treats the entry as
|
|
65
|
+
* provisional and one who builds on it. It gives no instruction: a notice that
|
|
66
|
+
* reaches one page in seven wears better as a statement than as a direction,
|
|
67
|
+
* and the fact carries the consequence for anyone who can act on it.
|
|
68
|
+
*
|
|
69
|
+
* @type {string}
|
|
70
|
+
*/
|
|
71
|
+
export const DRAFT_NOTICE: string;
|
|
72
|
+
/** The Font Awesome mark: a notice, not a hazard. */
|
|
73
|
+
export const DRAFT_NOTICE_ICON: "fa-circle-exclamation";
|
|
74
|
+
/** The class a package may style the HTML notice against. */
|
|
75
|
+
export const DRAFT_NOTICE_CLASS: "sohl-draft-notice";
|
|
76
|
+
/** The Typst function the book's notice calls. */
|
|
77
|
+
export const BOOK_DRAFT_NOTICE: "book-draft-notice";
|
package/types/engine/index.d.mts
CHANGED
|
@@ -16,6 +16,7 @@ export * as homepage from "./homepage.mjs";
|
|
|
16
16
|
export * as noteSchemas from "./note-schemas.mjs";
|
|
17
17
|
export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
18
18
|
export * as noteState from "./note-state.mjs";
|
|
19
|
+
export * as draftNotice from "./draft-notice.mjs";
|
|
19
20
|
export * as stubLint from "./stub-lint.mjs";
|
|
20
21
|
export * as placeRelations from "./place-relations.mjs";
|
|
21
22
|
export * as holdings from "./holdings.mjs";
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Every content type whose **prose compiles into a JournalEntry of its own**,
|
|
3
3
|
* addressed by the virtual `doc<type>` qualifier.
|
|
4
4
|
*
|
|
5
|
-
* Every item type, plus `macro` — a macro
|
|
6
|
-
* note also compiles into a Macro, which
|
|
7
|
-
* and its description: one note, two documents,
|
|
8
|
-
* journals pack.
|
|
5
|
+
* Every item type, every actor type, every map type, plus `macro` — a macro
|
|
6
|
+
* note's body documents the script the note also compiles into a Macro, which
|
|
7
|
+
* is the same shape as an item and its description: one note, two documents,
|
|
8
|
+
* the prose living in the journals pack.
|
|
9
9
|
*
|
|
10
10
|
* **One set, read by the compiler and the emitter alike.** The journals pass
|
|
11
11
|
* decides what to compile from it, and the link manifest decides what to
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* consumer can address. It is composed exactly once, in `defineConfig`, and
|
|
15
15
|
* read from there — never recomposed at a call site.
|
|
16
16
|
*
|
|
17
|
-
* `doc` notes
|
|
18
|
-
*
|
|
17
|
+
* `doc` notes are absent, for the reason that applies to them alone: a `doc`
|
|
18
|
+
* note's single document *is* its prose, so there is no second document for a
|
|
19
|
+
* `doc<type>` address to name.
|
|
19
20
|
*
|
|
20
21
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
21
22
|
* needs no configuration (#2).
|
|
@@ -28,8 +29,8 @@ export function docEntryTypes(): ReadonlySet<string>;
|
|
|
28
29
|
* its own.
|
|
29
30
|
*
|
|
30
31
|
* @param {string} type - The note's `type` frontmatter.
|
|
31
|
-
* @returns {boolean} True for an item type,
|
|
32
|
-
* false for `doc
|
|
32
|
+
* @returns {boolean} True for an item type, an actor type, a map type and
|
|
33
|
+
* `macro`; false for `doc`.
|
|
33
34
|
*/
|
|
34
35
|
export function hasDocEntry(type: string): boolean;
|
|
35
36
|
/**
|
|
@@ -143,9 +143,14 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
|
|
|
143
143
|
* that reads. It is **not** a page of its own — a page is what a UUID
|
|
144
144
|
* addresses, and a summary a reader has to navigate to is a summary they do
|
|
145
145
|
* not see.
|
|
146
|
+
* @param {string} [params.notice] - A statement about the entry rather than
|
|
147
|
+
* about its subject, already rendered to HTML — see {@link
|
|
148
|
+
* module:engine/draft-notice}. It leads the first page, ahead of the infobox,
|
|
149
|
+
* because a reader deciding whether to rely on the entry has to be told
|
|
150
|
+
* before they read it rather than after.
|
|
146
151
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
147
152
|
*/
|
|
148
|
-
export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infobox, }: {
|
|
153
|
+
export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infobox, notice, }: {
|
|
149
154
|
id: string;
|
|
150
155
|
name: string;
|
|
151
156
|
markdown: string;
|
|
@@ -154,6 +159,7 @@ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags,
|
|
|
154
159
|
flags?: object | undefined;
|
|
155
160
|
stats?: object | undefined;
|
|
156
161
|
infobox?: string | undefined;
|
|
162
|
+
notice?: string | undefined;
|
|
157
163
|
}): object;
|
|
158
164
|
/**
|
|
159
165
|
* Journals pack compiler.
|