@heroiclands/package-build 25.5.4 → 25.6.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 +40 -0
- package/README.md +7 -0
- package/assets/styles/content-base.css +1 -1
- package/bin/content-commands.mjs +32 -8
- package/docs/api.md +81 -32
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +59 -10
- package/docs/commands.md +60 -17
- package/docs/configuration.md +3 -2
- package/docs/content-format.md +50 -1
- package/docs/diagnostics.md +17 -0
- package/docs/guides/module-project.md +11 -3
- package/docs/guides/reference-pages.md +4 -1
- package/docs/guides/site.md +13 -3
- package/docs/guides/writing-in-invented-scripts.md +5 -2
- package/docs/reference/format-details.md +233 -138
- package/docs/reference/note-types.md +165 -152
- package/docs/reference/theme-pages.md +1 -0
- package/e2e.mjs +1 -1
- package/engine/base-compiler.mjs +2 -0
- package/engine/compile-corpus.mjs +6 -2
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format.yaml +9 -7
- package/engine/content-html.mjs +3 -2
- package/engine/content-index.mjs +23 -0
- package/engine/content-links.mjs +8 -0
- package/engine/content-render.mjs +14 -2
- package/engine/content-scripts.mjs +44 -6
- package/engine/content-session.mjs +11 -7
- package/engine/content-spans.mjs +3 -4
- package/engine/content-tables.mjs +12 -5
- package/engine/content-verse.mjs +52 -10
- package/engine/core-report.mjs +27 -18
- package/engine/culture-reach.mjs +3 -0
- package/engine/declared-sections.mjs +100 -0
- package/engine/derived-sections.mjs +36 -2
- package/engine/dictionary-rows.mjs +2 -1
- package/engine/generated-sections.mjs +2 -0
- package/engine/holdings.mjs +107 -3
- package/engine/index-records.mjs +3 -1
- package/engine/index.mjs +1 -1
- package/engine/language-data.mjs +0 -1
- package/engine/language-names.mjs +53 -23
- package/engine/language-rows.mjs +1 -3
- package/engine/language-terms.mjs +7 -10
- package/engine/map-dot.mjs +22 -0
- package/engine/markdown-blocks.mjs +23 -8
- package/engine/markdownlint-run.mjs +1 -1
- package/engine/markup-validation.mjs +2 -0
- package/engine/metadata-index.mjs +1 -0
- package/engine/note-addresses.mjs +16 -0
- package/engine/note-vocabulary.mjs +29 -13
- package/engine/npc-generate.mjs +52 -7
- package/engine/office-holders.mjs +76 -0
- package/engine/page-breaks.mjs +143 -0
- package/engine/pdf-build.mjs +12 -4
- package/engine/pdf-render.mjs +12 -2
- package/engine/place-kinds.mjs +106 -0
- package/engine/preview-index.mjs +4 -2
- package/engine/settlement-tables.mjs +410 -675
- package/engine/settlement-terms.mjs +118 -0
- package/engine/site-build.mjs +14 -7
- package/engine/site-preview.mjs +3 -0
- package/engine/site-render.mjs +18 -4
- package/engine/site-root.mjs +10 -1
- package/engine/site-serve-http.mjs +151 -0
- package/engine/site-serve.mjs +196 -126
- package/engine/sql-tables.mjs +101 -7
- package/engine/standing-terms.mjs +9 -5
- package/engine/standings.mjs +83 -31
- package/engine/unique-items.mjs +85 -0
- package/engine/verse-metre.mjs +50 -5
- package/engine/verse-regions.mjs +16 -0
- package/package.json +9 -4
- package/site-theme/data/banner-kinds.yaml +2 -0
- package/sohl/actors.mjs +4 -3
- package/types/e2e.d.mts +1 -1
- package/types/engine/content-html.d.mts +2 -1
- package/types/engine/content-scripts.d.mts +9 -1
- package/types/engine/content-verse.d.mts +11 -2
- package/types/engine/core-report.d.mts +6 -2
- package/types/engine/culture-reach.d.mts +3 -0
- package/types/engine/declared-sections.d.mts +30 -0
- package/types/engine/derived-sections.d.mts +5 -1
- package/types/engine/holdings.d.mts +34 -2
- package/types/engine/index-records.d.mts +2 -1
- package/types/engine/language-names.d.mts +18 -6
- package/types/engine/language-terms.d.mts +5 -1
- package/types/engine/markdown-blocks.d.mts +5 -1
- package/types/engine/npc-generate.d.mts +3 -0
- package/types/engine/office-holders.d.mts +33 -0
- package/types/engine/page-breaks.d.mts +38 -0
- package/types/engine/pdf-render.d.mts +2 -0
- package/types/engine/place-kinds.d.mts +18 -0
- package/types/engine/settlement-tables.d.mts +25 -158
- package/types/engine/settlement-terms.d.mts +53 -0
- package/types/engine/site-build.d.mts +3 -1
- package/types/engine/site-render.d.mts +6 -2
- package/types/engine/site-root.d.mts +3 -1
- package/types/engine/site-serve-http.d.mts +1 -0
- package/types/engine/site-serve.d.mts +12 -6
- package/types/engine/sql-tables.d.mts +35 -3
- package/types/engine/standing-terms.d.mts +3 -2
- package/types/engine/standings.d.mts +14 -2
- package/types/engine/unique-items.d.mts +12 -0
- package/types/engine/verse-metre.d.mts +2 -1
- package/types/engine/verse-regions.d.mts +8 -0
- package/vendor/markdownlint.mjs +855 -569
package/engine/standings.mjs
CHANGED
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
* **`rank` is a number, not a link.** The body declares `{level, title,
|
|
32
32
|
* description}` for every rung of its `governance.ranks`, so a number indexes
|
|
33
33
|
* into that ladder and the body stays the single source for what its level 5 is
|
|
34
|
-
* called. **`office` is a string
|
|
34
|
+
* called. **`office` is a string, or a list of them** for one being holding
|
|
35
|
+
* several posts in one body, each matched against the keys of the body's
|
|
35
36
|
* `governance.offices` map: there are far more offices in a tree than bodies,
|
|
36
37
|
* most of them held by one body alone, and an Address would make writing the
|
|
37
38
|
* fact wait on authoring a page about the post.
|
|
@@ -55,7 +56,7 @@ import { acceptsType, isAddressTuple, parseAddress, renderAddress } from "./addr
|
|
|
55
56
|
import { AddressEntries } from "./address-values.mjs";
|
|
56
57
|
import { slugify } from "./content-slug.mjs";
|
|
57
58
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
58
|
-
import { fitsKind } from "./data-keys.mjs";
|
|
59
|
+
import { fitsKind, pathLabel } from "./data-keys.mjs";
|
|
59
60
|
import { STANDING_BODY_TYPES, STANDING_FIELDS, STANDING_KEYS } from "./standing-terms.mjs";
|
|
60
61
|
|
|
61
62
|
export { STANDING_BODY_TYPES, STANDING_FIELDS, STANDING_KEYS };
|
|
@@ -221,12 +222,45 @@ export function standingsDigest(fm) {
|
|
|
221
222
|
};
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
/**
|
|
226
|
+
* The posts one standing names, as written: one post, or each post of a list.
|
|
227
|
+
*
|
|
228
|
+
* Every entry is kept, blank or not text included, so the check can locate
|
|
229
|
+
* each one; a reader of the posts themselves takes {@link standingOffices}.
|
|
230
|
+
*
|
|
231
|
+
* @param {unknown} office - A standing's authored `office`.
|
|
232
|
+
* @returns {Array<{value: unknown, at: Array<number>}>} Each post with its
|
|
233
|
+
* position below `office` — `[]` for the single form, `[i]` for a list entry.
|
|
234
|
+
*/
|
|
235
|
+
function writtenOffices(office) {
|
|
236
|
+
if (office === undefined || office === null) return [];
|
|
237
|
+
if (Array.isArray(office)) return office.map((value, i) => ({ value, at: [i] }));
|
|
238
|
+
return [{ value: office, at: [] }];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The posts a standing holds, trimmed, in the order written.
|
|
243
|
+
*
|
|
244
|
+
* `office` is one post or a list of them, and every reader — the line on the
|
|
245
|
+
* being's page, the SoHL item, the derived holders of an office — wants the
|
|
246
|
+
* same list from both.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} [standing] - `{rank, office}`, either or both absent.
|
|
249
|
+
* @returns {string[]} The posts, blank and non-text entries left out.
|
|
250
|
+
*/
|
|
251
|
+
export function standingOffices(standing) {
|
|
252
|
+
return writtenOffices(standing?.office)
|
|
253
|
+
.map(({ value }) => (typeof value === "string" ? value.trim() : ""))
|
|
254
|
+
.filter(Boolean);
|
|
255
|
+
}
|
|
256
|
+
|
|
224
257
|
/**
|
|
225
258
|
* What a standing says before the body that confers it — `War Chief, Hárár
|
|
226
259
|
* (5), of ` — or `""` where no standing is held.
|
|
227
260
|
*
|
|
228
261
|
* The office leads, because it is the more particular of the two: a reader
|
|
229
|
-
* meeting "War Chief" has been told the rung as well.
|
|
262
|
+
* meeting "War Chief" has been told the rung as well. Several posts in one
|
|
263
|
+
* body lead in the order written, `Treasurer, Chancellor, …`. The rung follows as its
|
|
230
264
|
* title with its level in parentheses, read from the body's own ladder; where
|
|
231
265
|
* the ladder names no rung at that level, or none is available, it reads as
|
|
232
266
|
* `Rank 5` and the number is not repeated. The body closes the phrase, which is
|
|
@@ -241,9 +275,7 @@ export function standingsDigest(fm) {
|
|
|
241
275
|
* @returns {string} The words before the body's name, ending `of `.
|
|
242
276
|
*/
|
|
243
277
|
export function standingLead(standing, digest) {
|
|
244
|
-
const parts = [];
|
|
245
|
-
if (typeof standing?.office === "string" && standing.office.trim())
|
|
246
|
-
parts.push(standing.office.trim());
|
|
278
|
+
const parts = [...standingOffices(standing)];
|
|
247
279
|
const rank = standing?.rank;
|
|
248
280
|
const level = typeof rank === "number" || typeof rank === "string" ? Number(rank) : NaN;
|
|
249
281
|
if (rank !== "" && Number.isFinite(level)) {
|
|
@@ -264,7 +296,7 @@ export function standingLead(standing, digest) {
|
|
|
264
296
|
* 3. the entry states a standing at all — what a stated standing holds is the
|
|
265
297
|
* inner-key check's, read from {@link STANDING_FIELDS};
|
|
266
298
|
* 4. a `rank` is a level the named body's `governance.ranks` declares;
|
|
267
|
-
* 5. an `office` is a key of that body's `governance.offices`.
|
|
299
|
+
* 5. each post an `office` names is a key of that body's `governance.offices`.
|
|
268
300
|
*
|
|
269
301
|
* Both of the last two are read from the named body's own frontmatter as the
|
|
270
302
|
* lint runs, so a rung or a post that does not exist fails on every being pointing
|
|
@@ -474,39 +506,59 @@ function rankFindings(note, standing, body, path, resolved) {
|
|
|
474
506
|
];
|
|
475
507
|
}
|
|
476
508
|
|
|
477
|
-
/**
|
|
509
|
+
/**
|
|
510
|
+
* Whether each post an `office` names is one the named body names.
|
|
511
|
+
*
|
|
512
|
+
* `office` is one post or a list of them, and each is checked on its own and
|
|
513
|
+
* located at its own value, so a list naming one unknown post among several is
|
|
514
|
+
* one finding at that post.
|
|
515
|
+
*/
|
|
478
516
|
function officeFindings(note, standing, body, path, resolved) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
517
|
+
const roster = resolved.fm ? officeRoster(resolved.fm.data?.governance?.offices) : undefined;
|
|
518
|
+
const findings = [];
|
|
519
|
+
for (const { value, at: below } of writtenOffices(standing.office)) {
|
|
520
|
+
if (typeof value !== "string") {
|
|
521
|
+
// A list entry that is not text is the inner-key check's finding,
|
|
522
|
+
// read from the declaration's `entries`. The single form is one
|
|
523
|
+
// post too, and the inner-key check reads only a list's entries.
|
|
524
|
+
if (!below.length)
|
|
525
|
+
findings.push({
|
|
526
|
+
...position(note, [...path, "office"]),
|
|
527
|
+
message:
|
|
528
|
+
`\`${pathLabel(["data", "affiliations", ...path.slice(2), "office"])}\` ` +
|
|
529
|
+
`should be ${STANDING_FIELDS.find((f) => f.name === "office").shape}, ` +
|
|
530
|
+
`but reads ${JSON.stringify(value)}`,
|
|
531
|
+
});
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const at = () => position(note, [...path, "office", ...below]);
|
|
535
|
+
if (!value.trim()) {
|
|
536
|
+
findings.push({
|
|
486
537
|
...at(),
|
|
487
538
|
message:
|
|
488
539
|
`\`data.affiliations\` entry for ${body}: \`office\` must name a post from its ` +
|
|
489
|
-
`\`governance.offices\`, but reads ${JSON.stringify(
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return [
|
|
497
|
-
{
|
|
540
|
+
`\`governance.offices\`, but reads ${JSON.stringify(value)}`,
|
|
541
|
+
});
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (!roster || roster.has(value)) continue;
|
|
545
|
+
if (!roster.size) {
|
|
546
|
+
findings.push({
|
|
498
547
|
...at(),
|
|
499
548
|
message:
|
|
500
549
|
`\`data.affiliations\` entry for ${body} names an office, and that body ` +
|
|
501
550
|
`${resolved.address} declares no \`data.governance.offices\` to hold one`,
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
551
|
+
});
|
|
552
|
+
// One finding says the body holds no posts; each further post
|
|
553
|
+
// would repeat it.
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
findings.push({
|
|
506
557
|
...at(),
|
|
507
558
|
message:
|
|
508
|
-
`\`data.affiliations\` entry for ${body}: office ${JSON.stringify(
|
|
559
|
+
`\`data.affiliations\` entry for ${body}: office ${JSON.stringify(value)} is not ` +
|
|
509
560
|
`an office ${resolved.address} names; it holds ${[...roster.keys()].join(", ")}`,
|
|
510
|
-
}
|
|
511
|
-
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return findings;
|
|
512
564
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **A one-of-a-kind item is tied to the places and beings involved.**
|
|
16
|
+
*
|
|
17
|
+
* `data.unique: true` on a gear note marks an item there is only one of — a
|
|
18
|
+
* named hammer, a crown, a sword in a tomb. Where it lies and who claims it are
|
|
19
|
+
* prose, because possession changes with play and ownership is a claim that
|
|
20
|
+
* can be disputed; so the note's own links are what tie it to the world, and a
|
|
21
|
+
* unique item whose note links no place and no being is a finding.
|
|
22
|
+
*
|
|
23
|
+
* A link counts wherever the note writes it: a wikilink in its body, or a
|
|
24
|
+
* `made` or `lost` event Address naming a place's or a being's event.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { isAddressTuple, parseAddress } from "./address.mjs";
|
|
30
|
+
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
31
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
32
|
+
import { parseWikilink, WIKILINK } from "./wikilink-syntax.mjs";
|
|
33
|
+
|
|
34
|
+
/** The note types a unique item's links must reach at least one of. */
|
|
35
|
+
export const UNIQUE_ITEM_LINK_TYPES = Object.freeze(["place", "being"]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The note type one written target names, or `""` where it is no Address.
|
|
39
|
+
*
|
|
40
|
+
* @param {unknown} target - A wikilink target or an authored Address.
|
|
41
|
+
* @param {object} [index] - The lint's address index.
|
|
42
|
+
* @returns {string} The type.
|
|
43
|
+
*/
|
|
44
|
+
function typeOf(target, index) {
|
|
45
|
+
if (isAddressTuple(target)) return target.type;
|
|
46
|
+
// An event Address arrives decoded as a link to its note and anchor.
|
|
47
|
+
if (isAddressTuple(target?.target)) return target.target.type;
|
|
48
|
+
const tuple = parseAddress(String(target ?? "").split("#")[0], {
|
|
49
|
+
package: index?.contentPackage,
|
|
50
|
+
system: "note",
|
|
51
|
+
types: index?.types,
|
|
52
|
+
packages: index?.packages,
|
|
53
|
+
});
|
|
54
|
+
return tuple.reason ? "" : String(tuple.type ?? "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Report a unique item whose note links no place or being.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} note - The note: `file`, `raw`, `body` and parsed `fm`.
|
|
61
|
+
* @param {{index?: object}} [options] - The lint's options.
|
|
62
|
+
* @returns {object[]} The finding, located at `unique`, or none.
|
|
63
|
+
*/
|
|
64
|
+
export function checkUniqueItem(note, { index } = {}) {
|
|
65
|
+
const data = note.fm?.data;
|
|
66
|
+
if (data?.unique !== true && data?.unique !== "true") return [];
|
|
67
|
+
const targets = [
|
|
68
|
+
data.made,
|
|
69
|
+
data.lost,
|
|
70
|
+
...matchAllOutsideCode(note.body ?? "", new RegExp(WIKILINK.source, "g")).map(
|
|
71
|
+
([, inner]) => parseWikilink(inner).target,
|
|
72
|
+
),
|
|
73
|
+
].filter(Boolean);
|
|
74
|
+
if (targets.some((target) => UNIQUE_ITEM_LINK_TYPES.includes(typeOf(target, index)))) return [];
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
file: note.file,
|
|
78
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "unique"], { key: true }),
|
|
79
|
+
severity: "error",
|
|
80
|
+
message:
|
|
81
|
+
"a unique item links no place or being; link where it lies or who " +
|
|
82
|
+
"holds or claims it in the note's prose",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
package/engine/verse-metre.mjs
CHANGED
|
@@ -62,8 +62,48 @@ import MarkdownIt from "markdown-it";
|
|
|
62
62
|
import { spanMarkdownPlugin } from "./content-spans.mjs";
|
|
63
63
|
import { parseVerse } from "./content-verse.mjs";
|
|
64
64
|
import { inlineText } from "./name-markup.mjs";
|
|
65
|
+
import { WIKILINK, authoredLabel, parseWikilink } from "./wikilink-syntax.mjs";
|
|
65
66
|
|
|
66
67
|
const verseParser = new MarkdownIt().use(spanMarkdownPlugin);
|
|
68
|
+
const wikilinkAt = new RegExp(WIKILINK.source, "y");
|
|
69
|
+
|
|
70
|
+
// Parse link labels in place so neighbouring spans keep their source offsets.
|
|
71
|
+
verseParser.inline.ruler.before("link", "metre_wikilink", (state, silent) => {
|
|
72
|
+
const start = state.pos;
|
|
73
|
+
wikilinkAt.lastIndex = start;
|
|
74
|
+
const match = wikilinkAt.exec(state.src);
|
|
75
|
+
if (!match || start + match[0].length > state.posMax) return false;
|
|
76
|
+
const finish = start + match[0].length;
|
|
77
|
+
if (silent) {
|
|
78
|
+
state.pos = finish;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const parsed = parseWikilink(match[1], match[0]);
|
|
82
|
+
const label = authoredLabel(parsed) ?? state.env.referenceName?.(parsed.target);
|
|
83
|
+
const token = state.push("span_open", "span", 1);
|
|
84
|
+
token.meta = {
|
|
85
|
+
span: true,
|
|
86
|
+
start,
|
|
87
|
+
end: finish,
|
|
88
|
+
unresolvedReference: !parsed.labelled || label === undefined || label === "",
|
|
89
|
+
};
|
|
90
|
+
if (parsed.attributes?.classes.length)
|
|
91
|
+
token.attrSet("class", parsed.attributes.classes.join(" "));
|
|
92
|
+
for (const [key, value] of Object.entries(parsed.attributes?.values ?? {}))
|
|
93
|
+
token.attrSet(key, value);
|
|
94
|
+
const nested = [];
|
|
95
|
+
state.md.inline.parse(label ?? "", state.md, state.env, nested);
|
|
96
|
+
const labelStart = start + match[0].indexOf("|") + 1;
|
|
97
|
+
for (const child of nested)
|
|
98
|
+
if (child.meta?.span) {
|
|
99
|
+
child.meta.start += labelStart;
|
|
100
|
+
child.meta.end += labelStart;
|
|
101
|
+
}
|
|
102
|
+
state.tokens.push(...nested);
|
|
103
|
+
state.push("span_close", "span", -1);
|
|
104
|
+
state.pos = finish;
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
67
107
|
|
|
68
108
|
export { METRE_KINDS, METRE_NAME, checkMetresField } from "./metre-declaration.mjs";
|
|
69
109
|
|
|
@@ -240,11 +280,12 @@ function wordBeats(word, book) {
|
|
|
240
280
|
const folded = foldWord(word);
|
|
241
281
|
const count = nucleiOf(folded, book.pairs).length;
|
|
242
282
|
if (count === 0) return { min: 0, max: 0 };
|
|
283
|
+
const bases = book.classes.get(folded);
|
|
243
284
|
if (count > 1) {
|
|
285
|
+
const functionWord = bases && [...bases].every((base) => !BEAT_BASES.includes(base));
|
|
244
286
|
const first = stressIndex(folded, book.rule, book.pairs) === 0;
|
|
245
|
-
return { min: 1, max: count >= 4 && !first ? 2 : 1 };
|
|
287
|
+
return { min: functionWord ? 0 : 1, max: count >= 4 && !first ? 2 : 1 };
|
|
246
288
|
}
|
|
247
|
-
const bases = book.classes.get(folded);
|
|
248
289
|
if (!bases) return { min: 1, max: 1 };
|
|
249
290
|
const carrying = [...bases].filter((base) => BEAT_BASES.includes(base)).length;
|
|
250
291
|
if (carrying === bases.size) return { min: 1, max: 1 };
|
|
@@ -292,7 +333,7 @@ export function metreMessage({ min, max }, metre) {
|
|
|
292
333
|
* @param {ReadonlyArray<{start: number, end: number, indent: string, attributes: Record<string, string>, classes: string[]}>} blocks -
|
|
293
334
|
* The divs, from {@link module:engine/content-verse.scanVerse}.
|
|
294
335
|
* @param {string[]} lines - The source's lines.
|
|
295
|
-
* @param {{languages?: Readonly<Record<string, {tag: string}>>, metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>, metreBooks?: ReadonlyMap<string, MetreBook>, lexicons?: ReadonlyMap<string, unknown>, attributeColumns: (line: string, key: string) => {key: number, value: number}}} ctx -
|
|
336
|
+
* @param {{languages?: Readonly<Record<string, {tag: string}>>, metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>, metreBooks?: ReadonlyMap<string, MetreBook>, lexicons?: ReadonlyMap<string, unknown>, referenceName?: (target: string) => string|undefined, attributeColumns: (line: string, key: string) => {key: number, value: number}}} ctx -
|
|
296
337
|
* The metres and books, and how the div says its columns and where its
|
|
297
338
|
* keys sit.
|
|
298
339
|
* @returns {{errors: object[], warnings: object[]}} The findings at 1-based
|
|
@@ -358,9 +399,12 @@ export function metreFindings(blocks, lines, ctx) {
|
|
|
358
399
|
({ offset }) =>
|
|
359
400
|
!model.layout.some(([from, to]) => offset >= from && offset < to),
|
|
360
401
|
);
|
|
402
|
+
const inlineContext = { referenceName: ctx.referenceName };
|
|
361
403
|
const parsed =
|
|
362
|
-
verseParser.parseInline(
|
|
363
|
-
|
|
404
|
+
verseParser.parseInline(
|
|
405
|
+
units.map((unit) => unit.text).join(""),
|
|
406
|
+
inlineContext,
|
|
407
|
+
)[0]?.children ?? [];
|
|
364
408
|
let presentationDepth = 0;
|
|
365
409
|
const tokens = parsed.filter((token) => {
|
|
366
410
|
if (presentationDepth) {
|
|
@@ -395,6 +439,7 @@ export function metreFindings(blocks, lines, ctx) {
|
|
|
395
439
|
);
|
|
396
440
|
continue;
|
|
397
441
|
}
|
|
442
|
+
if (tokens.some((token) => token.meta?.unresolvedReference)) continue;
|
|
398
443
|
const beats = lineBeats(text, book);
|
|
399
444
|
if (metre.beats >= beats.min && metre.beats <= beats.max) continue;
|
|
400
445
|
warnings.push({
|
package/engine/verse-regions.mjs
CHANGED
|
@@ -26,6 +26,8 @@ import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
|
26
26
|
* @property {number} bodyStart - First body offset.
|
|
27
27
|
* @property {number} bodyEnd - Offset past body text, excluding its final newline.
|
|
28
28
|
* @property {string} body - Authored body bytes.
|
|
29
|
+
* @property {string} logicalBody - Body without Markdown container prefixes.
|
|
30
|
+
* @property {number[]} bodyOffsets - Logical UTF-16 offsets mapped to authored body offsets, including the end.
|
|
29
31
|
* @property {Record<string,string>} attributes - Authored values.
|
|
30
32
|
* @property {string} id - Anchor or empty string.
|
|
31
33
|
* @property {string[]} classes - Authored classes including verse.
|
|
@@ -95,6 +97,18 @@ export function verseDivPlugin(md) {
|
|
|
95
97
|
if (original[bodyEnd - 1] === "\n") bodyEnd--;
|
|
96
98
|
if (original[bodyEnd - 1] === "\r") bodyEnd--;
|
|
97
99
|
}
|
|
100
|
+
const bodyOffsets = [];
|
|
101
|
+
let logicalBody = "";
|
|
102
|
+
for (let number = first + 1; number < (closed ? close : past); number++) {
|
|
103
|
+
const lineEnd = Math.min(starts[number] + rawLines[number].length, bodyEnd);
|
|
104
|
+
const contentStart = Math.min(contentStarts[number - first], lineEnd);
|
|
105
|
+
const pastLine = Math.min(starts[number + 1] ?? bodyEnd, bodyEnd);
|
|
106
|
+
for (let offset = contentStart; offset < pastLine; offset++) {
|
|
107
|
+
logicalBody += original[offset];
|
|
108
|
+
bodyOffsets.push(offset - bodyStart);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
bodyOffsets.push(bodyEnd - bodyStart);
|
|
98
112
|
const region = {
|
|
99
113
|
start: first,
|
|
100
114
|
end: closed ? close : past - 1,
|
|
@@ -104,6 +118,8 @@ export function verseDivPlugin(md) {
|
|
|
104
118
|
bodyStart,
|
|
105
119
|
bodyEnd,
|
|
106
120
|
body: original.slice(bodyStart, bodyEnd),
|
|
121
|
+
logicalBody,
|
|
122
|
+
bodyOffsets,
|
|
107
123
|
attributes: parsed.values,
|
|
108
124
|
id: parsed.id,
|
|
109
125
|
classes: parsed.classes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.6.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"eslint-plugin-yml": "^3.8.1",
|
|
158
158
|
"fflate": "^0.8.3",
|
|
159
159
|
"flesch": "^2.0.1",
|
|
160
|
-
"glob": "^
|
|
160
|
+
"glob": "^14.0.3",
|
|
161
161
|
"handlebars": "^4.7.10",
|
|
162
162
|
"highlight.js": "^11.12.0",
|
|
163
163
|
"ignore": "^7.0.12",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"markdown-it": "^15.0.0",
|
|
169
169
|
"markdown-it-deflist": "^4.0.0",
|
|
170
170
|
"markdown-it-footnote": "^4.0.0",
|
|
171
|
-
"minimatch": "^
|
|
171
|
+
"minimatch": "^11.0.3",
|
|
172
172
|
"pagefind": "^1.5.2",
|
|
173
173
|
"prettier": "^3.9.6",
|
|
174
174
|
"remark-frontmatter": "^5.0.0",
|
|
@@ -243,5 +243,10 @@
|
|
|
243
243
|
"sohl",
|
|
244
244
|
"song-of-heroic-lands"
|
|
245
245
|
],
|
|
246
|
-
"homepage": "https://github.com/HeroicLands/package-build#documentation"
|
|
246
|
+
"homepage": "https://github.com/HeroicLands/package-build#documentation",
|
|
247
|
+
"overrides": {
|
|
248
|
+
"minimatch": "$minimatch",
|
|
249
|
+
"katex": "0.18.2",
|
|
250
|
+
"re2js": "2.8.7"
|
|
251
|
+
}
|
|
247
252
|
}
|
package/sohl/actors.mjs
CHANGED
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
* **A membership is derived, never authored here.** `data.affiliations` names
|
|
43
43
|
* the bodies a being belongs to and the standing it holds in each, and each
|
|
44
44
|
* entry becomes one embedded affiliation item: the entry's `rank` is that
|
|
45
|
-
* item's `system.level`, its `office` is `system.office
|
|
45
|
+
* item's `system.level`, its `office` is `system.office` (several posts joined
|
|
46
|
+
* by commas), and the first of its
|
|
46
47
|
* `societies` is `system.society`, the one society the SoHL item carries. A
|
|
47
48
|
* standing is a fact about the setting rather than about one system — it does
|
|
48
49
|
* not become true because SoHL is installed — so the system block states none
|
|
@@ -76,7 +77,7 @@ import { blockProperty, mergeSystemData } from "../engine/system-block.mjs";
|
|
|
76
77
|
// A being's memberships and the standing it holds in each, read from the one
|
|
77
78
|
// place they are authored. The rank *is* the affiliation item's `level`, so
|
|
78
79
|
// there is nothing to map and nowhere for a second authoring to disagree.
|
|
79
|
-
import { readStandings } from "../engine/standings.mjs";
|
|
80
|
+
import { readStandings, standingOffices } from "../engine/standings.mjs";
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* The system this pass compiles for — the block its notes write.
|
|
@@ -234,7 +235,7 @@ export class Actors extends SystemActorCompiler {
|
|
|
234
235
|
const shortcode = body?.shortcode;
|
|
235
236
|
if (!shortcode) continue;
|
|
236
237
|
const level = Number(standing?.rank);
|
|
237
|
-
const office =
|
|
238
|
+
const office = standingOffices(standing).join(", ");
|
|
238
239
|
const [first] = Array.isArray(standing?.societies) ? standing.societies : [];
|
|
239
240
|
const society = typeof first === "string" ? first.trim() : "";
|
|
240
241
|
const system = {
|
package/types/e2e.d.mts
CHANGED
|
@@ -494,7 +494,7 @@ export const E2E_SCENE_ID: "heroiclandsScene";
|
|
|
494
494
|
/** How the suite may be run. */
|
|
495
495
|
export const E2E_MODES: readonly string[];
|
|
496
496
|
/** The Chrome for Testing build shared by local and CI Cypress runs. */
|
|
497
|
-
export const CHROME_BUILD_ID: "
|
|
497
|
+
export const CHROME_BUILD_ID: "159.0.8136.0";
|
|
498
498
|
/**
|
|
499
499
|
* The disposable world's identity and credentials.
|
|
500
500
|
*/
|
|
@@ -74,7 +74,8 @@ export function lintContentHtml(contentBase: string, { skipDirectories, sources
|
|
|
74
74
|
* markdown's own angle brackets out of it: an autolink is `<https://…>`, and
|
|
75
75
|
* `https` is followed by `:` rather than whitespace or `>`, so the pattern
|
|
76
76
|
* stops. A comparison written in prose — `a < b` — has no name after the
|
|
77
|
-
* bracket at all.
|
|
77
|
+
* bracket at all. A doubled `<<` opens a quotation, so its second bracket
|
|
78
|
+
* never starts an HTML tag.
|
|
78
79
|
*
|
|
79
80
|
* Attributes are consumed as "anything but a bracket", deliberately loosely: a
|
|
80
81
|
* finding names the tag, and a pattern that tried to parse attribute syntax
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* resolved configuration.
|
|
6
6
|
* @param {Iterable<Record<string, any>>} [records] - The content index's
|
|
7
7
|
* records, whose language notes supply lexical writings and script choices.
|
|
8
|
+
* @param {{referenceName?: (target: string) => string|undefined}} [options] -
|
|
9
|
+
* The rendering or validation index's own target-name lookup.
|
|
8
10
|
* @returns {RunContext} Its languages, the faces installed where it sits, and
|
|
9
11
|
* the encodings. A fonts package that cannot be read gives no faces; a run naming
|
|
10
12
|
* one is reported.
|
|
@@ -16,7 +18,9 @@ export function runContext(config: {
|
|
|
16
18
|
paths?: {
|
|
17
19
|
content?: string;
|
|
18
20
|
};
|
|
19
|
-
} | undefined, records?: Iterable<Record<string, any
|
|
21
|
+
} | undefined, records?: Iterable<Record<string, any>>, { referenceName: indexedName }?: {
|
|
22
|
+
referenceName?: (target: string) => string | undefined;
|
|
23
|
+
}): RunContext;
|
|
20
24
|
/**
|
|
21
25
|
* Find the spans, divs and verse divs marked with `lang=`, `font=` or
|
|
22
26
|
* `enc=`, and every fault in their markup and their text.
|
|
@@ -326,6 +330,10 @@ export type RunContext = {
|
|
|
326
330
|
* {@link module :engine/verse-metre.metreBooksFromRecords}.
|
|
327
331
|
*/
|
|
328
332
|
metreBooks?: ReadonlyMap<string, import("./verse-metre.mjs").MetreBook> | undefined;
|
|
333
|
+
/**
|
|
334
|
+
* - An indexed wikilink target's visible name.
|
|
335
|
+
*/
|
|
336
|
+
referenceName?: ((target: string) => string | undefined) | undefined;
|
|
329
337
|
/**
|
|
330
338
|
* -
|
|
331
339
|
* Exact Roman source inventories by configured alias and BCP 47 tag.
|
|
@@ -37,12 +37,13 @@ export function scanVerse(source: string, { checkWorks }?: {
|
|
|
37
37
|
* Offsets are UTF-16 positions relative to body. Delimiters and indentation are
|
|
38
38
|
* layout ranges; source newlines inside a logical line contribute one space.
|
|
39
39
|
* @param {string} body - Authored verse body.
|
|
40
|
-
* @param {{inline?: boolean, footnotes?: ReadonlySet<string
|
|
40
|
+
* @param {{inline?: boolean, footnotes?: ReadonlySet<string>, sourceOffsets?: number[]}} [options] - Inline quotations have no cantos; footnotes are same-note definition labels; sourceOffsets maps projected body positions to authored offsets, including the end.
|
|
41
41
|
* @returns {VerseModel} The shared structure with offsets relative to the body.
|
|
42
42
|
*/
|
|
43
|
-
export function parseVerse(body: string, { inline, footnotes }?: {
|
|
43
|
+
export function parseVerse(body: string, { inline, footnotes, sourceOffsets }?: {
|
|
44
44
|
inline?: boolean;
|
|
45
45
|
footnotes?: ReadonlySet<string>;
|
|
46
|
+
sourceOffsets?: number[];
|
|
46
47
|
}): VerseModel;
|
|
47
48
|
/**
|
|
48
49
|
* Validate a quoted verse span using its original source coordinates.
|
|
@@ -271,6 +272,14 @@ export type VerseBlock = {
|
|
|
271
272
|
* - Authored verse body.
|
|
272
273
|
*/
|
|
273
274
|
body: string;
|
|
275
|
+
/**
|
|
276
|
+
* - Body without Markdown container prefixes.
|
|
277
|
+
*/
|
|
278
|
+
logicalBody: string;
|
|
279
|
+
/**
|
|
280
|
+
* - Logical UTF-16 offsets mapped to authored body offsets, including the end.
|
|
281
|
+
*/
|
|
282
|
+
bodyOffsets: number[];
|
|
274
283
|
/**
|
|
275
284
|
* - Logical structure relative to the body.
|
|
276
285
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
5
5
|
* @param {{language?: string}} [options] - A language to report alone, by its
|
|
6
6
|
* name, shortcode or reference slug whether or not it states `data.core`.
|
|
7
|
-
* @returns {{concepts: number, languages: Array<{name: string, language: string, core: boolean, total: number, covered: number, missing: Array<{group: string, count: number, total: number, concepts: Array<{id: string, gloss: string}>}>}>}|{problem: string, about: string}}
|
|
7
|
+
* @returns {{concepts: number, notCovered: string[], languages: Array<{name: string, language: string, core: boolean, total: number, covered: number, missing: Array<{group: string, count: number, total: number, concepts: Array<{id: string, gloss: string}>}>}>}|{problem: string, about: string}}
|
|
8
8
|
* The report, or the problem when there is no concept list or the language
|
|
9
9
|
* asked for is none.
|
|
10
10
|
*/
|
|
@@ -12,6 +12,7 @@ export function coreReport(records: Iterable<Record<string, any>>, { language }?
|
|
|
12
12
|
language?: string;
|
|
13
13
|
}): {
|
|
14
14
|
concepts: number;
|
|
15
|
+
notCovered: string[];
|
|
15
16
|
languages: Array<{
|
|
16
17
|
name: string;
|
|
17
18
|
language: string;
|
|
@@ -36,10 +37,13 @@ export function coreReport(records: Iterable<Record<string, any>>, { language }?
|
|
|
36
37
|
* A core report as text: one block per language, its missing concepts under
|
|
37
38
|
* their groups with counts, each concept's id and gloss tab-separated.
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
+
* The default report ends with the names of languages it does not measure.
|
|
41
|
+
*
|
|
42
|
+
* @param {{concepts: number, notCovered: string[], languages: Array<object>}} report - A report from {@link coreReport}.
|
|
40
43
|
* @returns {string} The text, ending in a newline.
|
|
41
44
|
*/
|
|
42
45
|
export function coreReportText(report: {
|
|
43
46
|
concepts: number;
|
|
47
|
+
notCovered: string[];
|
|
44
48
|
languages: Array<object>;
|
|
45
49
|
}): string;
|
|
@@ -42,6 +42,9 @@ export function noteTerms(index: object, note: object, entry?: object): Map<stri
|
|
|
42
42
|
* in a tree that describes peoples. Its ranks and offices then have no
|
|
43
43
|
* culture associations for term queries.
|
|
44
44
|
*
|
|
45
|
+
* The note's `data.culture` is the culture the content index resolved for it:
|
|
46
|
+
* its own, else the one its `data.parents` chain agrees on.
|
|
47
|
+
*
|
|
45
48
|
* @param {object} note - An `affiliation` note.
|
|
46
49
|
* @param {{index?: object}} [context]
|
|
47
50
|
* @returns {object[]} One warning, at the note's `type`, or none.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Authored headings, excluding literal examples and named containers. */
|
|
2
|
+
export function declaredHeadings(markdown: any, syntax: any): {
|
|
3
|
+
level: number;
|
|
4
|
+
line: number;
|
|
5
|
+
text: string;
|
|
6
|
+
id: string;
|
|
7
|
+
classes: string[];
|
|
8
|
+
values: Record<string, string>;
|
|
9
|
+
}[];
|
|
10
|
+
/**
|
|
11
|
+
* Insert indexed content at its heading while preserving subsequent source lines.
|
|
12
|
+
* @param {string} markdown - Expanded body.
|
|
13
|
+
* @param {Array<{slug:string,heading:string,markdown:string}>} [sections] - Declared content.
|
|
14
|
+
* @param {{authored?:string,lineMap?:object[],generatedFrom?:number,syntax?:Function}} [context] - Captured source mappings.
|
|
15
|
+
* @returns {{markdown:string,lineMap:object[],generatedFrom?:number}} Expanded body and locations.
|
|
16
|
+
*/
|
|
17
|
+
export function insertDeclaredSections(markdown: string, sections?: Array<{
|
|
18
|
+
slug: string;
|
|
19
|
+
heading: string;
|
|
20
|
+
markdown: string;
|
|
21
|
+
}>, { authored, lineMap, generatedFrom, syntax }?: {
|
|
22
|
+
authored?: string;
|
|
23
|
+
lineMap?: object[];
|
|
24
|
+
generatedFrom?: number;
|
|
25
|
+
syntax?: Function;
|
|
26
|
+
}): {
|
|
27
|
+
markdown: string;
|
|
28
|
+
lineMap: object[];
|
|
29
|
+
generatedFrom?: number;
|
|
30
|
+
};
|
|
@@ -51,15 +51,19 @@ export function drawMapsFrom(records: object[], { outDir, foreignIndex, config,
|
|
|
51
51
|
* prepared results are keyed.
|
|
52
52
|
* @param {Map<string, object>} [opts.foreignIndex] - The fetched indexes.
|
|
53
53
|
* @param {object} [opts.config] - The resolved configuration.
|
|
54
|
+
* @param {(file:string)=>string|undefined} [opts.rawOf] - Captured source locators.
|
|
55
|
+
* @param {object[]} [opts.all=records] - Complete records for settlement validation.
|
|
54
56
|
* @param {string} [opts.only] - One note file to give sections to; absent,
|
|
55
57
|
* every note.
|
|
56
58
|
* @returns {Map<string, Map<string, string>>} Note file to its sections, each
|
|
57
59
|
* keyed by its slug, for every note given at least one.
|
|
58
60
|
*/
|
|
59
|
-
export function derivedSections(records: object[], { fileOf, foreignIndex, config, only }: {
|
|
61
|
+
export function derivedSections(records: object[], { fileOf, foreignIndex, config, only, rawOf, all }: {
|
|
60
62
|
fileOf: (record: object) => string;
|
|
61
63
|
foreignIndex?: Map<string, object> | undefined;
|
|
62
64
|
config?: object | undefined;
|
|
65
|
+
rawOf?: ((file: string) => string | undefined) | undefined;
|
|
66
|
+
all?: object[] | undefined;
|
|
63
67
|
only?: string | undefined;
|
|
64
68
|
}): Map<string, Map<string, string>>;
|
|
65
69
|
/**
|