@heroiclands/package-build 22.5.1 → 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 +21 -0
- package/CONTENT.md +24 -7
- package/docs/api.md +20 -0
- package/docs/content-format.md +129 -27
- package/engine/actor-compiler.mjs +12 -0
- package/engine/base-compiler.mjs +51 -36
- package/engine/draft-notice.mjs +175 -0
- package/engine/foundry-entries.mjs +25 -7
- package/engine/generate.mjs +81 -10
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +14 -13
- package/engine/item-compiler.mjs +12 -0
- package/engine/item-docs.mjs +9 -8
- package/engine/journals.mjs +23 -4
- package/engine/note-claims.mjs +98 -2
- package/engine/pdf-build.mjs +8 -1
- package/engine/pdf-render.mjs +6 -0
- package/engine/subtype-registry.mjs +42 -5
- package/hm3/actors.mjs +5 -1
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/base-compiler.d.mts +46 -16
- package/types/engine/draft-notice.d.mts +77 -0
- package/types/engine/generate.d.mts +29 -0
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +11 -12
- package/types/engine/item-compiler.d.mts +11 -0
- package/types/engine/item-docs.d.mts +9 -8
- package/types/engine/journals.d.mts +7 -1
- package/types/engine/note-claims.d.mts +6 -1
- package/types/engine/subtype-registry.d.mts +32 -5
|
@@ -65,6 +65,8 @@ import {
|
|
|
65
65
|
NEVER_PACKED_TYPES,
|
|
66
66
|
DERIVED_PACKED_TYPES,
|
|
67
67
|
} from "./note-claims.mjs";
|
|
68
|
+
import { DEFAULT_DOCUMENT_SUBTYPES, SYSTEM_DOCUMENT_CLASSES } from "./subtype-registry.mjs";
|
|
69
|
+
import { carriesSystemBlock } from "./system-block.mjs";
|
|
68
70
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
69
71
|
import { resolveNoteId } from "./note-ids.mjs";
|
|
70
72
|
import { compendiumUuid, currentType, packForType, pageUuid } from "./ids.mjs";
|
|
@@ -207,14 +209,30 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
207
209
|
return undefined;
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
type,
|
|
213
|
-
id,
|
|
212
|
+
const docType = routeFm ? packForType(type).docType : "JournalEntry";
|
|
213
|
+
const pack =
|
|
214
214
|
routeFm ?
|
|
215
|
-
packRouter.resolveOrNull(routeFm,
|
|
216
|
-
: packRouter.defaultOf("JournalEntry")
|
|
217
|
-
|
|
215
|
+
packRouter.resolveOrNull(routeFm, docType)
|
|
216
|
+
: packRouter.defaultOf("JournalEntry");
|
|
217
|
+
|
|
218
|
+
// **A system block is what makes a game document**, so a note carrying
|
|
219
|
+
// none compiles into no Actor and no Item — and an address naming one
|
|
220
|
+
// would send every consumer to a document that is not there. The same
|
|
221
|
+
// statement `pack: none` makes about a pack, made about a system: the
|
|
222
|
+
// note keeps its address, its page and its prose journal, and publishes
|
|
223
|
+
// no UUID for the document it does not produce.
|
|
224
|
+
//
|
|
225
|
+
// Which block is asked for is the pack's, because the pack decides the
|
|
226
|
+
// pass: one declaring a `system:` is compiled by that system's pass,
|
|
227
|
+
// and one declaring none by the fallback, which reads a single block of
|
|
228
|
+
// its own.
|
|
229
|
+
if (SYSTEM_DOCUMENT_CLASSES.has(docType)) {
|
|
230
|
+
const system =
|
|
231
|
+
(pack && packRouter.systemOf?.(pack)) || DEFAULT_DOCUMENT_SUBTYPES.system;
|
|
232
|
+
if (!carriesSystemBlock(routeFm, system)) return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return compendiumUuid(foundryPackageId, type, id, pack);
|
|
218
236
|
};
|
|
219
237
|
|
|
220
238
|
const carriesDoc =
|
package/engine/generate.mjs
CHANGED
|
@@ -102,7 +102,22 @@ const COMPILERS = {
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
105
|
+
* Every pass this toolchain ships that writes one system's data.
|
|
106
|
+
*
|
|
107
|
+
* The list a system joins, and the only place it is named. Each class already
|
|
108
|
+
* declares which system it is for (`documentSubtypes`) and which document it
|
|
109
|
+
* writes (`documentClass`), so the pairing is read off the passes rather than
|
|
110
|
+
* spelled a second time beside them — a table keyed by system id is a table
|
|
111
|
+
* free to disagree with the classes it names, and the way it disagrees is
|
|
112
|
+
* silent: a system absent from it took the fallback pass and compiled its packs
|
|
113
|
+
* out of a neighbour's blocks.
|
|
114
|
+
*
|
|
115
|
+
* @type {readonly Function[]}
|
|
116
|
+
*/
|
|
117
|
+
const SYSTEM_PASSES = Object.freeze([Items, Actors, Hm3Items, Hm3Actors]);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The system-specific compilers, derived from the passes themselves.
|
|
106
121
|
*
|
|
107
122
|
* A repository feeding two systems declares one Item pack and one Actor pack
|
|
108
123
|
* per system — `harn-ensemble` has `actors-hm3` and `actors-sohl` — and each
|
|
@@ -111,28 +126,84 @@ const COMPILERS = {
|
|
|
111
126
|
* `itemBuilders` lookup already read, so nothing new is declared to make the
|
|
112
127
|
* compiler follow it.
|
|
113
128
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
129
|
+
* @param {readonly Function[]} passes - The system passes to table.
|
|
130
|
+
* @returns {Readonly<Record<string, Readonly<Record<string, Function>>>>}
|
|
131
|
+
* System id → document class → pass.
|
|
132
|
+
* @throws {Error} When a pass declares no system or no document class, which is
|
|
133
|
+
* a pass that cannot be addressed by either question this table answers.
|
|
134
|
+
*/
|
|
135
|
+
export function systemCompilers(passes) {
|
|
136
|
+
/** @type {Record<string, Record<string, Function>>} */
|
|
137
|
+
const table = {};
|
|
138
|
+
for (const pass of passes) {
|
|
139
|
+
const declared =
|
|
140
|
+
/** @type {{documentSubtypes?: {system?: string},
|
|
141
|
+
* documentClass?: string, name: string}} */ (pass);
|
|
142
|
+
const system = declared.documentSubtypes?.system;
|
|
143
|
+
const docType = declared.documentClass;
|
|
144
|
+
if (!system || !docType) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`${declared.name} is registered as a system pass but declares ` +
|
|
147
|
+
`${system ? "no `documentClass`" : "no `documentSubtypes`"} — a pass ` +
|
|
148
|
+
`writing a system's data states both, so which system and which ` +
|
|
149
|
+
`document it answers for is read from the class itself.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
(table[system] ??= {})[docType] = pass;
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze(
|
|
155
|
+
Object.fromEntries(
|
|
156
|
+
Object.entries(table).map(([system, byType]) => [system, Object.freeze(byType)]),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @type {Readonly<Record<string, Readonly<Record<string, Function>>>>} */
|
|
162
|
+
const SYSTEM_COMPILERS = systemCompilers(SYSTEM_PASSES);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The document classes some system writes its own data into.
|
|
166
|
+
*
|
|
167
|
+
* Derived from {@link SYSTEM_COMPILERS}, so it is the set of classes for which
|
|
168
|
+
* "which system?" is a question with consequences. Every other class has one
|
|
169
|
+
* implementation whatever a pack declares.
|
|
118
170
|
*
|
|
119
|
-
* @type {
|
|
171
|
+
* @type {ReadonlySet<string>}
|
|
120
172
|
*/
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
173
|
+
const SYSTEM_DOCUMENT_CLASSES = Object.freeze(
|
|
174
|
+
new Set(Object.values(SYSTEM_COMPILERS).flatMap((byType) => Object.keys(byType))),
|
|
175
|
+
);
|
|
125
176
|
|
|
126
177
|
/**
|
|
127
178
|
* The compiler class a pack of one document type and one system gets.
|
|
128
179
|
*
|
|
180
|
+
* A pack declaring **no** system takes the fallback pass for its document
|
|
181
|
+
* class, which reads
|
|
182
|
+
* {@link module:engine/subtype-registry.DEFAULT_DOCUMENT_SUBTYPES} — the
|
|
183
|
+
* single-system convenience every tree shipping for one system is built on.
|
|
184
|
+
*
|
|
185
|
+
* A pack declaring a system this toolchain has no pass for is **refused by
|
|
186
|
+
* name**. Falling through to the fallback would compile that pack out of
|
|
187
|
+
* another system's blocks and ship it stamped with the system it asked for,
|
|
188
|
+
* which is the quiet failure the refusal exists to replace.
|
|
189
|
+
*
|
|
129
190
|
* @param {string} docType - The Foundry document type the pack holds.
|
|
130
191
|
* @param {string|null} [system] - The system the pack declares, if any.
|
|
131
192
|
* @returns {Function|undefined} The compiler class, or `undefined` for a
|
|
132
193
|
* document type nothing here compiles — which {@link generatePack} reports
|
|
133
194
|
* rather than defaulting past.
|
|
195
|
+
* @throws {Error} When a system-bearing document class is asked for a system
|
|
196
|
+
* this toolchain ships no pass for.
|
|
134
197
|
*/
|
|
135
198
|
export function compilerFor(docType, system = null) {
|
|
199
|
+
if (system && SYSTEM_DOCUMENT_CLASSES.has(docType) && !SYSTEM_COMPILERS[system]?.[docType]) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`no ${docType} compiler for system "${system}" — this toolchain ships ` +
|
|
202
|
+
`passes for ${Object.keys(SYSTEM_COMPILERS).sort().join(", ")}. Declare a ` +
|
|
203
|
+
`system one of those names, or add the pass that reads a ` +
|
|
204
|
+
`\`${system}:\` block.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
136
207
|
return (system && SYSTEM_COMPILERS[system]?.[docType]) || COMPILERS[docType];
|
|
137
208
|
}
|
|
138
209
|
|
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
|
|
|
@@ -58,24 +58,23 @@ export function infoboxFor(system) {
|
|
|
58
58
|
* Whether one system compiles a document for one note.
|
|
59
59
|
*
|
|
60
60
|
* This is what a system box's _available_ asserts, and it is the compile's own
|
|
61
|
-
* question rather than a reading of the frontmatter.
|
|
62
|
-
* is not a note a system has nothing for: where the pack compiling its document
|
|
63
|
-
* declares no `system:`, the document is built from `data:` and the field
|
|
64
|
-
* defaults and ships exactly like any other.
|
|
61
|
+
* question rather than a reading of the frontmatter.
|
|
65
62
|
*
|
|
66
63
|
* Three statements answer it, and they are the three the compile itself
|
|
67
64
|
* follows:
|
|
68
65
|
*
|
|
69
|
-
* 1. **The
|
|
66
|
+
* 1. **The note's block.** A system block is what makes a game document, so a
|
|
67
|
+
* note carrying nothing for this system compiles into no document of it
|
|
68
|
+
* anywhere — the rule
|
|
69
|
+
* {@link module:engine/base-compiler.BasePackCompiler#eligibleFor} applies,
|
|
70
|
+
* asked here from outside.
|
|
71
|
+
* 2. **The map** says which document class this system makes of the note's
|
|
70
72
|
* type. No row, no document — and no box either, which is why a caller
|
|
71
73
|
* reaching here already has one.
|
|
72
|
-
*
|
|
73
|
-
* list this build is driven by
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* {@link module:engine/base-compiler.BasePackCompiler#eligibleFor} applies,
|
|
77
|
-
* asked here from outside. Declaring none, it is compiled by the fallback
|
|
78
|
-
* pass, which needs no block and answers for
|
|
74
|
+
* 3. **The router** says which pack that document goes to, read from the pack
|
|
75
|
+
* list this build is driven by, and **that pack's `system:`** decides whose
|
|
76
|
+
* it is. Declaring one, it writes that system's data. Declaring none, it is
|
|
77
|
+
* compiled by the fallback pass, which reads
|
|
79
78
|
* {@link module:engine/subtype-registry.DEFAULT_DOCUMENT_SUBTYPES} alone —
|
|
80
79
|
* so a tree with no HM3 pack ships no HM3 document however a note is
|
|
81
80
|
* written.
|
|
@@ -86,6 +85,8 @@ export function infoboxFor(system) {
|
|
|
86
85
|
* @returns {boolean} True when this system compiles a document for this note.
|
|
87
86
|
*/
|
|
88
87
|
export function compilesSystemDocument(fm, map, router) {
|
|
88
|
+
if (!carriesSystemBlock(fm, map.block)) return false;
|
|
89
|
+
|
|
89
90
|
const row = subtypeRow(map, fm?.type);
|
|
90
91
|
if (!row?.document) return false;
|
|
91
92
|
|
|
@@ -94,7 +95,7 @@ export function compilesSystemDocument(fm, map, router) {
|
|
|
94
95
|
|
|
95
96
|
const packSystem = router.systemOf(packName);
|
|
96
97
|
if (!packSystem) return map.system === DEFAULT_DOCUMENT_SUBTYPES.system;
|
|
97
|
-
return packSystem === map.system
|
|
98
|
+
return packSystem === map.system;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -116,6 +116,18 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
116
116
|
*/
|
|
117
117
|
static requiresSystemBlock = true;
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The Foundry document class this pass writes.
|
|
121
|
+
*
|
|
122
|
+
* Declared by the class that does the writing, so the table pairing a
|
|
123
|
+
* system with its passes is derived from the passes themselves rather than
|
|
124
|
+
* written out a second time — see
|
|
125
|
+
* {@link module:engine/generate.systemCompilers}.
|
|
126
|
+
*
|
|
127
|
+
* @type {string}
|
|
128
|
+
*/
|
|
129
|
+
static documentClass = "Item";
|
|
130
|
+
|
|
119
131
|
/**
|
|
120
132
|
* An Item carries one piece of art — its icon — which this pass writes from
|
|
121
133
|
* the address the note names, falling back to the type's default art.
|
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/note-claims.mjs
CHANGED
|
@@ -94,7 +94,13 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
94
94
|
import { declaresNoPack } from "./pack-router.mjs";
|
|
95
95
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
96
96
|
import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
|
|
97
|
-
import {
|
|
97
|
+
import {
|
|
98
|
+
DEFAULT_DOCUMENT_SUBTYPES,
|
|
99
|
+
KNOWN_DOCUMENT_SUBTYPE_MAPS,
|
|
100
|
+
SHIPPED_SYSTEMS,
|
|
101
|
+
subtypeMapFor,
|
|
102
|
+
} from "./subtype-registry.mjs";
|
|
103
|
+
import { carriesSystemBlock } from "./system-block.mjs";
|
|
98
104
|
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
99
105
|
import { FOLDER_TYPE } from "./folder-notes.mjs";
|
|
100
106
|
import { BUNDLE_TYPE } from "./bundle-notes.mjs";
|
|
@@ -615,7 +621,92 @@ function authoringMessage(type) {
|
|
|
615
621
|
}
|
|
616
622
|
|
|
617
623
|
/**
|
|
618
|
-
*
|
|
624
|
+
* Which pass would read one system's block, given a configuration's packs.
|
|
625
|
+
*
|
|
626
|
+
* A pack declaring `system: hm3` is compiled by the HM3 pass and reads `hm3:`.
|
|
627
|
+
* A pack declaring **no** system is compiled by the fallback pass, which reads
|
|
628
|
+
* {@link module:engine/subtype-registry.DEFAULT_DOCUMENT_SUBTYPES} and nothing
|
|
629
|
+
* else — so a systemless pack answers for one system, not for every system a
|
|
630
|
+
* note might carry. That asymmetry is the whole of this question: the router
|
|
631
|
+
* will happily route an `hm3:` document into a systemless pack, and the pass
|
|
632
|
+
* waiting there reads a different block.
|
|
633
|
+
*
|
|
634
|
+
* A **prebuilt** pack compiles nothing — its JSON is checked in — so it reads
|
|
635
|
+
* no block either.
|
|
636
|
+
*
|
|
637
|
+
* @param {object} config - The resolved build configuration.
|
|
638
|
+
* @param {string} docType - The document class the block would compile into.
|
|
639
|
+
* @param {string} system - The system whose block is in question.
|
|
640
|
+
* @returns {{packs: number, serving: number}} How many non-prebuilt packs of
|
|
641
|
+
* that class the configuration has, and how many of them compile that
|
|
642
|
+
* system's documents.
|
|
643
|
+
*/
|
|
644
|
+
function packsServing(config, docType, system) {
|
|
645
|
+
const packs = (config?.packs ?? []).filter((pack) => !pack?.prebuilt && pack?.type === docType);
|
|
646
|
+
const serving = packs.filter((pack) =>
|
|
647
|
+
pack.system ? pack.system === system : system === DEFAULT_DOCUMENT_SUBTYPES.system,
|
|
648
|
+
);
|
|
649
|
+
return { packs: packs.length, serving: serving.length };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Every system block on one note that no pack in this package will read.
|
|
654
|
+
*
|
|
655
|
+
* **A discarded field is the quiet-failure class the diagnostics exist to
|
|
656
|
+
* close**, and a system block is the largest field a note can write: a whole
|
|
657
|
+
* document's worth of authored data, compiled by nobody and reported by
|
|
658
|
+
* nobody. The claim check beside this one asks whether a note's *type* is
|
|
659
|
+
* claimed; this asks whether its *systems* are.
|
|
660
|
+
*
|
|
661
|
+
* `error`, because the data is lost either way and the author cannot see it
|
|
662
|
+
* happen. It says nothing about a note carrying **no** block — that note is
|
|
663
|
+
* making a statement, and the statement is that nobody rolls dice against this
|
|
664
|
+
* person.
|
|
665
|
+
*
|
|
666
|
+
* Silent, too, where the class has no pack at all: the note's whole document is
|
|
667
|
+
* missing, which {@link partialMessage} already reports with the remedy.
|
|
668
|
+
*
|
|
669
|
+
* @param {object} fm - The note's frontmatter.
|
|
670
|
+
* @param {string} absPath - The note's path, for the finding's position.
|
|
671
|
+
* @param {string} type - The note's current type.
|
|
672
|
+
* @param {object} config - The resolved build configuration.
|
|
673
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
674
|
+
* severity: "error", message: string, type: string}>} One finding per
|
|
675
|
+
* discarded block.
|
|
676
|
+
*/
|
|
677
|
+
function discardedSystemBlockFindings(fm, absPath, type, config) {
|
|
678
|
+
const findings = [];
|
|
679
|
+
for (const system of SHIPPED_SYSTEMS) {
|
|
680
|
+
if (!carriesSystemBlock(fm, system)) continue;
|
|
681
|
+
const docType = subtypeRow(subtypeMapFor(system), type)?.document;
|
|
682
|
+
// This system makes no document of this note type, so its block says
|
|
683
|
+
// nothing that could be discarded — it is read for the note's fields
|
|
684
|
+
// wherever another pass wants them.
|
|
685
|
+
if (!docType) continue;
|
|
686
|
+
const { packs, serving } = packsServing(config, docType, system);
|
|
687
|
+
if (!packs || serving) continue;
|
|
688
|
+
findings.push({
|
|
689
|
+
file: absPath,
|
|
690
|
+
...locateFrontmatterKey(absPath, system, undefined, { topLevel: true }),
|
|
691
|
+
severity: /** @type {"error"} */ ("error"),
|
|
692
|
+
type,
|
|
693
|
+
message:
|
|
694
|
+
`carries a \`${system}:\` block, and no ${docType} pack in this ` +
|
|
695
|
+
`package compiles ${system} documents, so the block is discarded — ` +
|
|
696
|
+
`declare \`system: ${system}\` on one of the \`packs:\`, or remove ` +
|
|
697
|
+
`the block.`,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return findings;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Every note in the content tree that no configured pack would compile, and
|
|
705
|
+
* every system block no configured pack would read.
|
|
706
|
+
*
|
|
707
|
+
* Two questions of one walk, because they are the same question asked of a
|
|
708
|
+
* note's type and of a note's systems, and a note can fail either while passing
|
|
709
|
+
* the other — see {@link discardedSystemBlockFindings}.
|
|
619
710
|
*
|
|
620
711
|
* Read-only: it walks the tree and reports, and writes nothing. Three kinds of
|
|
621
712
|
* note are passed over, each for a stated reason rather than by omission — a
|
|
@@ -672,6 +763,11 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
672
763
|
// lint, which can say what to write instead.
|
|
673
764
|
const current = currentType(type);
|
|
674
765
|
|
|
766
|
+
// Asked first, and asked whatever the type's claim turns out to be: a
|
|
767
|
+
// note whose documents all have packs may still carry a block none of
|
|
768
|
+
// them reads, and that is invisible to every question below.
|
|
769
|
+
findings.push(...discardedSystemBlockFindings(fm, absPath, current, config));
|
|
770
|
+
|
|
675
771
|
// Every document this note produces, against the classes this
|
|
676
772
|
// configuration has a pack for. Three outcomes, and the middle one is
|
|
677
773
|
// the half-claimed one — invisible while the question is only "is anything
|
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" +
|
|
@@ -68,12 +68,15 @@ export const KNOWN_DOCUMENT_SUBTYPE_MAPS = Object.freeze([
|
|
|
68
68
|
/**
|
|
69
69
|
* The map a pack declaring no `system:` compiles its documents against.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* **The single-system fallback, and not a default any system inherits.** A pack
|
|
72
|
+
* names the system its documents are shaped for, and one declaring none is
|
|
73
|
+
* compiled by the fallback pass for its document type — a single-system
|
|
73
74
|
* package's ordinary arrangement, and the one `sohl-thalorna` ships. That pass
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* reads this map's block and no other, so a note carrying nothing for this
|
|
76
|
+
* system compiles into no document there; a note carrying another system's
|
|
77
|
+
* block reaches that system's pass or nothing at all, never this one. This is
|
|
78
|
+
* the answer to "a document compiled here belongs to *which* system?" wherever
|
|
79
|
+
* a pack has not said, and it answers nothing about a system that has said.
|
|
77
80
|
*
|
|
78
81
|
* Stated here rather than read off the compiler classes because those are not
|
|
79
82
|
* reachable from a leaf: `engine/item-compiler.mjs` imports the journals pass,
|
|
@@ -86,6 +89,40 @@ export const KNOWN_DOCUMENT_SUBTYPE_MAPS = Object.freeze([
|
|
|
86
89
|
*/
|
|
87
90
|
export const DEFAULT_DOCUMENT_SUBTYPES = SOHL_DOCUMENT_SUBTYPES;
|
|
88
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Every system this toolchain ships a map for, by id.
|
|
94
|
+
*
|
|
95
|
+
* Derived from {@link KNOWN_DOCUMENT_SUBTYPE_MAPS}, so registering a map is the
|
|
96
|
+
* whole of adding a system to every reader that asks "which systems are there?"
|
|
97
|
+
* — the per-system findings among them.
|
|
98
|
+
*
|
|
99
|
+
* @type {readonly string[]}
|
|
100
|
+
*/
|
|
101
|
+
export const SHIPPED_SYSTEMS = Object.freeze(KNOWN_DOCUMENT_SUBTYPE_MAPS.map((map) => map.system));
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The Foundry document classes a system compiles its own data into.
|
|
105
|
+
*
|
|
106
|
+
* Derived from the shipped maps' rows, so it is the set of classes for which
|
|
107
|
+
* "which system?" is a question with consequences: a pack of one of these
|
|
108
|
+
* classes is compiled by one system's pass reading one system's block, and a
|
|
109
|
+
* note carrying no such block produces nothing there. Every other class — a
|
|
110
|
+
* JournalEntry, a Macro, a Scene, an Adventure — has one implementation
|
|
111
|
+
* whatever a pack declares, because it is Foundry's document rather than any
|
|
112
|
+
* system's.
|
|
113
|
+
*
|
|
114
|
+
* @type {ReadonlySet<string>}
|
|
115
|
+
*/
|
|
116
|
+
export const SYSTEM_DOCUMENT_CLASSES = Object.freeze(
|
|
117
|
+
new Set(
|
|
118
|
+
KNOWN_DOCUMENT_SUBTYPE_MAPS.flatMap((map) =>
|
|
119
|
+
Object.values(map.types)
|
|
120
|
+
.map((row) => row?.document)
|
|
121
|
+
.filter(Boolean),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
|
|
89
126
|
/**
|
|
90
127
|
* Every note type any shipped map compiles into an **Actor**.
|
|
91
128
|
*
|
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
|