@heroiclands/package-build 22.20.0 → 22.22.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-content-build.md +0 -8
- package/CHANGELOG.md +204 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +26 -3
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +73 -4
- package/docs/api.md +46 -28
- package/docs/authoring/dates-and-calendars.md +4 -2
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +237 -28
- package/docs/configuration.md +27 -1
- package/docs/content-format.md +3 -1
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +265 -42
- package/docs/reference/note-types.md +45 -28
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +10 -20
- package/engine/base-compiler.mjs +86 -21
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +0 -1
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-captions.mjs +57 -8
- package/engine/content-footnotes.mjs +189 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-index.mjs +2 -2
- 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/frontmatter-lint.mjs +62 -66
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +308 -0
- package/engine/helpers.mjs +19 -13
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-render.mjs +21 -4
- package/engine/infobox.mjs +167 -7
- package/engine/journals.mjs +36 -17
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +178 -53
- package/engine/note-addresses.mjs +17 -8
- package/engine/note-dates.mjs +49 -0
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +216 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pdf-build.mjs +76 -42
- package/engine/pdf-render.mjs +422 -49
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/reference-targets.mjs +13 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +10 -3
- package/engine/site-build.mjs +41 -12
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +39 -4
- package/engine/site-preview.mjs +18 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standing-terms.mjs +19 -0
- package/engine/standings.mjs +538 -0
- package/engine/svg-theme.mjs +15 -4
- package/engine/system-block.mjs +12 -14
- package/engine/web-wikilinks.mjs +48 -1
- package/engine/wikilinks.mjs +3 -1
- package/hm3/actors.mjs +6 -3
- package/manifest.mjs +13 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +82 -103
- package/sohl/item-fields.mjs +4 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/base-compiler.d.mts +19 -0
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-captions.d.mts +14 -1
- package/types/engine/content-footnotes.d.mts +73 -0
- package/types/engine/content-format-check.d.mts +8 -6
- 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/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +174 -0
- package/types/engine/helpers.d.mts +7 -4
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/journals.d.mts +14 -7
- package/types/engine/markdown-expressions.d.mts +23 -0
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +98 -8
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pdf-render.d.mts +72 -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 +13 -1
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +13 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standing-terms.d.mts +16 -0
- package/types/engine/standings.d.mts +147 -0
- package/types/engine/system-block.d.mts +11 -12
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/engine/wikilinks.d.mts +2 -1
- package/types/hm3/actors.d.mts +3 -3
- package/types/sohl/actors.d.mts +25 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-secrets.d.mts +0 -17
package/engine/homepage.mjs
CHANGED
|
@@ -131,12 +131,12 @@ export function isHomepage(fm) {
|
|
|
131
131
|
* `name` and `shortcode` are permitted: the shortcode is what a link is
|
|
132
132
|
* written with, and `name` titles the page like every other note's.
|
|
133
133
|
*
|
|
134
|
-
* **A named class, not an allow-list, and that boundary is the decision.**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
134
|
+
* **A named class, not an allow-list, and that boundary is the decision.**
|
|
135
|
+
* What this class answers is a key the top-level vocabulary *accepts* and a
|
|
136
|
+
* homepage still cannot use, which is a different question from an unknown
|
|
137
|
+
* key: the frontmatter lint refuses those for every note alike, naming the
|
|
138
|
+
* closed vocabulary, so repeating that judgement per type would be a second
|
|
139
|
+
* answer to one question.
|
|
140
140
|
*
|
|
141
141
|
* `aliases` is deliberately not in the class: it is a **retired** field, refused
|
|
142
142
|
* on every note whatever its type, so it is answered there rather than
|
|
@@ -340,7 +340,7 @@ function positionOfType(file) {
|
|
|
340
340
|
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
341
341
|
* package — so a homepage that adds nothing to it need not restate it. An
|
|
342
342
|
* authored `name.full` wins, because a front page may greet a reader
|
|
343
|
-
* differently from a package browser.
|
|
343
|
+
* differently from a package browser.
|
|
344
344
|
*
|
|
345
345
|
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
346
346
|
* own still yields a titled page rather than a blank heading.
|
|
@@ -350,7 +350,7 @@ function positionOfType(file) {
|
|
|
350
350
|
* @returns {string} The title.
|
|
351
351
|
*/
|
|
352
352
|
export function homepageTitle(fm, config) {
|
|
353
|
-
const authored = fm?.name?.full
|
|
353
|
+
const authored = fm?.name?.full;
|
|
354
354
|
if (typeof authored === "string" && authored.trim()) return authored;
|
|
355
355
|
const manifest = /** @type {Record<string, unknown>|undefined} */ (
|
|
356
356
|
config?.packageBuild?.manifest
|
package/engine/index.mjs
CHANGED
|
@@ -220,6 +220,9 @@ export * as contentHtml from "./content-html.mjs";
|
|
|
220
220
|
/** An image saying how wide it is and where it sits, in two closed vocabularies. */
|
|
221
221
|
export * as contentImages from "./content-images.mjs";
|
|
222
222
|
|
|
223
|
+
/** A list item opened with `[ ]` or `[x]`, which every surface reads as text. */
|
|
224
|
+
export * as contentTaskLists from "./content-tasklists.mjs";
|
|
225
|
+
|
|
223
226
|
/** `![[…]]` — the wikilink that renders a picture where it stands. */
|
|
224
227
|
export * as contentEmbeds from "./content-embeds.mjs";
|
|
225
228
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { encodeAddresses } from "./address-values.mjs";
|
|
38
38
|
import { sectionHolds } from "./infobox.mjs";
|
|
39
|
-
import { escapeTypst, escapeTypstString } from "./pdf-render.mjs";
|
|
39
|
+
import { absolutePageUrl, escapeTypst, escapeTypstString } from "./pdf-render.mjs";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* How many cells an attribute grid packs into a row.
|
|
@@ -139,7 +139,12 @@ function sectionToHtml(section, link) {
|
|
|
139
139
|
if (section.layout === "rows") {
|
|
140
140
|
out.push('<dl class="infobox-rows">');
|
|
141
141
|
for (const row of section.rows ?? []) {
|
|
142
|
-
|
|
142
|
+
// A row that declares an anchor carries it on the `<dt>`, so the
|
|
143
|
+
// label is the destination a link to that row arrives at. A row
|
|
144
|
+
// that declares none emits none: an id nobody wrote would be
|
|
145
|
+
// invented once per renderer, and the two would not agree.
|
|
146
|
+
const id = row.id ? ` id="${escapeHtml(row.id)}"` : "";
|
|
147
|
+
out.push(`<dt${id}>${escapeHtml(row.label)}</dt><dd>${valueToHtml(row, link)}</dd>`);
|
|
143
148
|
}
|
|
144
149
|
out.push("</dl>");
|
|
145
150
|
} else if (section.layout === "grid") {
|
|
@@ -221,16 +226,22 @@ export function sectionHasContent(section) {
|
|
|
221
226
|
* reference, so the link works on paper as a cross-reference and in a PDF
|
|
222
227
|
* viewer as a jump. Everything else is set as its own words.
|
|
223
228
|
*
|
|
229
|
+
* A panel's rows address pages the same way prose does, so a page the book does
|
|
230
|
+
* not print is set as the absolute URL a reader can follow — see
|
|
231
|
+
* {@link module:engine/pdf-render.absolutePageUrl}.
|
|
232
|
+
*
|
|
224
233
|
* @param {object} value - A `link` value.
|
|
225
234
|
* @param {Map<string, string>} links - Address slug → the book's anchor.
|
|
226
235
|
* @param {(anchor: string) => string} labelFor - The anchor's Typst label.
|
|
236
|
+
* @param {string} [site] - The absolute address this book's pages are served at.
|
|
227
237
|
* @returns {string} Typst markup.
|
|
228
238
|
*/
|
|
229
|
-
export function linkToTypst(value, links, labelFor) {
|
|
239
|
+
export function linkToTypst(value, links, labelFor, site) {
|
|
230
240
|
const text = escapeTypst(value?.text ?? "");
|
|
231
241
|
const anchor = value?.address ? links?.get(encodeAddresses(value.address)) : undefined;
|
|
232
242
|
if (anchor) return `#link(<${labelFor(anchor)}>)[${text}]`;
|
|
233
|
-
if (value?.url)
|
|
243
|
+
if (value?.url)
|
|
244
|
+
return `#link("${escapeTypstString(absolutePageUrl(value.url, site))}")[${text}]`;
|
|
234
245
|
return text;
|
|
235
246
|
}
|
|
236
247
|
|
|
@@ -370,7 +381,13 @@ export function infoboxTypstPreamble() {
|
|
|
370
381
|
"fill: infobox-rule)[#upper(t)]; v(0.20em) }",
|
|
371
382
|
'#let infobox-head(t) = { v(0.40em); text(size: 7.6pt, weight: "bold", ' +
|
|
372
383
|
"tracking: 1.3pt, fill: infobox-rule)[#upper(t)]; v(0.16em) }",
|
|
384
|
+
// One definition-list style across the three surfaces: the term right
|
|
385
|
+
// against a fixed column so every term in the panel shares one edge,
|
|
386
|
+
// and space below the row rather than rows run together. A page has one
|
|
387
|
+
// width, so the stacked state the screen surfaces fall back to has
|
|
388
|
+
// nothing to answer here and is not drawn.
|
|
373
389
|
"#let infobox-row(k, v) = grid(columns: (2.05cm, 1fr), column-gutter: 4pt, " +
|
|
390
|
+
"inset: (bottom: 2pt), align: (right + top, left + top), " +
|
|
374
391
|
"text(size: 7pt, tracking: 0.5pt, fill: infobox-faint)[#upper(k)], text(size: 8pt)[#v])",
|
|
375
392
|
"#let infobox-cell(lab, val) = align(center)[" +
|
|
376
393
|
"#text(size: 7pt, tracking: 0.6pt, fill: infobox-faint)[#lab]#h(2.5pt)" +
|
package/engine/infobox.mjs
CHANGED
|
@@ -121,6 +121,13 @@ import { subtypeRow } from "./document-subtypes.mjs";
|
|
|
121
121
|
import { authoredKey } from "./system-block.mjs";
|
|
122
122
|
import { formatDateInCalendar, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
123
123
|
import { displayBeingHeight, displayBeingWeight } from "./being-measurements.mjs";
|
|
124
|
+
import {
|
|
125
|
+
officeAnchor,
|
|
126
|
+
officeRoster,
|
|
127
|
+
rankAnchor,
|
|
128
|
+
readStandings,
|
|
129
|
+
standingPhrase,
|
|
130
|
+
} from "./standings.mjs";
|
|
124
131
|
|
|
125
132
|
/**
|
|
126
133
|
* How a section arranges what it holds.
|
|
@@ -326,7 +333,8 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
326
333
|
}),
|
|
327
334
|
"being.appearance.complexion": Object.freeze({
|
|
328
335
|
group: "appearance",
|
|
329
|
-
|
|
336
|
+
whole: true,
|
|
337
|
+
phrase: (v) => `${humanizedList(v)} complexion`,
|
|
330
338
|
}),
|
|
331
339
|
"being.appearance.extra_features": Object.freeze({ group: "appearance" }),
|
|
332
340
|
});
|
|
@@ -468,6 +476,24 @@ export function humanizeValue(value) {
|
|
|
468
476
|
.trim();
|
|
469
477
|
}
|
|
470
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Several enumerated values as one clause — `weathered, ruddy and scarred`.
|
|
481
|
+
*
|
|
482
|
+
* A field that admits one value or a list of them reads as a single phrase, so
|
|
483
|
+
* the noun it qualifies is said once rather than once per value. Internal: the
|
|
484
|
+
* phrase that wants it is declared in this module.
|
|
485
|
+
*
|
|
486
|
+
* @param {unknown} value - One value, or a list of them.
|
|
487
|
+
* @returns {string} The text.
|
|
488
|
+
*/
|
|
489
|
+
function humanizedList(value) {
|
|
490
|
+
const parts = (Array.isArray(value) ? value : [value])
|
|
491
|
+
.filter((entry) => entry !== undefined && entry !== null && entry !== "")
|
|
492
|
+
.map((entry) => humanizeValue(entry));
|
|
493
|
+
if (parts.length < 2) return parts[0] ?? "";
|
|
494
|
+
return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
471
497
|
/**
|
|
472
498
|
* A value as a row shows it.
|
|
473
499
|
*
|
|
@@ -553,11 +579,143 @@ function rowValue(kind, raw, resolve, hint) {
|
|
|
553
579
|
return presentValue(raw);
|
|
554
580
|
}
|
|
555
581
|
|
|
582
|
+
/**
|
|
583
|
+
* A being's memberships, each with the standing it holds there.
|
|
584
|
+
*
|
|
585
|
+
* One row, one entry per body, and the standing reads inside the entry rather
|
|
586
|
+
* than beside it — "War Chief, of Vrystwald Tribes" rather than a rank link
|
|
587
|
+
* with nothing saying which body confers it.
|
|
588
|
+
*
|
|
589
|
+
* **The office carries the link where the body's page anchors it.** An office's
|
|
590
|
+
* description is already declared in the body's own `governance.offices`, and
|
|
591
|
+
* the row that prints it is anchored by the same derivation, so the string a
|
|
592
|
+
* being wrote reaches the description without anyone authoring a note about the
|
|
593
|
+
* post. In a medium with no page to reach — a compendium journal links by
|
|
594
|
+
* document UUID — the entry keeps the body's own document, so the reader still
|
|
595
|
+
* lands somewhere.
|
|
596
|
+
*
|
|
597
|
+
* @param {object} field - The declaration.
|
|
598
|
+
* @param {unknown} raw - The authored value, in either form.
|
|
599
|
+
* @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
|
|
600
|
+
* medium's resolver.
|
|
601
|
+
* @param {string} label - The row's label.
|
|
602
|
+
* @returns {object[]} The row, or none where nothing is held.
|
|
603
|
+
*/
|
|
604
|
+
function standingRows(field, raw, resolve, label) {
|
|
605
|
+
const { entries } = readStandings(raw);
|
|
606
|
+
const value = [];
|
|
607
|
+
for (const { body, standing } of entries) {
|
|
608
|
+
if (!hasValue(body)) continue;
|
|
609
|
+
const link = linkValue(body, resolve, { type: field.ref });
|
|
610
|
+
const digest = resolve?.(body, { type: field.ref })?.standings;
|
|
611
|
+
const text = standingPhrase(standing, link.text, digest);
|
|
612
|
+
const anchor =
|
|
613
|
+
(
|
|
614
|
+
typeof standing?.office === "string" &&
|
|
615
|
+
digest?.offices?.[standing.office] !== undefined
|
|
616
|
+
) ?
|
|
617
|
+
officeAnchor(standing.office)
|
|
618
|
+
: "";
|
|
619
|
+
value.push({
|
|
620
|
+
...link,
|
|
621
|
+
text,
|
|
622
|
+
...(anchor && link.url ? { url: `${link.url}#${anchor}` } : {}),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return value.length ? [{ label, kind: "links", value }] : [];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* A map of named posts, one row each, anchored by its name.
|
|
630
|
+
*
|
|
631
|
+
* The **name is the label and its description is the value**, which is the way
|
|
632
|
+
* round a reader needs: a field name belongs in the label column, and a whole
|
|
633
|
+
* sentence there leaves the post itself standing where a value goes.
|
|
634
|
+
*
|
|
635
|
+
* Each row carries the anchor its name derives, so the description is a
|
|
636
|
+
* destination rather than merely text on a page — which is what lets a being's
|
|
637
|
+
* `office` string link to the post it names without anyone authoring a note
|
|
638
|
+
* about the post.
|
|
639
|
+
*
|
|
640
|
+
* @param {unknown} raw - The authored map.
|
|
641
|
+
* @returns {object[]|null} One row per post, in the order the body declared
|
|
642
|
+
* them, or `null` where the value is not a map of them — a note writing
|
|
643
|
+
* something else keeps its row and states what it wrote.
|
|
644
|
+
*/
|
|
645
|
+
function rosterRows(raw) {
|
|
646
|
+
if (!isMapping(raw) || isAddressTuple(raw)) return null;
|
|
647
|
+
const rows = [];
|
|
648
|
+
for (const [post, description] of officeRoster(raw)) {
|
|
649
|
+
if (!hasValue(post)) continue;
|
|
650
|
+
const anchor = officeAnchor(post);
|
|
651
|
+
rows.push({
|
|
652
|
+
...(anchor ? { id: anchor } : {}),
|
|
653
|
+
label: String(post),
|
|
654
|
+
kind: "text",
|
|
655
|
+
value: hasValue(description) ? String(description) : String(post),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return rows;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* A ladder of named ranks, one row each, ordered by the level a body confers.
|
|
663
|
+
*
|
|
664
|
+
* The **title is the label and its description is the value**, the same
|
|
665
|
+
* convention {@link rosterRows} draws a body's offices with, so a reader
|
|
666
|
+
* meets the two lists the same way. A rung's optional `lore` — already
|
|
667
|
+
* resolved to an Address by the time this runs — reads alongside the
|
|
668
|
+
* description rather than replacing it, since the description is what the
|
|
669
|
+
* rung itself states and the lore is a pointer to more.
|
|
670
|
+
*
|
|
671
|
+
* @param {unknown} raw - The authored `governance.ranks` list.
|
|
672
|
+
* @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
|
|
673
|
+
* medium's resolver.
|
|
674
|
+
* @returns {object[]|null} One row per rung, in ascending level, or `null`
|
|
675
|
+
* where the value is not a list of them.
|
|
676
|
+
*/
|
|
677
|
+
function rankRows(raw, resolve) {
|
|
678
|
+
if (!Array.isArray(raw)) return null;
|
|
679
|
+
const rungs = raw
|
|
680
|
+
.filter((rung) => isMapping(rung) && hasValue(rung.title))
|
|
681
|
+
.toSorted((a, b) => Number(a.level ?? 0) - Number(b.level ?? 0));
|
|
682
|
+
const rows = [];
|
|
683
|
+
for (const rung of rungs) {
|
|
684
|
+
const anchor = rankAnchor(String(rung.title));
|
|
685
|
+
const description =
|
|
686
|
+
hasValue(rung.description) ? String(rung.description) : String(rung.title);
|
|
687
|
+
const lore = hasValue(rung.lore) ? linkValue(rung.lore, resolve, { type: "lore" }) : null;
|
|
688
|
+
rows.push({
|
|
689
|
+
...(anchor ? { id: anchor } : {}),
|
|
690
|
+
label: String(rung.title),
|
|
691
|
+
kind: "text",
|
|
692
|
+
value: lore?.text ? `${description} (Lore: ${lore.text})` : description,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return rows;
|
|
696
|
+
}
|
|
697
|
+
|
|
556
698
|
/** Expand structured references using the existing linked-row shape. */
|
|
557
699
|
function structuredRows(field, raw, resolve, label, fm) {
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
700
|
+
if (field.standings) return standingRows(field, raw, resolve, label);
|
|
701
|
+
if (field.roster) {
|
|
702
|
+
const roster = rosterRows(raw);
|
|
703
|
+
if (roster) return roster;
|
|
704
|
+
}
|
|
705
|
+
if (field.ranks) {
|
|
706
|
+
const ranks = rankRows(raw, resolve);
|
|
707
|
+
if (ranks) return ranks;
|
|
708
|
+
}
|
|
709
|
+
// A relationship entry — `{to, ...}` — is a mapping that is not itself an
|
|
710
|
+
// Address: an authored plain list of addresses normalises to a list of
|
|
711
|
+
// Address tuples, which `isMapping` also reports true for, and treating
|
|
712
|
+
// one as a relationship keyed off a `to` it never carries is what emptied
|
|
713
|
+
// `lore`, `parents`, `domains` and `economy` silently.
|
|
714
|
+
const relations =
|
|
715
|
+
Array.isArray(raw) ? raw.filter((entry) => isMapping(entry) && !isAddressTuple(entry)) : [];
|
|
716
|
+
if (relations.length) {
|
|
717
|
+
const links = relations
|
|
718
|
+
.filter((entry) => hasValue(entry.to))
|
|
561
719
|
.map((entry) => {
|
|
562
720
|
const link = linkValue(entry.to, resolve, { type: field.ref ?? "place" });
|
|
563
721
|
const details = Object.entries(entry)
|
|
@@ -581,7 +739,7 @@ function structuredRows(field, raw, resolve, label, fm) {
|
|
|
581
739
|
const address = parseAddress(
|
|
582
740
|
target,
|
|
583
741
|
{
|
|
584
|
-
package:
|
|
742
|
+
package: "local",
|
|
585
743
|
system: "note",
|
|
586
744
|
types: new Set(field.accepts),
|
|
587
745
|
},
|
|
@@ -704,7 +862,7 @@ function noteBox(
|
|
|
704
862
|
const rows = [];
|
|
705
863
|
/** @type {Set<string>} */
|
|
706
864
|
const shown = new Set();
|
|
707
|
-
const name = fm?.name?.full
|
|
865
|
+
const name = fm?.name?.full;
|
|
708
866
|
if (hasValue(name)) rows.push({ label: "Name", kind: "text", value: String(name) });
|
|
709
867
|
if (Array.isArray(fm?.name?.aliases) && fm.name.aliases.length) {
|
|
710
868
|
rows.push({ label: "Aliases", kind: "list", value: fm.name.aliases });
|
|
@@ -772,7 +930,9 @@ function noteBox(
|
|
|
772
930
|
label: GROUP_LABELS[overlay.group] ?? humanizeFieldName(overlay.group),
|
|
773
931
|
entries: [],
|
|
774
932
|
};
|
|
775
|
-
|
|
933
|
+
// A `whole` phrase reads the value entire, so several values compose
|
|
934
|
+
// into one clause rather than repeating the noun once each.
|
|
935
|
+
const parts = overlay.whole || !Array.isArray(raw) ? [raw] : raw.filter(hasValue);
|
|
776
936
|
for (const part of parts) {
|
|
777
937
|
group.entries.push(overlay.phrase ? overlay.phrase(part) : humanizeValue(part));
|
|
778
938
|
}
|
package/engine/journals.mjs
CHANGED
|
@@ -71,6 +71,14 @@ import { journalHasContent } from "./note-state.mjs";
|
|
|
71
71
|
import { draftNoticeFor } from "./draft-notice.mjs";
|
|
72
72
|
import { scanCaptions } from "./content-captions.mjs";
|
|
73
73
|
import { separateFootnotes } from "./content-footnotes.mjs";
|
|
74
|
+
import { WITHHELD_CLASS, pageOpenings } from "./heading-attributes.mjs";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE` — the default ownership a withheld
|
|
78
|
+
* page states. A GM reads every document whatever it says; everyone else reads
|
|
79
|
+
* this one not at all.
|
|
80
|
+
*/
|
|
81
|
+
const WITHHELD_OWNERSHIP = 0;
|
|
74
82
|
|
|
75
83
|
/**
|
|
76
84
|
* Splits a markdown body into pages by top-level H1 headings. Fenced
|
|
@@ -78,7 +86,8 @@ import { separateFootnotes } from "./content-footnotes.mjs";
|
|
|
78
86
|
* a split. Content before the first H1 (if non-empty) becomes a leading
|
|
79
87
|
* page. Each H1 yields a page whose name is the heading
|
|
80
88
|
* text (with any `{#anchor-id}` suffix stripped out and surfaced as
|
|
81
|
-
* `anchorSlug`).
|
|
89
|
+
* `anchorSlug`). The classes the heading declares come along as `classes`, so
|
|
90
|
+
* a surface that honours one has it without reading the suffix again.
|
|
82
91
|
*
|
|
83
92
|
* `leadName` names that leading page. A journal note's is "Introduction",
|
|
84
93
|
* because it introduces the pages that follow. An item doc's is the item — a
|
|
@@ -89,7 +98,7 @@ import { separateFootnotes } from "./content-footnotes.mjs";
|
|
|
89
98
|
* @param {string} body - Markdown body to split.
|
|
90
99
|
* @param {string} [leadName] - Name of the page before the first heading.
|
|
91
100
|
* @returns {Array<{name: string, anchorSlug: string|null, level: number,
|
|
92
|
-
* markdown: string}>} Pages in document order.
|
|
101
|
+
* classes: string[], markdown: string}>} Pages in document order.
|
|
93
102
|
*/
|
|
94
103
|
export function splitPages(body, leadName = "Introduction") {
|
|
95
104
|
const { markdown, definitions } = separateFootnotes(body);
|
|
@@ -101,6 +110,7 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
101
110
|
.filter((caption) => /^\s*#{1,6}\s/.test(lines[caption.blockStart] ?? ""))
|
|
102
111
|
.map((caption) => caption.blockStart),
|
|
103
112
|
);
|
|
113
|
+
const openings = pageOpenings(markdown);
|
|
104
114
|
const pages = [];
|
|
105
115
|
const beforeFirstH1 = [];
|
|
106
116
|
let current = null;
|
|
@@ -113,6 +123,7 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
113
123
|
name: current.name,
|
|
114
124
|
anchorSlug: current.anchorSlug,
|
|
115
125
|
level: current.level,
|
|
126
|
+
classes: current.classes,
|
|
116
127
|
markdown: current.lines.join("\n").trim(),
|
|
117
128
|
});
|
|
118
129
|
current = null;
|
|
@@ -128,11 +139,13 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
128
139
|
// An H1 starts a page, as does any heading carrying an `{#slug}`
|
|
129
140
|
// anchor: a Foundry UUID can only address a page, so a linkable
|
|
130
141
|
// section has to be one.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
// {@link module:engine/heading-attributes.parseHeadingLine} is the one
|
|
143
|
+
// reading of that rule — `scanBlocks` and `scanCaptions` refuse such a
|
|
144
|
+
// heading written where it cannot become a page, from the same
|
|
145
|
+
// function, so none of them can disagree about what starts one. The
|
|
146
|
+
// lines it claims here are collected by `pageOpenings`, which also
|
|
147
|
+
// skips a fence and a named block.
|
|
148
|
+
const opening = openings.get(lineIndex);
|
|
136
149
|
const caption = !inCodeBlock && !inSecret ? captionStarts.get(lineIndex) : null;
|
|
137
150
|
if (caption) {
|
|
138
151
|
closeCurrent();
|
|
@@ -140,16 +153,18 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
140
153
|
name: caption.label,
|
|
141
154
|
anchorSlug: caption.id,
|
|
142
155
|
level: 1,
|
|
156
|
+
classes: [],
|
|
143
157
|
lines: [line],
|
|
144
158
|
};
|
|
145
159
|
continue;
|
|
146
160
|
}
|
|
147
|
-
if (
|
|
161
|
+
if (opening && !captionedHeadings.has(lineIndex)) {
|
|
148
162
|
closeCurrent();
|
|
149
163
|
current = {
|
|
150
|
-
name:
|
|
151
|
-
anchorSlug:
|
|
152
|
-
level:
|
|
164
|
+
name: opening.text,
|
|
165
|
+
anchorSlug: opening.id || null,
|
|
166
|
+
level: opening.level,
|
|
167
|
+
classes: opening.classes,
|
|
153
168
|
lines: [],
|
|
154
169
|
};
|
|
155
170
|
continue;
|
|
@@ -169,6 +184,7 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
169
184
|
name: leadName,
|
|
170
185
|
anchorSlug: null,
|
|
171
186
|
level: 1,
|
|
187
|
+
classes: [],
|
|
172
188
|
markdown: intro,
|
|
173
189
|
});
|
|
174
190
|
}
|
|
@@ -273,10 +289,14 @@ export function journalPageId(entryId, page) {
|
|
|
273
289
|
* @param {Array<object>} rawPages - From {@link splitPages}.
|
|
274
290
|
* @param {string} entryId - The owning JournalEntry's `_id`.
|
|
275
291
|
* @param {string} noteName - The note, for error messages.
|
|
292
|
+
* A page whose heading carries `.secret` is given to the GM alone, by the one
|
|
293
|
+
* ownership value Foundry reads as "nobody but a GM". Every other page states
|
|
294
|
+
* no ownership and inherits the journal's.
|
|
295
|
+
*
|
|
276
296
|
* @returns {Array<{_id: string, name: string, type: string,
|
|
277
297
|
* title: {show: boolean, level: number},
|
|
278
|
-
* text: {format: number, content: string}, _key: string
|
|
279
|
-
* documents, in order.
|
|
298
|
+
* text: {format: number, content: string}, _key: string,
|
|
299
|
+
* ownership?: {default: number}}>} The page documents, in order.
|
|
280
300
|
* @throws {Error} When the note has no content at all, or repeats an anchor.
|
|
281
301
|
*/
|
|
282
302
|
export function buildPages(rawPages, entryId, noteName, captions) {
|
|
@@ -296,6 +316,7 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
296
316
|
}
|
|
297
317
|
return rawPages.map((page) => {
|
|
298
318
|
const pageId = journalPageId(entryId, page);
|
|
319
|
+
const withheld = (page.classes ?? []).includes(WITHHELD_CLASS);
|
|
299
320
|
return {
|
|
300
321
|
_id: pageId,
|
|
301
322
|
name: page.name,
|
|
@@ -308,6 +329,7 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
308
329
|
renderFoundryMarkdown(page.markdown, captions, footnoteNumbers, pageId)
|
|
309
330
|
: "",
|
|
310
331
|
},
|
|
332
|
+
...(withheld ? { ownership: { default: WITHHELD_OWNERSHIP } } : {}),
|
|
311
333
|
_key: `!journal.pages!${entryId}.${pageId}`,
|
|
312
334
|
};
|
|
313
335
|
});
|
|
@@ -329,7 +351,6 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
329
351
|
* @param {string} [params.leadName] - Name for the page before the first
|
|
330
352
|
* heading; see {@link splitPages}.
|
|
331
353
|
* @param {string|null} [params.folder] - The folder id, or `null`.
|
|
332
|
-
* @param {object} [params.flags] - Document flags.
|
|
333
354
|
* @param {object} [params.stats] - The `_stats` block to stamp. Passed by the
|
|
334
355
|
* caller because it is a property of the *pack* being written, not of the
|
|
335
356
|
* entry: a module may ship the same content for two systems, and each pack's
|
|
@@ -352,7 +373,6 @@ export function buildJournalEntry({
|
|
|
352
373
|
markdown,
|
|
353
374
|
leadName,
|
|
354
375
|
folder = null,
|
|
355
|
-
flags,
|
|
356
376
|
stats = defaultStats(),
|
|
357
377
|
infoboxes = [],
|
|
358
378
|
notice = "",
|
|
@@ -379,7 +399,7 @@ export function buildJournalEntry({
|
|
|
379
399
|
folder,
|
|
380
400
|
sort: 0,
|
|
381
401
|
ownership: { default: 0 },
|
|
382
|
-
flags:
|
|
402
|
+
flags: {},
|
|
383
403
|
_id: id,
|
|
384
404
|
_stats: stats,
|
|
385
405
|
_key: `!journal!${id}`,
|
|
@@ -546,7 +566,6 @@ export class Journals extends BasePackCompiler {
|
|
|
546
566
|
// "Introduction" — see {@link splitPages}.
|
|
547
567
|
leadName: ownsDoc ? name : undefined,
|
|
548
568
|
folder,
|
|
549
|
-
flags: fm.flags,
|
|
550
569
|
stats: this.stats,
|
|
551
570
|
});
|
|
552
571
|
}
|
package/engine/macros.mjs
CHANGED