@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
|
@@ -0,0 +1,358 @@
|
|
|
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
|
+
* Page lists: a tag in, the pages carrying it out.
|
|
16
|
+
*
|
|
17
|
+
* ````markdown
|
|
18
|
+
* ```pagelist {tag="key-concept" descriptions=true}
|
|
19
|
+
* ```
|
|
20
|
+
* ````
|
|
21
|
+
*
|
|
22
|
+
* **A group expressed by intent.** A hand-enumerated list of links drifts — a
|
|
23
|
+
* page is written and nobody adds it to the landing that should name it. A page
|
|
24
|
+
* list names the *tag* instead: tag a page and it files itself, untag it and it
|
|
25
|
+
* leaves.
|
|
26
|
+
*
|
|
27
|
+
* **It is a sibling of the `sql` content table, and answered the same way.**
|
|
28
|
+
* The pages are resolved over the content index ahead of the synchronous
|
|
29
|
+
* expansion pass, then
|
|
30
|
+
* {@link module:engine/content-tables.expandContentTables} splices the result
|
|
31
|
+
* in as ordinary Markdown — a bullet list of wikilinks. So each surface's own
|
|
32
|
+
* wikilink resolver turns them into what that surface links with: a compendium
|
|
33
|
+
* UUID in Foundry, a page URL on the web and in the book. One authored
|
|
34
|
+
* construct, three real lists.
|
|
35
|
+
*
|
|
36
|
+
* **Attributes, never a body.** Everything the directive says about itself is
|
|
37
|
+
* said in the braced attribute grammar every other body extension uses, so
|
|
38
|
+
* there is one grammar to learn and one validator to maintain. The fence holds
|
|
39
|
+
* nothing, and text written inside it is reported rather than ignored.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { FENCE_LINE, parseHeaderArgs } from "./code-fences.mjs";
|
|
45
|
+
import { booleanAttribute } from "./extension-attributes.mjs";
|
|
46
|
+
import { isNoteRecord } from "./index-records.mjs";
|
|
47
|
+
import { NOTE_VOCABULARY, hasTag } from "./note-vocabulary.mjs";
|
|
48
|
+
|
|
49
|
+
/** The fence language that opens a page list. */
|
|
50
|
+
export const PAGE_LIST_LANGUAGE = "pagelist";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The orders a page list may come out in.
|
|
54
|
+
*
|
|
55
|
+
* Ordering is about output rather than about the pages, so it is an attribute
|
|
56
|
+
* rather than something read off a note. `name` is alphabetical, case and
|
|
57
|
+
* accents folded; `type` groups the note types and orders by name inside each.
|
|
58
|
+
*
|
|
59
|
+
* @type {readonly string[]}
|
|
60
|
+
*/
|
|
61
|
+
export const PAGE_LIST_SORTS = Object.freeze(["name", "type"]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The attributes a page-list fence takes, with what each one is for.
|
|
65
|
+
*
|
|
66
|
+
* **This is the one list.** The fence is validated against it, the authoring
|
|
67
|
+
* document's table is generated from it, and the reference is checked against
|
|
68
|
+
* it — so an attribute added here is one both documents describe, and anything
|
|
69
|
+
* else an author writes is reported by name.
|
|
70
|
+
*
|
|
71
|
+
* @type {Readonly<Record<string, Readonly<{value: string, default: string, summary: string}>>>}
|
|
72
|
+
*/
|
|
73
|
+
export const PAGE_LIST_ATTRIBUTES = Object.freeze({
|
|
74
|
+
tag: Object.freeze({
|
|
75
|
+
value: "a tag",
|
|
76
|
+
default: "required",
|
|
77
|
+
summary:
|
|
78
|
+
"The tag a page carries to join the list. Matched the way `tags:` is read — " +
|
|
79
|
+
"case and a leading `#` are not significant, the spelling of the tag itself is.",
|
|
80
|
+
}),
|
|
81
|
+
type: Object.freeze({
|
|
82
|
+
value: "a note type",
|
|
83
|
+
default: "every type",
|
|
84
|
+
summary:
|
|
85
|
+
"Restricts the list to one note type. A page's address is `<type>-<shortcode>`, " +
|
|
86
|
+
"so the type is what divides the corpus into kinds of page.",
|
|
87
|
+
}),
|
|
88
|
+
sort: Object.freeze({
|
|
89
|
+
value: "`name` or `type`",
|
|
90
|
+
default: "`name`",
|
|
91
|
+
summary:
|
|
92
|
+
"The order the pages come out in. `name` is alphabetical; `type` groups the " +
|
|
93
|
+
"note types and orders by name within each.",
|
|
94
|
+
}),
|
|
95
|
+
descriptions: Object.freeze({
|
|
96
|
+
value: "`true` or `false`",
|
|
97
|
+
default: "`false`",
|
|
98
|
+
summary:
|
|
99
|
+
"Whether each page's `description` follows its link. A page that states none " +
|
|
100
|
+
"carries its link alone.",
|
|
101
|
+
}),
|
|
102
|
+
"allow-empty": Object.freeze({
|
|
103
|
+
value: "`true` or `false`",
|
|
104
|
+
default: "`false`",
|
|
105
|
+
summary:
|
|
106
|
+
"Whether a tag carried by no page is allowed. It is a finding otherwise, " +
|
|
107
|
+
"because a heading with nothing under it is how a misspelled tag goes unnoticed.",
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** The note types a `type` attribute may name, from the vocabulary itself. */
|
|
112
|
+
const noteTypes = () => Object.keys(NOTE_VOCABULARY);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Every `pagelist` fence in a markdown body, with the position each occupies.
|
|
116
|
+
*
|
|
117
|
+
* Positions are 0-based lines into the body as given, which is what a
|
|
118
|
+
* diagnostic about a directive needs and what the expander uses to splice
|
|
119
|
+
* results back in.
|
|
120
|
+
*
|
|
121
|
+
* **A malformed directive is still a directive.** It is returned carrying its
|
|
122
|
+
* problems rather than skipped, so the expander finds a prepared answer at its
|
|
123
|
+
* ordinal and reports the fault where the fence sits. That includes a fence
|
|
124
|
+
* nobody closed, which the author meant as a directive and which would
|
|
125
|
+
* otherwise swallow the rest of the note into a code block.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} markdown - The note body, frontmatter already stripped.
|
|
128
|
+
* @returns {Array<{line: number, close: number, indent: string, tag: string,
|
|
129
|
+
* type: string, sort: string, descriptions: boolean, allowEmpty: boolean,
|
|
130
|
+
* problems: string[], block: string}>} One entry per fence, in document order.
|
|
131
|
+
*/
|
|
132
|
+
export function findPageListBlocks(markdown) {
|
|
133
|
+
const lines = String(markdown ?? "").split("\n");
|
|
134
|
+
const blocks = [];
|
|
135
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
136
|
+
const opening = FENCE_LINE.exec(lines[i]);
|
|
137
|
+
if (!opening) continue;
|
|
138
|
+
const [, indent, marker, info] = opening;
|
|
139
|
+
const closer = new RegExp(`^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`);
|
|
140
|
+
let close = i + 1;
|
|
141
|
+
while (close < lines.length && !closer.test(lines[close])) close += 1;
|
|
142
|
+
const { language, args, problems } = parseHeaderArgs(info);
|
|
143
|
+
if (language !== PAGE_LIST_LANGUAGE) {
|
|
144
|
+
// Not ours, but still a fence: skip its body so a directive shown
|
|
145
|
+
// as an example inside a longer fence is never read as one.
|
|
146
|
+
i = close;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const errors = [...problems];
|
|
150
|
+
for (const key of Object.keys(args)) {
|
|
151
|
+
if (!Object.hasOwn(PAGE_LIST_ATTRIBUTES, key))
|
|
152
|
+
errors.push(`${key} is not a page-list attribute`);
|
|
153
|
+
}
|
|
154
|
+
const tag = String(args.tag ?? "").trim();
|
|
155
|
+
if (!tag) errors.push("a pagelist fence needs a tag attribute");
|
|
156
|
+
const type = String(args.type ?? "").trim();
|
|
157
|
+
if (type && !noteTypes().includes(type))
|
|
158
|
+
errors.push(`type ${JSON.stringify(type)} is not a note type`);
|
|
159
|
+
const sort = String(args.sort ?? PAGE_LIST_SORTS[0]).trim();
|
|
160
|
+
if (!PAGE_LIST_SORTS.includes(sort))
|
|
161
|
+
errors.push(`sort needs ${PAGE_LIST_SORTS.join(" or ")}`);
|
|
162
|
+
const flags = { descriptions: false, allowEmpty: false };
|
|
163
|
+
for (const [key, field] of [
|
|
164
|
+
["descriptions", "descriptions"],
|
|
165
|
+
["allow-empty", "allowEmpty"],
|
|
166
|
+
]) {
|
|
167
|
+
if (!Object.hasOwn(args, key)) continue;
|
|
168
|
+
try {
|
|
169
|
+
flags[field] = booleanAttribute(args[key], key);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
errors.push(error.message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (close >= lines.length) errors.push("a pagelist fence needs a closing fence");
|
|
175
|
+
else if (
|
|
176
|
+
lines
|
|
177
|
+
.slice(i + 1, close)
|
|
178
|
+
.join("\n")
|
|
179
|
+
.trim()
|
|
180
|
+
)
|
|
181
|
+
errors.push("a pagelist fence takes no body; its tag is the tag attribute");
|
|
182
|
+
blocks.push({
|
|
183
|
+
line: i,
|
|
184
|
+
close,
|
|
185
|
+
indent,
|
|
186
|
+
tag,
|
|
187
|
+
type,
|
|
188
|
+
sort,
|
|
189
|
+
...flags,
|
|
190
|
+
problems: errors,
|
|
191
|
+
block: lines.slice(i, Math.min(close + 1, lines.length)).join("\n"),
|
|
192
|
+
});
|
|
193
|
+
i = close;
|
|
194
|
+
}
|
|
195
|
+
return blocks;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A page's sort key, folded so a circumflex sorts with its letter. */
|
|
199
|
+
const nameKey = (record) => String(record?.nameAscii ?? record?.name?.full ?? "").toLowerCase();
|
|
200
|
+
|
|
201
|
+
/** The address a page is addressed by, as a stable tiebreak. */
|
|
202
|
+
const slugKey = (record) => String(record?.address?.slug ?? "");
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* By name, tiebroken on address.
|
|
206
|
+
*
|
|
207
|
+
* The tiebreak keeps the list stable where two pages share a name, which a
|
|
208
|
+
* corpus of a few thousand eventually does.
|
|
209
|
+
*/
|
|
210
|
+
function byName(a, b) {
|
|
211
|
+
const an = nameKey(a);
|
|
212
|
+
const bn = nameKey(b);
|
|
213
|
+
if (an !== bn) return an < bn ? -1 : 1;
|
|
214
|
+
const as = slugKey(a);
|
|
215
|
+
const bs = slugKey(b);
|
|
216
|
+
return (
|
|
217
|
+
as < bs ? -1
|
|
218
|
+
: as > bs ? 1
|
|
219
|
+
: 0
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** By note type, then by name within it. */
|
|
224
|
+
function byType(a, b) {
|
|
225
|
+
const at = String(a?.type ?? "");
|
|
226
|
+
const bt = String(b?.type ?? "");
|
|
227
|
+
if (at !== bt) return at < bt ? -1 : 1;
|
|
228
|
+
return byName(a, b);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const COMPARATORS = Object.freeze({ name: byName, type: byType });
|
|
232
|
+
|
|
233
|
+
/** A link label, with the characters a wikilink reads replaced. */
|
|
234
|
+
function linkLabel(record) {
|
|
235
|
+
const written = String(record?.name?.full ?? record?.shortcode ?? "")
|
|
236
|
+
// A wikilink's own separator, so a name carrying one would truncate
|
|
237
|
+
// the label at it.
|
|
238
|
+
.replace(/\|/g, "/")
|
|
239
|
+
.replace(/[\r\n]+/g, " ")
|
|
240
|
+
.trim();
|
|
241
|
+
return written || slugKey(record);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A description on one line, so a list item stays a list item. */
|
|
245
|
+
const oneLine = (value) => (typeof value === "string" ? value.replace(/[\r\n]+/g, " ").trim() : "");
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The pages a directive selects, as index records.
|
|
249
|
+
*
|
|
250
|
+
* A **stub** is left out: an empty body publishes no page, which the index says
|
|
251
|
+
* by holding no address, so there is nothing for the list to link to. A
|
|
252
|
+
* documentation journal and an asset are not pages an author tagged, which
|
|
253
|
+
* {@link module:engine/index-records.isNoteRecord} already answers.
|
|
254
|
+
*
|
|
255
|
+
* @param {object[]} records - Content-index records.
|
|
256
|
+
* @param {{tag: string, type: string}} directive - From {@link findPageListBlocks}.
|
|
257
|
+
* @returns {object[]} The selected records, unordered.
|
|
258
|
+
*/
|
|
259
|
+
export function pagesForList(records, { tag, type }) {
|
|
260
|
+
return (records ?? []).filter((record) => {
|
|
261
|
+
if (!isNoteRecord(record) || !record.address) return false;
|
|
262
|
+
if (type && record.type !== type) return false;
|
|
263
|
+
return hasTag(record, tag);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Render one directive's answer as markdown.
|
|
269
|
+
*
|
|
270
|
+
* **The emitted link is the page's `address.slug`** — the `type-shortcode`
|
|
271
|
+
* form a wikilink resolves — which is exactly what a table's `_ref` cell
|
|
272
|
+
* writes. A short form names the package the link is written in, so it is
|
|
273
|
+
* unambiguous for a list of this package's own pages, and it resolves to the
|
|
274
|
+
* note's **page** on each surface: a compendium journal in Foundry, a URL on
|
|
275
|
+
* the web and in the book. An item note's page is its documentation journal,
|
|
276
|
+
* and the short form reaches it the way prose does.
|
|
277
|
+
*
|
|
278
|
+
* @param {object[]} records - Content-index records.
|
|
279
|
+
* @param {object} directive - From {@link findPageListBlocks}.
|
|
280
|
+
* @returns {{markdown: string, pages: number}} The list, and how many pages
|
|
281
|
+
* are in it.
|
|
282
|
+
*/
|
|
283
|
+
export function renderPageList(records, directive) {
|
|
284
|
+
const selected = pagesForList(records, directive).sort(
|
|
285
|
+
COMPARATORS[directive.sort] ?? COMPARATORS.name,
|
|
286
|
+
);
|
|
287
|
+
const rows = selected.map((record) => {
|
|
288
|
+
const link = `- [[${slugKey(record)}|${linkLabel(record)}]]`;
|
|
289
|
+
if (!directive.descriptions) return link;
|
|
290
|
+
const summary = oneLine(record.description);
|
|
291
|
+
return summary ? `${link} — ${summary}` : link;
|
|
292
|
+
});
|
|
293
|
+
return { markdown: rows.join("\n"), pages: selected.length };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* What the expander prints where a tag selects nothing.
|
|
298
|
+
*
|
|
299
|
+
* The tag is quoted, because a misspelled tag is the whole of what goes wrong
|
|
300
|
+
* here and a reader needs to see the exact spelling the directive asked for.
|
|
301
|
+
*/
|
|
302
|
+
function emptyMessage({ tag, type }) {
|
|
303
|
+
const scope = type ? ` of type ${JSON.stringify(type)}` : "";
|
|
304
|
+
return (
|
|
305
|
+
`page list selects no page; no page${scope} carries the tag ${JSON.stringify(tag)}. ` +
|
|
306
|
+
"Check the spelling, tag some pages, or state {allow-empty=true}"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Answer every `pagelist` directive in a set of note bodies, ahead of expansion.
|
|
312
|
+
*
|
|
313
|
+
* **Why a separate pass.** The same reason the SQL tables have one:
|
|
314
|
+
* {@link module:engine/content-tables.expandContentTables} is synchronous and
|
|
315
|
+
* exported, and the corpus is enumerated once by whoever already holds it
|
|
316
|
+
* rather than derived again inside a renderer. A page list needs no database —
|
|
317
|
+
* it is a filter over the records — so preparing one costs a walk of what the
|
|
318
|
+
* caller already has.
|
|
319
|
+
*
|
|
320
|
+
* **Keyed by note, then by the directive's ordinal within it**, not by its
|
|
321
|
+
* line: the passes do not agree on what a body is, and trim it differently,
|
|
322
|
+
* but a directive's position in the sequence of fences is the same in each.
|
|
323
|
+
*
|
|
324
|
+
* A malformed directive is recorded rather than thrown, so one bad fence costs
|
|
325
|
+
* its own list and not the whole build's report.
|
|
326
|
+
*
|
|
327
|
+
* @param {object[]} records - Content-index records, already narrowed to the
|
|
328
|
+
* audience the surface publishes to.
|
|
329
|
+
* @param {Array<{source: string, markdown: string}>} sources - The bodies to scan.
|
|
330
|
+
* @returns {Map<string, Array<{markdown?: string, pages?: number, empty?: string,
|
|
331
|
+
* reason?: string, allowEmpty: boolean}>>} Note to results, in document order.
|
|
332
|
+
*/
|
|
333
|
+
export function preparePageLists(records, sources) {
|
|
334
|
+
const prepared = new Map();
|
|
335
|
+
for (const { source, markdown } of sources ?? []) {
|
|
336
|
+
const blocks = findPageListBlocks(markdown);
|
|
337
|
+
if (!blocks.length) continue;
|
|
338
|
+
const forNote = [];
|
|
339
|
+
prepared.set(source, forNote);
|
|
340
|
+
for (const block of blocks) {
|
|
341
|
+
if (block.problems.length) {
|
|
342
|
+
forNote.push({
|
|
343
|
+
reason: block.problems.join("; "),
|
|
344
|
+
allowEmpty: block.allowEmpty,
|
|
345
|
+
});
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const { markdown: rendered, pages } = renderPageList(records, block);
|
|
349
|
+
forNote.push({
|
|
350
|
+
markdown: rendered,
|
|
351
|
+
pages,
|
|
352
|
+
empty: emptyMessage(block),
|
|
353
|
+
allowEmpty: block.allowEmpty,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return prepared;
|
|
358
|
+
}
|
package/engine/pathnames.mjs
CHANGED
|
@@ -443,3 +443,39 @@ export function resolvePathname(raw, config) {
|
|
|
443
443
|
pdf: `${ASSETS_SEGMENT}/${suffix}`,
|
|
444
444
|
};
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The pathname one authored image address keys an asset's role and pixel
|
|
449
|
+
* size under — `<package>/assets/<suffix>`, the same key
|
|
450
|
+
* {@link module:engine/art-fields.assetImageInfoByPathname} builds from the
|
|
451
|
+
* asset record itself.
|
|
452
|
+
*
|
|
453
|
+
* **Why this exists apart from `resolvePathname`'s own forms.** An embed
|
|
454
|
+
* rewrites to that exact pathname — see
|
|
455
|
+
* {@link module:engine/content-embeds.resolveEmbeds} — but an author's own
|
|
456
|
+
* body image is free to write the bare, own-package form instead
|
|
457
|
+
* (`images/m.webp`), which names the identical file without naming the
|
|
458
|
+
* package. The two forms have to key the same lookup, and `resolvePathname`
|
|
459
|
+
* already answers which package a bare form belongs to; this just restates
|
|
460
|
+
* that answer in the one shape a renderer looks a picture's role up by.
|
|
461
|
+
*
|
|
462
|
+
* **Never throws.** A lookup is a best-effort read, not a validation — the
|
|
463
|
+
* pathname lint is what refuses a Foundry-spelled address — so a value
|
|
464
|
+
* {@link resolvePathname} refuses outright answers `null` here as well, the
|
|
465
|
+
* same as a blank or an external one.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} src - The address, as authored or as an embed resolved it.
|
|
468
|
+
* @param {object} config - The resolved build configuration.
|
|
469
|
+
* @returns {string|null} The pathname, or `null` for an address naming no
|
|
470
|
+
* asset record to look up.
|
|
471
|
+
*/
|
|
472
|
+
export function assetPathnameKey(src, config) {
|
|
473
|
+
let forms;
|
|
474
|
+
try {
|
|
475
|
+
forms = resolvePathname(src, config);
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
if (!forms || forms.state !== "package" || !forms.package) return null;
|
|
480
|
+
return `${forms.package}/${ASSETS_SEGMENT}/${forms.suffix}`;
|
|
481
|
+
}
|