@heroiclands/package-build 22.21.0 → 22.23.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 +223 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +77 -17
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +153 -6
- package/docs/api.md +120 -89
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +41 -3
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +325 -35
- package/docs/commands.md +12 -10
- package/docs/configuration.md +117 -14
- package/docs/content-format.md +4 -2
- package/docs/guides/book.md +5 -4
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +545 -104
- package/docs/reference/note-types.md +30 -13
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +26 -30
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +139 -22
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +13 -2
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +197 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +38 -12
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +120 -65
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +321 -0
- package/engine/helpers.mjs +109 -41
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +129 -15
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +294 -50
- package/engine/lore-events.mjs +197 -0
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +434 -59
- package/engine/note-dates.mjs +354 -12
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +183 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +431 -74
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +563 -66
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +26 -7
- package/engine/site-build.mjs +166 -23
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +90 -3
- package/engine/site-preview.mjs +20 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +56 -30
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +8 -5
- package/manifest.mjs +43 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +5 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/content-config.d.mts +48 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +19 -17
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +80 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +179 -0
- package/types/engine/helpers.d.mts +29 -10
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +41 -12
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +46 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +84 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +27 -4
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +67 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +37 -14
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +21 -3
- package/types/manifest.d.mts +19 -23
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-captions.mjs +0 -181
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-captions.d.mts +0 -45
- package/types/engine/content-secrets.d.mts +0 -17
package/engine/journals.mjs
CHANGED
|
@@ -51,16 +51,20 @@
|
|
|
51
51
|
|
|
52
52
|
import log from "loglevel";
|
|
53
53
|
|
|
54
|
+
import path from "node:path";
|
|
55
|
+
|
|
54
56
|
import {
|
|
55
57
|
sohlField,
|
|
56
58
|
makeId,
|
|
57
59
|
resolveName,
|
|
60
|
+
resolveImg,
|
|
58
61
|
defaultStats,
|
|
59
62
|
renderFoundryMarkdown,
|
|
60
63
|
md,
|
|
61
64
|
folderField,
|
|
62
65
|
} from "./helpers.mjs";
|
|
63
66
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
67
|
+
import { scanBlocks } from "./content-blocks.mjs";
|
|
64
68
|
import { anchorPageId, resolveReference } from "./wikilinks.mjs";
|
|
65
69
|
import { infoboxesToHtml, linkToUuid } from "./infobox-render.mjs";
|
|
66
70
|
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
@@ -69,16 +73,50 @@ import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
|
69
73
|
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
70
74
|
import { journalHasContent } from "./note-state.mjs";
|
|
71
75
|
import { draftNoticeFor } from "./draft-notice.mjs";
|
|
72
|
-
import {
|
|
76
|
+
import { scanFigures } from "./content-figures.mjs";
|
|
73
77
|
import { separateFootnotes } from "./content-footnotes.mjs";
|
|
78
|
+
import { WITHHELD_CLASS, pageOpenings } from "./heading-attributes.mjs";
|
|
79
|
+
import { imagesIn } from "./content-images.mjs";
|
|
80
|
+
import { IMAGE_EXTENSIONS } from "./asset-types.mjs";
|
|
81
|
+
import { pathnameRoles } from "./art-fields.mjs";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE` — the default ownership a withheld
|
|
85
|
+
* page states. A GM reads every document whatever it says; everyone else reads
|
|
86
|
+
* this one not at all.
|
|
87
|
+
*/
|
|
88
|
+
const WITHHELD_OWNERSHIP = 0;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Every line a named block's own markup occupies, from its `:::name` opener
|
|
92
|
+
* to its closing `:::` inclusive.
|
|
93
|
+
*
|
|
94
|
+
* Read from {@link module:engine/content-blocks.scanBlocks}'s own ranges
|
|
95
|
+
* rather than restated here, so a figure cannot disagree with the blocks pass
|
|
96
|
+
* about what counts as a named block or where one ends. `scanBlocks` already
|
|
97
|
+
* recognises every name in its registry, carrying an attribute block or not,
|
|
98
|
+
* and already tracks a nested construct's own closer by depth rather than by
|
|
99
|
+
* a flag a bare `:::` clears regardless of whose closer it is.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} markdown - A note's body, as {@link splitPages} receives it.
|
|
102
|
+
* @returns {Set<number>} 0-based line indices inside a named block.
|
|
103
|
+
*/
|
|
104
|
+
function namedBlockLines(markdown) {
|
|
105
|
+
const lines = new Set();
|
|
106
|
+
for (const { start, end } of scanBlocks(markdown).blocks) {
|
|
107
|
+
for (let i = start; i <= end; i++) lines.add(i);
|
|
108
|
+
}
|
|
109
|
+
return lines;
|
|
110
|
+
}
|
|
74
111
|
|
|
75
112
|
/**
|
|
76
113
|
* Splits a markdown body into pages by top-level H1 headings. Fenced
|
|
77
|
-
* code blocks are respected so `# foo` inside ``` blocks doesn't
|
|
78
|
-
* a split. Content before the first H1 (if non-empty) becomes a
|
|
79
|
-
* page. Each H1 yields a page whose name is the heading
|
|
114
|
+
* code blocks are respected so `# foo` inside ``` or ~~~ blocks doesn't
|
|
115
|
+
* trigger a split. Content before the first H1 (if non-empty) becomes a
|
|
116
|
+
* leading page. Each H1 yields a page whose name is the heading
|
|
80
117
|
* text (with any `{#anchor-id}` suffix stripped out and surfaced as
|
|
81
|
-
* `anchorSlug`).
|
|
118
|
+
* `anchorSlug`). The classes the heading declares come along as `classes`, so
|
|
119
|
+
* a surface that honours one has it without reading the suffix again.
|
|
82
120
|
*
|
|
83
121
|
* `leadName` names that leading page. A journal note's is "Introduction",
|
|
84
122
|
* because it introduces the pages that follow. An item doc's is the item — a
|
|
@@ -86,26 +124,32 @@ import { separateFootnotes } from "./content-footnotes.mjs";
|
|
|
86
124
|
* calling that page "Introduction" would label the description as a preamble to
|
|
87
125
|
* nothing.
|
|
88
126
|
*
|
|
127
|
+
* A figure fence carries its own record along onto the page it starts, as
|
|
128
|
+
* `figure` — `null` for a page a heading started. {@link buildPages} reads it
|
|
129
|
+
* to build the `image` or `text` page a fence begins; nothing else sets it.
|
|
130
|
+
*
|
|
89
131
|
* @param {string} body - Markdown body to split.
|
|
90
132
|
* @param {string} [leadName] - Name of the page before the first heading.
|
|
133
|
+
* @param {(address: string) => string|undefined} [resolveRole] - From a
|
|
134
|
+
* picture's address to the role its asset declares, so a figure page's
|
|
135
|
+
* name reads `Map 1` rather than `Figure 1` where it is due — see
|
|
136
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
91
137
|
* @returns {Array<{name: string, anchorSlug: string|null, level: number,
|
|
92
|
-
* markdown: string}>} Pages in
|
|
138
|
+
* classes: string[], figure: object|null, markdown: string}>} Pages in
|
|
139
|
+
* document order.
|
|
93
140
|
*/
|
|
94
|
-
export function splitPages(body, leadName = "Introduction") {
|
|
141
|
+
export function splitPages(body, leadName = "Introduction", resolveRole) {
|
|
95
142
|
const { markdown, definitions } = separateFootnotes(body);
|
|
96
143
|
const lines = markdown.split("\n");
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const captionedHeadings = new Set(
|
|
100
|
-
captions
|
|
101
|
-
.filter((caption) => /^\s*#{1,6}\s/.test(lines[caption.blockStart] ?? ""))
|
|
102
|
-
.map((caption) => caption.blockStart),
|
|
144
|
+
const figureStarts = new Map(
|
|
145
|
+
scanFigures(markdown, { resolveRole }).figures.map((figure) => [figure.line - 1, figure]),
|
|
103
146
|
);
|
|
147
|
+
const blockLines = namedBlockLines(markdown);
|
|
148
|
+
const openings = pageOpenings(markdown, blockLines);
|
|
104
149
|
const pages = [];
|
|
105
150
|
const beforeFirstH1 = [];
|
|
106
151
|
let current = null;
|
|
107
|
-
let
|
|
108
|
-
let inSecret = false;
|
|
152
|
+
let codeFence = null;
|
|
109
153
|
|
|
110
154
|
const closeCurrent = () => {
|
|
111
155
|
if (!current) return;
|
|
@@ -113,43 +157,61 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
113
157
|
name: current.name,
|
|
114
158
|
anchorSlug: current.anchorSlug,
|
|
115
159
|
level: current.level,
|
|
160
|
+
classes: current.classes,
|
|
161
|
+
figure: current.figure ?? null,
|
|
116
162
|
markdown: current.lines.join("\n").trim(),
|
|
117
163
|
});
|
|
118
164
|
current = null;
|
|
119
165
|
};
|
|
120
166
|
|
|
121
167
|
for (const [lineIndex, line] of lines.entries()) {
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
// A fence's closer must carry the same character as its opener and
|
|
169
|
+
// be at least as long — a longer fence is what lets a fenced example
|
|
170
|
+
// carry a shorter fence of its own as literal content, same as
|
|
171
|
+
// {@link module:engine/content-blocks.scanBlocks} and
|
|
172
|
+
// {@link module:engine/content-figures.scanFigures} track it.
|
|
173
|
+
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
174
|
+
if (codeFence) {
|
|
175
|
+
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length) {
|
|
176
|
+
codeFence = null;
|
|
177
|
+
}
|
|
178
|
+
} else if (fence) {
|
|
179
|
+
codeFence = fence[1];
|
|
124
180
|
}
|
|
125
|
-
|
|
126
|
-
else if (!inCodeBlock && /^:::[ \t]*$/.test(line)) inSecret = false;
|
|
181
|
+
const inCodeBlock = codeFence !== null;
|
|
127
182
|
|
|
128
183
|
// An H1 starts a page, as does any heading carrying an `{#slug}`
|
|
129
184
|
// anchor: a Foundry UUID can only address a page, so a linkable
|
|
130
185
|
// section has to be one.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
186
|
+
// {@link module:engine/heading-attributes.parseHeadingLine} is the one
|
|
187
|
+
// reading of that rule — `scanBlocks` and `scanFigures` refuse such a
|
|
188
|
+
// heading written where it cannot become a page, from the same
|
|
189
|
+
// function, so none of them can disagree about what starts one. The
|
|
190
|
+
// lines it claims here are collected by `pageOpenings`, which also
|
|
191
|
+
// skips a fence and a named block. A figure fence opens a page of its
|
|
192
|
+
// own, named for the figure's label.
|
|
193
|
+
const opening = openings.get(lineIndex);
|
|
194
|
+
const figure =
|
|
195
|
+
!inCodeBlock && !blockLines.has(lineIndex) ? figureStarts.get(lineIndex) : null;
|
|
196
|
+
if (figure) {
|
|
138
197
|
closeCurrent();
|
|
139
198
|
current = {
|
|
140
|
-
name:
|
|
141
|
-
anchorSlug:
|
|
199
|
+
name: figure.label,
|
|
200
|
+
anchorSlug: figure.id || null,
|
|
142
201
|
level: 1,
|
|
202
|
+
classes: [],
|
|
203
|
+
figure,
|
|
143
204
|
lines: [line],
|
|
144
205
|
};
|
|
145
206
|
continue;
|
|
146
207
|
}
|
|
147
|
-
if (
|
|
208
|
+
if (opening) {
|
|
148
209
|
closeCurrent();
|
|
149
210
|
current = {
|
|
150
|
-
name:
|
|
151
|
-
anchorSlug:
|
|
152
|
-
level:
|
|
211
|
+
name: opening.text,
|
|
212
|
+
anchorSlug: opening.id || null,
|
|
213
|
+
level: opening.level,
|
|
214
|
+
classes: opening.classes,
|
|
153
215
|
lines: [],
|
|
154
216
|
};
|
|
155
217
|
continue;
|
|
@@ -169,6 +231,8 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
169
231
|
name: leadName,
|
|
170
232
|
anchorSlug: null,
|
|
171
233
|
level: 1,
|
|
234
|
+
classes: [],
|
|
235
|
+
figure: null,
|
|
172
236
|
markdown: intro,
|
|
173
237
|
});
|
|
174
238
|
}
|
|
@@ -267,19 +331,169 @@ export function journalPageId(entryId, page) {
|
|
|
267
331
|
: makeId("journal-page", `${entryId}:${page.name}`);
|
|
268
332
|
}
|
|
269
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Whether a caption carries markup an `image` page's `caption` field cannot
|
|
336
|
+
* hold.
|
|
337
|
+
*
|
|
338
|
+
* `image.caption` is a `StringField`; `text.content` is an `HTMLField`.
|
|
339
|
+
* Parsed as inline Markdown, plain prose tokenises as `text` alone, wrapping
|
|
340
|
+
* at most on a `softbreak` — anything else (emphasis, a code span, a link, an
|
|
341
|
+
* inline image) is a token an image page has nowhere to render.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} caption - The figure's raw caption markdown.
|
|
344
|
+
* @returns {boolean} Whether the caption needs a `text` page.
|
|
345
|
+
*/
|
|
346
|
+
function captionCarriesMarkup(caption) {
|
|
347
|
+
if (!caption) return false;
|
|
348
|
+
const [inline] = md.parseInline(caption, {});
|
|
349
|
+
return (inline?.children ?? []).some(
|
|
350
|
+
(token) => token.type !== "text" && token.type !== "softbreak",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The one picture a `figure`-kind fence holds, when it holds exactly one and
|
|
356
|
+
* nothing else — the shape an `image` page can draw on its own. A grouped
|
|
357
|
+
* fence, two pictures or more, keeps the `text` page a single `src` cannot
|
|
358
|
+
* hold, and an embed that resolves to a sound rather than a picture does too,
|
|
359
|
+
* there being no audio page type.
|
|
360
|
+
*
|
|
361
|
+
* By the time a journal note's markdown reaches this pass, an embed has
|
|
362
|
+
* already been rewritten into the ordinary `` image every surface
|
|
363
|
+
* renders ({@link module:engine/content-embeds}), so the file this needs is
|
|
364
|
+
* already sitting in the fence's own contents rather than behind an address
|
|
365
|
+
* this pass would have to resolve a second time.
|
|
366
|
+
*
|
|
367
|
+
* @param {object} figure - The figure record, `kind` already derived.
|
|
368
|
+
* @param {object} page - From {@link splitPages}; `page.markdown` starts on
|
|
369
|
+
* the figure's own opening line, so `figure`'s line-based offsets translate
|
|
370
|
+
* into it directly.
|
|
371
|
+
* @returns {string|null} The picture's `src`, or `null`.
|
|
372
|
+
*/
|
|
373
|
+
function soleFigurePicture(figure, page) {
|
|
374
|
+
if (figure.kind !== "figure" && figure.kind !== "map") return null;
|
|
375
|
+
const offset = figure.line - 1;
|
|
376
|
+
const lines = page.markdown.split("\n");
|
|
377
|
+
const contents = lines.slice(figure.bodyStart - offset, figure.bodyEnd - offset).join("\n");
|
|
378
|
+
const images = imagesIn(contents);
|
|
379
|
+
if (images.length !== 1) return null;
|
|
380
|
+
const extension = path.extname(images[0].src).toLowerCase();
|
|
381
|
+
return IMAGE_EXTENSIONS.includes(extension) ? images[0].src : null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Whether a figure fence is the whole of the page {@link splitPages} gave it.
|
|
386
|
+
*
|
|
387
|
+
* A figure always starts a page of its own, but nothing closes one: trailing
|
|
388
|
+
* prose before the next heading or figure stays on it, exactly as it would
|
|
389
|
+
* behind an ordinary heading. An `image` page holds a `src` and a caption and
|
|
390
|
+
* nowhere else to put that prose, so it is refused the type and kept a `text`
|
|
391
|
+
* page, where the prose renders after the figure's own div as it always has.
|
|
392
|
+
*
|
|
393
|
+
* @param {object} figure - The figure record.
|
|
394
|
+
* @param {object} page - From {@link splitPages}.
|
|
395
|
+
* @returns {boolean} Whether nothing follows the fence's closing `:::`.
|
|
396
|
+
*/
|
|
397
|
+
function figureIsWholePage(figure, page) {
|
|
398
|
+
const offset = figure.line - 1;
|
|
399
|
+
const after = page.markdown
|
|
400
|
+
.split("\n")
|
|
401
|
+
.slice(figure.close - offset + 1)
|
|
402
|
+
.join("\n");
|
|
403
|
+
return after.trim() === "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The fields particular to the Foundry page a `:::figure` fence begins.
|
|
408
|
+
*
|
|
409
|
+
* A fence holding exactly one picture, captioned with no inline markup, and
|
|
410
|
+
* standing alone on its page becomes a page of type `image`: `src` the
|
|
411
|
+
* picture, `image.caption` the caption text, the page named for the figure's
|
|
412
|
+
* own number. Every other fence — grouped, an audio embed, a caption
|
|
413
|
+
* `image.caption` cannot hold, or one trailing prose keeps company with —
|
|
414
|
+
* becomes a page of type `text`.
|
|
415
|
+
*
|
|
416
|
+
* The `text` page's content is rendered and then relabeled. The render sees
|
|
417
|
+
* only this one fence — {@link scanFigures}, called again inside
|
|
418
|
+
* {@link module:engine/content-figures.renderFigureBlocks} on this page's own
|
|
419
|
+
* markdown, recounts it as the first of its kind regardless of its true,
|
|
420
|
+
* note-wide number. The fence is the whole of the figure on this page, so its
|
|
421
|
+
* label paragraph is the one element to correct, to the number the note-wide
|
|
422
|
+
* scan in {@link buildJournalEntry} already assigned.
|
|
423
|
+
*
|
|
424
|
+
* @param {object} figure - The figure record from {@link scanFigures},
|
|
425
|
+
* carried onto the page by {@link splitPages}.
|
|
426
|
+
* @param {object} page - From {@link splitPages}.
|
|
427
|
+
* @param {string} pageId - The page's `_id`, threaded into footnote anchors.
|
|
428
|
+
* @param {Map<string, number>} footnoteNumbers - Shared across the note.
|
|
429
|
+
* @param {(address: string) => string|undefined} [resolveRole] - See
|
|
430
|
+
* {@link splitPages}.
|
|
431
|
+
* @returns {{name: string, type: "image", src: string, image: {caption?: string}}
|
|
432
|
+
* |{name: string, type: "text", text: {format: number, content: string}}}
|
|
433
|
+
*/
|
|
434
|
+
function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRole) {
|
|
435
|
+
const picture = soleFigurePicture(figure, page);
|
|
436
|
+
if (picture && figureIsWholePage(figure, page) && !captionCarriesMarkup(figure.caption)) {
|
|
437
|
+
let src = picture;
|
|
438
|
+
try {
|
|
439
|
+
src = resolveImg(picture) ?? picture;
|
|
440
|
+
} catch {
|
|
441
|
+
// Left as the authored pathname — the same fallback an inline
|
|
442
|
+
// image's own render takes when no configuration resolves it.
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
name: figure.label,
|
|
446
|
+
type: "image",
|
|
447
|
+
src,
|
|
448
|
+
image: figure.hasCaption ? { caption: figure.caption } : {},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const label =
|
|
452
|
+
figure.hasCaption ? `${figure.label}: ${md.renderInline(figure.caption)}` : figure.label;
|
|
453
|
+
const content = renderFoundryMarkdown(
|
|
454
|
+
page.markdown,
|
|
455
|
+
undefined,
|
|
456
|
+
footnoteNumbers,
|
|
457
|
+
pageId,
|
|
458
|
+
resolveRole,
|
|
459
|
+
);
|
|
460
|
+
return {
|
|
461
|
+
name: figure.label,
|
|
462
|
+
type: "text",
|
|
463
|
+
text: {
|
|
464
|
+
format: 1,
|
|
465
|
+
content: content.replace(
|
|
466
|
+
/<p class="content-figure-label">[\s\S]*?<\/p>/,
|
|
467
|
+
() => `<p class="content-figure-label">${label}</p>`,
|
|
468
|
+
),
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
270
473
|
/**
|
|
271
474
|
* Compile split pages into JournalEntryPage documents.
|
|
272
475
|
*
|
|
273
476
|
* @param {Array<object>} rawPages - From {@link splitPages}.
|
|
274
477
|
* @param {string} entryId - The owning JournalEntry's `_id`.
|
|
275
478
|
* @param {string} noteName - The note, for error messages.
|
|
479
|
+
* A page whose heading carries `.secret` is given to the GM alone, by the one
|
|
480
|
+
* ownership value Foundry reads as "nobody but a GM". Every other page states
|
|
481
|
+
* no ownership and inherits the journal's. A figure-started page states none
|
|
482
|
+
* either: {@link scanFigures} validates a figure's classes against its own
|
|
483
|
+
* closed vocabulary, which carries no `.secret`.
|
|
484
|
+
*
|
|
485
|
+
* @param {Array<object>} figures - From {@link scanFigures}, numbered across
|
|
486
|
+
* the whole note. Read by a page a heading started; a page a figure started
|
|
487
|
+
* carries its own figure already, from {@link splitPages}.
|
|
488
|
+
* @param {(address: string) => string|undefined} [resolveRole] - See
|
|
489
|
+
* {@link splitPages}.
|
|
276
490
|
* @returns {Array<{_id: string, name: string, type: string,
|
|
277
491
|
* title: {show: boolean, level: number},
|
|
278
|
-
* text: {format: number, content: string}, _key: string
|
|
279
|
-
* documents, in order.
|
|
492
|
+
* text: {format: number, content: string}, _key: string,
|
|
493
|
+
* ownership?: {default: number}}>} The page documents, in order.
|
|
280
494
|
* @throws {Error} When the note has no content at all, or repeats an anchor.
|
|
281
495
|
*/
|
|
282
|
-
export function buildPages(rawPages, entryId, noteName,
|
|
496
|
+
export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
|
|
283
497
|
if (rawPages.length === 0) {
|
|
284
498
|
throw new Error(
|
|
285
499
|
`note "${noteName}" has no Introduction content and no H1 headings — nothing to compile`,
|
|
@@ -296,18 +510,32 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
296
510
|
}
|
|
297
511
|
return rawPages.map((page) => {
|
|
298
512
|
const pageId = journalPageId(entryId, page);
|
|
513
|
+
const withheld = (page.classes ?? []).includes(WITHHELD_CLASS);
|
|
514
|
+
const fields =
|
|
515
|
+
page.figure ?
|
|
516
|
+
buildFigurePageFields(page.figure, page, pageId, footnoteNumbers, resolveRole)
|
|
517
|
+
: {
|
|
518
|
+
name: page.name,
|
|
519
|
+
type: "text",
|
|
520
|
+
text: {
|
|
521
|
+
format: 1,
|
|
522
|
+
content:
|
|
523
|
+
page.markdown ?
|
|
524
|
+
renderFoundryMarkdown(
|
|
525
|
+
page.markdown,
|
|
526
|
+
figures,
|
|
527
|
+
footnoteNumbers,
|
|
528
|
+
pageId,
|
|
529
|
+
resolveRole,
|
|
530
|
+
)
|
|
531
|
+
: "",
|
|
532
|
+
},
|
|
533
|
+
};
|
|
299
534
|
return {
|
|
300
535
|
_id: pageId,
|
|
301
|
-
|
|
302
|
-
type: "text",
|
|
536
|
+
...fields,
|
|
303
537
|
title: { show: true, level: page.level ?? 1 },
|
|
304
|
-
|
|
305
|
-
format: 1,
|
|
306
|
-
content:
|
|
307
|
-
page.markdown ?
|
|
308
|
-
renderFoundryMarkdown(page.markdown, captions, footnoteNumbers, pageId)
|
|
309
|
-
: "",
|
|
310
|
-
},
|
|
538
|
+
...(withheld ? { ownership: { default: WITHHELD_OWNERSHIP } } : {}),
|
|
311
539
|
_key: `!journal.pages!${entryId}.${pageId}`,
|
|
312
540
|
};
|
|
313
541
|
});
|
|
@@ -329,7 +557,6 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
329
557
|
* @param {string} [params.leadName] - Name for the page before the first
|
|
330
558
|
* heading; see {@link splitPages}.
|
|
331
559
|
* @param {string|null} [params.folder] - The folder id, or `null`.
|
|
332
|
-
* @param {object} [params.flags] - Document flags.
|
|
333
560
|
* @param {object} [params.stats] - The `_stats` block to stamp. Passed by the
|
|
334
561
|
* caller because it is a property of the *pack* being written, not of the
|
|
335
562
|
* entry: a module may ship the same content for two systems, and each pack's
|
|
@@ -344,6 +571,10 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
344
571
|
* module:engine/draft-notice}. It leads the first authored page,
|
|
345
572
|
* because a reader deciding whether to rely on the entry has to be told
|
|
346
573
|
* before they read it rather than after.
|
|
574
|
+
* @param {(address: string) => string|undefined} [params.resolveRole] - From
|
|
575
|
+
* a picture's address to the role its asset declares, so a figure page's
|
|
576
|
+
* name reads `Map 1` rather than `Figure 1` where it is due — see
|
|
577
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
347
578
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
348
579
|
*/
|
|
349
580
|
export function buildJournalEntry({
|
|
@@ -352,13 +583,19 @@ export function buildJournalEntry({
|
|
|
352
583
|
markdown,
|
|
353
584
|
leadName,
|
|
354
585
|
folder = null,
|
|
355
|
-
flags,
|
|
356
586
|
stats = defaultStats(),
|
|
357
587
|
infoboxes = [],
|
|
358
588
|
notice = "",
|
|
589
|
+
resolveRole,
|
|
359
590
|
}) {
|
|
360
|
-
const rawPages = splitPages(markdown, leadName);
|
|
361
|
-
const pages = buildPages(
|
|
591
|
+
const rawPages = splitPages(markdown, leadName, resolveRole);
|
|
592
|
+
const pages = buildPages(
|
|
593
|
+
rawPages,
|
|
594
|
+
id,
|
|
595
|
+
name,
|
|
596
|
+
scanFigures(markdown, { resolveRole }).figures,
|
|
597
|
+
resolveRole,
|
|
598
|
+
);
|
|
362
599
|
for (const box of infoboxes) {
|
|
363
600
|
const pageId = makeId("journal-infobox-page", `${id}:${box.id}`);
|
|
364
601
|
pages.push({
|
|
@@ -379,7 +616,7 @@ export function buildJournalEntry({
|
|
|
379
616
|
folder,
|
|
380
617
|
sort: 0,
|
|
381
618
|
ownership: { default: 0 },
|
|
382
|
-
flags:
|
|
619
|
+
flags: {},
|
|
383
620
|
_id: id,
|
|
384
621
|
_stats: stats,
|
|
385
622
|
_key: `!journal!${id}`,
|
|
@@ -521,12 +758,20 @@ export class Journals extends BasePackCompiler {
|
|
|
521
758
|
// by the pack list this build is being driven by.
|
|
522
759
|
router: this.router,
|
|
523
760
|
dates: reckoningContext(this.linkIndex),
|
|
761
|
+
contentPackage: this.linkIndex?.contentPackage,
|
|
524
762
|
});
|
|
525
763
|
|
|
764
|
+
// The markdown this pass reads has already had its embeds rewritten
|
|
765
|
+
// into ordinary images, each `src` the pathname `pathnameRoles` keys
|
|
766
|
+
// its roles by — so a `:::figure` of a map names itself `Map 1` here
|
|
767
|
+
// exactly as the website and the book name the same picture.
|
|
768
|
+
const roles = pathnameRoles(this.linkIndex);
|
|
769
|
+
|
|
526
770
|
return buildJournalEntry({
|
|
527
771
|
id,
|
|
528
772
|
name,
|
|
529
773
|
markdown,
|
|
774
|
+
resolveRole: (pathname) => roles.get(pathname),
|
|
530
775
|
infoboxes: boxes.map((box) => ({
|
|
531
776
|
id: box.id,
|
|
532
777
|
name: `${
|
|
@@ -546,7 +791,6 @@ export class Journals extends BasePackCompiler {
|
|
|
546
791
|
// "Introduction" — see {@link splitPages}.
|
|
547
792
|
leadName: ownsDoc ? name : undefined,
|
|
548
793
|
folder,
|
|
549
|
-
flags: fm.flags,
|
|
550
794
|
stats: this.stats,
|
|
551
795
|
});
|
|
552
796
|
}
|