@heroiclands/package-build 22.25.0 → 22.27.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 +17 -0
- package/README.md +4 -0
- package/assets/styles/content-base.css +46 -3
- package/docs/api.md +35 -12
- package/docs/authoring/frontmatter.md +21 -0
- package/docs/authoring/links-and-markup.md +96 -149
- package/docs/content-format.md +6 -2
- package/docs/guides/government-migration.md +52 -0
- package/docs/guides/site.md +5 -5
- package/docs/reference/format-details.md +152 -246
- package/docs/reference/note-types.md +27 -28
- package/docs/reference/theme-pages.md +33 -29
- package/engine/anchors.mjs +85 -13
- package/engine/art-fields.mjs +1 -1
- package/engine/base-compiler.mjs +9 -1
- package/engine/bundles.mjs +1 -1
- package/engine/calendar-notes.mjs +1 -1
- package/engine/code-fences.mjs +6 -2
- package/engine/content-alerts.mjs +195 -0
- package/engine/content-blocks.mjs +119 -241
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-figures.mjs +166 -230
- package/engine/content-format-check.mjs +3 -2
- package/engine/content-format.yaml +1 -5
- package/engine/content-images.mjs +3 -3
- package/engine/content-poetry.mjs +141 -0
- package/engine/content-spans.mjs +177 -0
- package/engine/foundry-entries.mjs +2 -0
- package/engine/frontmatter-lint.mjs +2 -1
- package/engine/heading-attributes.mjs +1 -1
- package/engine/helpers.mjs +8 -3
- package/engine/holdings.mjs +197 -333
- package/engine/index.mjs +1 -1
- package/engine/infobox.mjs +11 -1
- package/engine/journals.mjs +8 -6
- package/engine/markdown-expressions.mjs +9 -3
- package/engine/metadata-index.mjs +5 -3
- package/engine/note-vocabulary.mjs +30 -13
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-render.mjs +90 -31
- package/engine/populations.mjs +17 -83
- package/engine/site-build.mjs +44 -16
- package/engine/site-preview.mjs +22 -6
- package/engine/wikilinks.mjs +18 -4
- package/hugo-theme/layouts/partials/holdings/entry.html +2 -2
- package/hugo-theme/layouts/partials/holdings.html +14 -14
- package/hugo-theme/static/css/style.css +40 -2
- package/package.json +1 -1
- package/sohl/item-fields.mjs +0 -16
- package/types/engine/anchors.d.mts +13 -4
- package/types/engine/art-fields.d.mts +1 -1
- package/types/engine/calendar-notes.d.mts +1 -1
- package/types/engine/code-fences.d.mts +3 -1
- package/types/engine/content-alerts.d.mts +62 -0
- package/types/engine/content-blocks.d.mts +3 -10
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-figures.d.mts +13 -20
- package/types/engine/content-images.d.mts +1 -1
- package/types/engine/content-poetry.d.mts +32 -0
- package/types/engine/content-spans.d.mts +18 -0
- package/types/engine/holdings.d.mts +75 -118
- package/types/engine/infobox.d.mts +1 -0
- package/types/engine/markdown-expressions.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +14 -2
- package/types/engine/populations.d.mts +4 -13
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/wikilinks.d.mts +5 -1
|
@@ -177,7 +177,7 @@ export function resolveEmbed(index, embed) {
|
|
|
177
177
|
* The role an address's asset declares, read through the same resolution an
|
|
178
178
|
* embed's address takes.
|
|
179
179
|
*
|
|
180
|
-
* This is the lookup a
|
|
180
|
+
* This is the lookup a captioned item's `map` counter reaches through —
|
|
181
181
|
* see {@link module:engine/content-figures.scanFigures}'s `resolveRole` — for
|
|
182
182
|
* an address still in its authored form, before {@link resolveEmbeds} has
|
|
183
183
|
* rewritten it into the ordinary image every surface renders.
|
|
@@ -2,83 +2,22 @@
|
|
|
2
2
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* `:::figure` — the fence that declares what is numbered and captioned.
|
|
7
|
-
*
|
|
8
|
-
* ```
|
|
9
|
-
* :::figure {#thorn .border}
|
|
10
|
-
* ![[being-foobar|The Great Beast]]
|
|
11
|
-
* ///
|
|
12
|
-
* The great beast, as drawn by [[person-havard]].
|
|
13
|
-
* :::
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* The fence states the unit, so nothing is inferred about scope and a group —
|
|
17
|
-
* two portraits as one plate, a table with its key beneath it — is one figure
|
|
18
|
-
* with one number.
|
|
19
|
-
*
|
|
20
|
-
* **The caption is optional and the fence is what numbers a thing.** A fence
|
|
21
|
-
* with no `///` draws its label alone. So the rule is "no fence, no number":
|
|
22
|
-
* a bare embed takes no number and nothing is drawn beneath it.
|
|
23
|
-
*
|
|
24
|
-
* **`{#id}` is optional too.** A fence that declares none is numbered and drawn
|
|
25
|
-
* and carries no anchor, so nothing can reference it.
|
|
26
|
-
*
|
|
27
|
-
* ## The split is lexical
|
|
28
|
-
*
|
|
29
|
-
* The first `///` at the top level of the body divides the contents from the
|
|
30
|
-
* caption, and it is found **on the raw lines, before any markdown parsing**.
|
|
31
|
-
* The alternative delimiters are claimed by the grammar: `---` or `===` on the
|
|
32
|
-
* line after a paragraph is a setext heading, so an image tight against one
|
|
33
|
-
* becomes an `<h2>` with nothing said. `///` has no CommonMark meaning, which
|
|
34
|
-
* leaves a genuine thematic break available inside a grouped figure.
|
|
35
|
-
*
|
|
36
|
-
* A delimiter inside a nested code fence is content: a captioned listing
|
|
37
|
-
* carries `///` as a doc comment, so the scan for both the delimiter and the
|
|
38
|
-
* closing `:::` tracks nested fences and reads neither inside one.
|
|
39
|
-
*
|
|
40
|
-
* ## The kind is derived, never authored
|
|
41
|
-
*
|
|
42
|
-
* A table is self-evidently a table and a lone picture a figure, so the counter
|
|
43
|
-
* comes from the fence's contents: a table is `table`, an `sql` fence is
|
|
44
|
-
* `table`, any other fence or code block is `code`, pictures alone are
|
|
45
|
-
* `figure`, and everything else is `prose`. A fence around prose, with
|
|
46
|
-
* `.border`, is a numbered, referable boxed aside, which is what `Prose` names.
|
|
47
|
-
*
|
|
48
|
-
* **A picture counts as `map` instead of `figure` when every picture the fence
|
|
49
|
-
* holds is one.** The fence's own markup cannot say so — a picture of a map
|
|
50
|
-
* looks like any other picture — so the fact travels through a `resolveRole`
|
|
51
|
-
* function the caller supplies, from the address each picture draws (an
|
|
52
|
-
* embed's address, an image's path, or an `<img>` tag's `src`) to the role its
|
|
53
|
-
* asset declares. This pass resolves no address of its own; `resolveRole` is
|
|
54
|
-
* the one seam a caller reaches through with whatever lookup it already holds.
|
|
55
|
-
* A caller that supplies none, or whose lookup answers nothing for a picture,
|
|
56
|
-
* gets `figure` — the same reading as a picture with a role other than `map`.
|
|
57
|
-
*
|
|
58
|
-
* ## The class vocabulary is closed
|
|
59
|
-
*
|
|
60
|
-
* {@link FIGURE_CLASSES} holds what an author may write, and a class outside it
|
|
61
|
-
* is a finding naming the class — so `.border` does not become the first of
|
|
62
|
-
* twenty presentational classes. `key=value` is refused: the construct takes an
|
|
63
|
-
* id and classes.
|
|
64
|
-
*
|
|
65
|
-
* @module
|
|
66
|
-
*/
|
|
5
|
+
/** Leading captions apply to the next supported Markdown block. @module */
|
|
67
6
|
|
|
7
|
+
import { spanMarkdownPlugin } from "./content-spans.mjs";
|
|
68
8
|
import MarkdownIt from "markdown-it";
|
|
69
9
|
import footnotePlugin from "markdown-it-footnote";
|
|
70
10
|
import deflistPlugin from "markdown-it-deflist";
|
|
71
11
|
|
|
72
12
|
import { slugify } from "./content-slug.mjs";
|
|
73
|
-
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
74
|
-
import { HEADING_LINE,
|
|
13
|
+
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
14
|
+
import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
75
15
|
import { IMAGE_PATTERN } from "./content-images.mjs";
|
|
76
16
|
|
|
77
|
-
const parser = new MarkdownIt({ html: true })
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const SPLIT = /^\/\/\/\s*$/;
|
|
17
|
+
const parser = new MarkdownIt({ html: true })
|
|
18
|
+
.use(footnotePlugin)
|
|
19
|
+
.use(deflistPlugin)
|
|
20
|
+
.use(spanMarkdownPlugin);
|
|
82
21
|
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
83
22
|
|
|
84
23
|
/**
|
|
@@ -104,6 +43,7 @@ const HTML_IMG_SRC = /<img\b[^>]*\bsrc="([^"]*)"/g;
|
|
|
104
43
|
/** A figure's automatically determined kind, and the word that labels it. */
|
|
105
44
|
export const FIGURE_NAMES = Object.freeze({
|
|
106
45
|
code: "Code",
|
|
46
|
+
example: "Example",
|
|
107
47
|
table: "Table",
|
|
108
48
|
figure: "Figure",
|
|
109
49
|
map: "Map",
|
|
@@ -112,12 +52,8 @@ export const FIGURE_NAMES = Object.freeze({
|
|
|
112
52
|
});
|
|
113
53
|
|
|
114
54
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* `.border` states that the captioned thing is drawn inside a border, and each
|
|
118
|
-
* medium owns what that means. The vocabulary is closed: a class absent from it
|
|
119
|
-
* is a finding naming the class.
|
|
120
|
-
*
|
|
55
|
+
* Historical built-in presentation classes, retained for theme consumers.
|
|
56
|
+
* Captions now accept arbitrary classes through the shared attribute grammar.
|
|
121
57
|
* @type {readonly string[]}
|
|
122
58
|
*/
|
|
123
59
|
export const FIGURE_CLASSES = Object.freeze(["border"]);
|
|
@@ -125,6 +61,7 @@ export const FIGURE_CLASSES = Object.freeze(["border"]);
|
|
|
125
61
|
/** Number a sequence of figures with counters shared across documents. */
|
|
126
62
|
export function numberFigures(figures, counts) {
|
|
127
63
|
return figures.map((figure) => {
|
|
64
|
+
if (figure.numbered === false) return { ...figure, number: 0, label: "" };
|
|
128
65
|
const number = (counts[figure.kind] ?? 0) + 1;
|
|
129
66
|
counts[figure.kind] = number;
|
|
130
67
|
return { ...figure, number, label: `${FIGURE_NAMES[figure.kind]} ${number}` };
|
|
@@ -208,7 +145,7 @@ function picturesAreMap(contents, resolveRole) {
|
|
|
208
145
|
* @returns {"code"|"table"|"figure"|"map"|"poem"|"prose"} The kind.
|
|
209
146
|
*/
|
|
210
147
|
function figureKind(first, contents, resolveRole) {
|
|
211
|
-
if (/^(
|
|
148
|
+
if (/^(?:<div\b[^>]*\bclass="poetry(?:\s|"))/.test(contents)) return "poem";
|
|
212
149
|
if (first?.type === "table_open") return "table";
|
|
213
150
|
if (first?.type === "fence" && /^\s*sql\b/i.test(first.info ?? "")) return "table";
|
|
214
151
|
if (first?.type === "fence" || first?.type === "code_block") return "code";
|
|
@@ -221,35 +158,27 @@ function figureKind(first, contents, resolveRole) {
|
|
|
221
158
|
}
|
|
222
159
|
|
|
223
160
|
/**
|
|
224
|
-
* Read
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* @param {string} source - A note body with expanded tables.
|
|
161
|
+
* Read leading captions and their following blocks outside literal fences.
|
|
162
|
+
* Historical figure API names remain for caption references and consumers.
|
|
163
|
+
* @param {string} source - The authored or expanded note body.
|
|
229
164
|
* @param {object} [options] - Options.
|
|
230
|
-
* @param {(address: string) => string|undefined} [options.resolveRole] -
|
|
231
|
-
* a picture's address to the role its asset declares, so a picture whose
|
|
232
|
-
* asset declares `role: map` counts as `map` rather than `figure`. Omitted,
|
|
233
|
-
* every picture counts as `figure` — the same reading a role other than
|
|
234
|
-
* `map` gets.
|
|
165
|
+
* @param {(address: string) => string|undefined} [options.resolveRole] - Asset role lookup.
|
|
235
166
|
* @returns {{figures: Array<{id: string, slug: string, caption: string,
|
|
236
|
-
* hasCaption: boolean, classes: string[],
|
|
237
|
-
*
|
|
238
|
-
* captionStart: number, captionEnd: number, close: number}>,
|
|
167
|
+
* hasCaption: boolean, classes: string[], attributes: Record<string,string>,
|
|
168
|
+
* kind: string, numbered: boolean, number: number, label: string, line: number,
|
|
169
|
+
* bodyStart: number, bodyEnd: number, captionStart: number, captionEnd: number, close: number}>,
|
|
239
170
|
* errors: Array<{line: number, column: number, message: string}>}}
|
|
240
171
|
*/
|
|
241
172
|
export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
242
173
|
const lines = String(source ?? "").split("\n");
|
|
243
|
-
const figures = []
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const headingIds = [];
|
|
174
|
+
const figures = [],
|
|
175
|
+
errors = [],
|
|
176
|
+
ids = new Set();
|
|
177
|
+
const counts = {};
|
|
248
178
|
let codeFence = null;
|
|
249
|
-
|
|
179
|
+
const fault = (line, message) => errors.push({ line: line + 1, column: 1, message });
|
|
250
180
|
for (let i = 0; i < lines.length; i++) {
|
|
251
|
-
const
|
|
252
|
-
const fence = FENCE.exec(line);
|
|
181
|
+
const fence = FENCE.exec(lines[i]);
|
|
253
182
|
if (codeFence) {
|
|
254
183
|
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
|
|
255
184
|
codeFence = null;
|
|
@@ -259,166 +188,167 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
259
188
|
codeFence = fence[1];
|
|
260
189
|
continue;
|
|
261
190
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (!FIGURE_LINE.test(line)) continue;
|
|
266
|
-
const open = OPEN.exec(line);
|
|
267
|
-
if (!open) {
|
|
268
|
-
errors.push({
|
|
269
|
-
line: i + 1,
|
|
270
|
-
column: 1,
|
|
271
|
-
message: "a figure's attributes are written as {#id .border}",
|
|
272
|
-
});
|
|
191
|
+
const match = /^ {0,3}:(@)?[ \t]+(.+?)\s*$/.exec(lines[i]);
|
|
192
|
+
if (!match) {
|
|
193
|
+
if (/^ {0,3}:@?[ \t]*$/.test(lines[i])) fault(i, "a caption needs text");
|
|
273
194
|
continue;
|
|
274
195
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
faults.push(
|
|
281
|
-
`a figure takes no .${name} class — the classes a figure takes are ` +
|
|
282
|
-
FIGURE_CLASSES.map((known) => `.${known}`).join(", "),
|
|
283
|
-
);
|
|
196
|
+
// A definition-list description has a nonblank term immediately before it.
|
|
197
|
+
if (!match[1] && i > 0 && lines[i - 1].trim()) continue;
|
|
198
|
+
if ((i > 0 && lines[i - 1].trim()) || lines[i + 1]?.trim()) {
|
|
199
|
+
fault(i, "a caption needs a blank line before and after it");
|
|
200
|
+
continue;
|
|
284
201
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
202
|
+
let caption = match[2],
|
|
203
|
+
attributes = parseExtensionAttributes("");
|
|
204
|
+
const attr = /\s+(\{.*\})$/.exec(caption);
|
|
205
|
+
if (attr) {
|
|
206
|
+
attributes = parseExtensionAttributes(attr[1].slice(1, -1));
|
|
207
|
+
caption = caption.slice(0, attr.index).trim();
|
|
208
|
+
} else if (/\s+\{/.test(caption)) fault(i, "a caption has malformed attributes");
|
|
209
|
+
for (const message of [...attributes.problems, ...refusedAttributes(attributes.values)])
|
|
210
|
+
fault(i, message);
|
|
211
|
+
const explicit = attributes.values.type;
|
|
212
|
+
if (
|
|
213
|
+
explicit &&
|
|
214
|
+
!["figure", "table", "poetry", "code", "prose", "example"].includes(explicit)
|
|
215
|
+
)
|
|
216
|
+
fault(i, `unsupported caption type "${explicit}"`);
|
|
217
|
+
if (!caption) fault(i, "a caption needs text");
|
|
218
|
+
let bodyStart = i + 1;
|
|
219
|
+
while (bodyStart < lines.length && !lines[bodyStart].trim()) bodyStart++;
|
|
220
|
+
if (bodyStart === lines.length || /^ {0,3}:@?[ \t]+/.test(lines[bodyStart])) {
|
|
221
|
+
fault(i, "a caption needs a following item");
|
|
222
|
+
continue;
|
|
289
223
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (nested) {
|
|
309
|
-
fenced.add(close);
|
|
224
|
+
let bodyEnd;
|
|
225
|
+
const firstLine = lines[bodyStart];
|
|
226
|
+
let kind;
|
|
227
|
+
if (/^ {0,3}:::(?:[ \t]+\{.*\})?[ \t]*$/.test(firstLine)) {
|
|
228
|
+
let depth = 1,
|
|
229
|
+
literal = null;
|
|
230
|
+
bodyEnd = bodyStart + 1;
|
|
231
|
+
for (; bodyEnd < lines.length; bodyEnd++) {
|
|
232
|
+
const code = FENCE.exec(lines[bodyEnd]);
|
|
233
|
+
if (literal) {
|
|
234
|
+
if (code && code[1][0] === literal[0] && code[1].length >= literal.length)
|
|
235
|
+
literal = null;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (code) {
|
|
239
|
+
literal = code[1];
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
310
242
|
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
243
|
+
/^ *:::(?:[ \t]+\{.*\}|(?:secret|info|warn)\b.*)?[ \t]*$/.test(lines[bodyEnd])
|
|
244
|
+
) {
|
|
245
|
+
if (/^ *:::[ \t]*$/.test(lines[bodyEnd])) depth--;
|
|
246
|
+
else depth++;
|
|
247
|
+
if (!depth) {
|
|
248
|
+
bodyEnd++;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
318
252
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
253
|
+
if (depth) fault(bodyStart, "a captioned div needs a closing :::");
|
|
254
|
+
kind = "prose";
|
|
255
|
+
} else {
|
|
256
|
+
if (/^ *:::/.test(firstLine)) {
|
|
257
|
+
fault(bodyStart, "this fence cannot be captioned");
|
|
323
258
|
continue;
|
|
324
259
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
260
|
+
const tokens = parser.parse(lines.slice(bodyStart).join("\n"), {});
|
|
261
|
+
const first = tokens.find((token) => token.level === 0 && token.map);
|
|
262
|
+
if (
|
|
263
|
+
!first ||
|
|
264
|
+
![
|
|
265
|
+
"table_open",
|
|
266
|
+
"fence",
|
|
267
|
+
"code_block",
|
|
268
|
+
"paragraph_open",
|
|
269
|
+
"blockquote_open",
|
|
270
|
+
"html_block",
|
|
271
|
+
].includes(first.type)
|
|
272
|
+
) {
|
|
273
|
+
fault(
|
|
274
|
+
bodyStart,
|
|
275
|
+
"a caption must be followed by an image, table, poetry or code fence, paragraph, quote, or fenced div",
|
|
276
|
+
);
|
|
329
277
|
continue;
|
|
330
278
|
}
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
279
|
+
bodyEnd = bodyStart + first.map[1];
|
|
280
|
+
if (first.type === "html_block" && /^<div\b/.test(firstLine.trim())) {
|
|
281
|
+
let depth = 0;
|
|
282
|
+
for (let at = bodyStart; at < lines.length; at++) {
|
|
283
|
+
depth += (lines[at].match(/<div\b/g) ?? []).length;
|
|
284
|
+
depth -= (lines[at].match(/<\/div>/g) ?? []).length;
|
|
285
|
+
if (!depth) {
|
|
286
|
+
bodyEnd = at + 1;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
340
290
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const caption = split < 0 ? "" : lines.slice(captionStart, captionEnd).join("\n").trim();
|
|
354
|
-
if (!contents.trim())
|
|
355
|
-
errors.push({ line: start + 1, column: 1, message: "a figure has no contents" });
|
|
356
|
-
if (split >= 0 && !caption)
|
|
357
|
-
errors.push({
|
|
358
|
-
line: split + 1,
|
|
359
|
-
column: 1,
|
|
360
|
-
message:
|
|
361
|
-
"a figure's /// section carries no caption — write no /// to leave the " +
|
|
362
|
-
"figure uncaptioned",
|
|
363
|
-
});
|
|
364
|
-
// An H1, or an anchored heading at any level, starts a Foundry
|
|
365
|
-
// journal page — see `engine/content-blocks.mjs`'s identical refusal
|
|
366
|
-
// for a named block.
|
|
367
|
-
for (let at = bodyStart; at < close; at++) {
|
|
368
|
-
if (fenced.has(at)) continue;
|
|
369
|
-
if (!parseHeadingLine(lines[at])?.startsPage) continue;
|
|
370
|
-
errors.push({
|
|
371
|
-
line: at + 1,
|
|
372
|
-
column: 1,
|
|
373
|
-
message:
|
|
374
|
-
"a heading that starts a page cannot be written inside a figure — " +
|
|
375
|
-
"keep an H1 or an anchored heading at the top level, or drop the " +
|
|
376
|
-
"anchor and the level to stay inside it",
|
|
377
|
-
});
|
|
291
|
+
const contents = lines.slice(bodyStart, bodyEnd).join("\n");
|
|
292
|
+
if (
|
|
293
|
+
first.type === "html_block" &&
|
|
294
|
+
!/^(?:<figure\b|<img\b|<div\b)/.test(contents.trim())
|
|
295
|
+
) {
|
|
296
|
+
fault(bodyStart, "this block cannot be captioned");
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
kind =
|
|
300
|
+
first.type === "fence" && /^poetry(?:\s|$)/.test(first.info) ?
|
|
301
|
+
"poem"
|
|
302
|
+
: figureKind(first, contents.trim(), resolveRole);
|
|
378
303
|
}
|
|
304
|
+
if (explicit) kind = explicit === "poetry" ? "poem" : explicit;
|
|
379
305
|
const id = attributes.id;
|
|
380
|
-
if (id) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const trimmed = contents.trim();
|
|
387
|
-
const tokens = parser.parse(contents, {});
|
|
388
|
-
const first = tokens.find((token) => token.level === 0 && token.map);
|
|
389
|
-
const kind = figureKind(first, trimmed, resolveRole);
|
|
390
|
-
const number = ++counts[kind];
|
|
306
|
+
if (id && ids.has(slugify(id))) fault(i, `duplicate figure id "${id}"`);
|
|
307
|
+
if (id) ids.add(slugify(id));
|
|
308
|
+
const numbered = Boolean(match[1]);
|
|
309
|
+
const number = numbered ? (counts[kind] = (counts[kind] ?? 0) + 1) : 0;
|
|
391
310
|
figures.push({
|
|
392
311
|
id,
|
|
393
312
|
slug: id ? slugify(id) : "",
|
|
394
313
|
caption,
|
|
395
|
-
hasCaption:
|
|
314
|
+
hasCaption: true,
|
|
396
315
|
classes: attributes.classes,
|
|
316
|
+
attributes: attributes.values,
|
|
397
317
|
kind,
|
|
318
|
+
numbered,
|
|
398
319
|
number,
|
|
399
|
-
label: `${FIGURE_NAMES[kind]} ${number}
|
|
400
|
-
line:
|
|
320
|
+
label: numbered ? `${FIGURE_NAMES[kind]} ${number}` : "",
|
|
321
|
+
line: i + 1,
|
|
401
322
|
bodyStart,
|
|
402
323
|
bodyEnd,
|
|
403
|
-
captionStart,
|
|
404
|
-
captionEnd,
|
|
405
|
-
close,
|
|
324
|
+
captionStart: i,
|
|
325
|
+
captionEnd: i + 1,
|
|
326
|
+
close: bodyEnd - 1,
|
|
406
327
|
});
|
|
407
|
-
i =
|
|
328
|
+
i = bodyEnd - 1;
|
|
408
329
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
330
|
+
let literal = null;
|
|
331
|
+
for (let at = 0; at < lines.length; at++) {
|
|
332
|
+
const fence = FENCE.exec(lines[at]);
|
|
333
|
+
if (literal) {
|
|
334
|
+
if (fence && fence[1][0] === literal[0] && fence[1].length >= literal.length)
|
|
335
|
+
literal = null;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (fence) {
|
|
339
|
+
literal = fence[1];
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const heading = HEADING_LINE.exec(lines[at]);
|
|
343
|
+
const id = heading ? splitHeadingAttributes(heading[2]).id : "";
|
|
344
|
+
if (id && ids.has(slugify(id)))
|
|
345
|
+
fault(at, `heading and figure declare the same anchor "${slugify(id)}"`);
|
|
416
346
|
}
|
|
417
347
|
return { figures, errors };
|
|
418
348
|
}
|
|
419
349
|
|
|
420
350
|
/**
|
|
421
|
-
* Render
|
|
351
|
+
* Render leading captions and their next blocks as HTML, leaving other Markdown untouched.
|
|
422
352
|
*
|
|
423
353
|
* A code, table or prose figure's contents are left as Markdown inside the
|
|
424
354
|
* wrapper, blank-line separated from its tags exactly as
|
|
@@ -463,7 +393,11 @@ export function renderFigureBlocks(
|
|
|
463
393
|
const contents = lines.slice(figure.bodyStart, figure.bodyEnd).join("\n");
|
|
464
394
|
const classes = ["content-figure", `content-figure-${numbered.kind}`, ...figure.classes];
|
|
465
395
|
const id = figure.slug ? ` id="${escapeHtml(figure.slug)}"` : "";
|
|
466
|
-
|
|
396
|
+
const attrs = Object.entries(figure.attributes ?? {})
|
|
397
|
+
.filter(([key]) => key !== "type")
|
|
398
|
+
.map(([key, value]) => ` ${key}="${escapeHtml(value)}"`)
|
|
399
|
+
.join("");
|
|
400
|
+
output.push(`<div${id} class="${escapeHtml(classes.join(" "))}"${attrs}>`);
|
|
467
401
|
if (figure.kind === "figure" || figure.kind === "map") {
|
|
468
402
|
output.push(renderMarkdown(contents).trim());
|
|
469
403
|
} else {
|
|
@@ -472,7 +406,9 @@ export function renderFigureBlocks(
|
|
|
472
406
|
const label = escapeHtml(numbered.label);
|
|
473
407
|
output.push(
|
|
474
408
|
`<p class="content-figure-label">${
|
|
475
|
-
figure.hasCaption ?
|
|
409
|
+
figure.hasCaption ?
|
|
410
|
+
`${label ? `${label}: ` : ""}${parser.renderInline(figure.caption)}`
|
|
411
|
+
: label
|
|
476
412
|
}</p>`,
|
|
477
413
|
"</div>",
|
|
478
414
|
);
|
|
@@ -106,7 +106,7 @@ import { RETIRED_FIELD_ALIASES } from "./retired-fields.mjs";
|
|
|
106
106
|
import { authoredKey } from "./system-block.mjs";
|
|
107
107
|
// The top-level vocabulary, read rather than restated — a second list of the
|
|
108
108
|
// keys a note's own identity uses is one more thing to drift.
|
|
109
|
-
import { NOTE_TOP_LEVEL_KEY_SET } from "./note-vocabulary.mjs";
|
|
109
|
+
import { NOTE_TOP_LEVEL_KEY_SET, isLegacyDataField } from "./note-vocabulary.mjs";
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* Every field path any subtype of a published schema declares.
|
|
@@ -371,7 +371,8 @@ export function measureNote(note, format, { severity = "warning" } = {}) {
|
|
|
371
371
|
[...spec.dataKeys].map((key) => RETIRED_FIELD_ALIASES[key]).filter(Boolean),
|
|
372
372
|
);
|
|
373
373
|
for (const key of Object.keys(data ?? {})) {
|
|
374
|
-
if (spec.dataKeys.has(key) || renamedDataKeys.has(key))
|
|
374
|
+
if (spec.dataKeys.has(key) || renamedDataKeys.has(key) || isLegacyDataField(type, key))
|
|
375
|
+
continue;
|
|
375
376
|
const guess = nearest(key, spec.dataKeys);
|
|
376
377
|
add(
|
|
377
378
|
"unknown-data-key",
|
|
@@ -52,7 +52,6 @@ types:
|
|
|
52
52
|
- governance.ranks
|
|
53
53
|
- governance.offices
|
|
54
54
|
- seat
|
|
55
|
-
- domains
|
|
56
55
|
- population
|
|
57
56
|
- economy
|
|
58
57
|
- lore
|
|
@@ -292,6 +291,7 @@ types:
|
|
|
292
291
|
- lore
|
|
293
292
|
- parents
|
|
294
293
|
- population
|
|
294
|
+
- government
|
|
295
295
|
- market
|
|
296
296
|
- borders
|
|
297
297
|
- routes
|
|
@@ -395,10 +395,6 @@ claims:
|
|
|
395
395
|
system: sohl
|
|
396
396
|
source: data.seat
|
|
397
397
|
target: system.seat
|
|
398
|
-
- noteType: affiliation
|
|
399
|
-
system: sohl
|
|
400
|
-
source: data.domains
|
|
401
|
-
target: system.domain
|
|
402
398
|
- noteType: affiliation
|
|
403
399
|
system: sohl
|
|
404
400
|
source: data.parents
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
* floated run of text inside a paragraph would mean.
|
|
60
60
|
*
|
|
61
61
|
* **Alt text renders as a caption on no surface.** A caption is drawn only for
|
|
62
|
-
* an image inside a
|
|
62
|
+
* an image inside a captioned item that carries an authored caption, and the
|
|
63
63
|
* text drawn is always that caption. An image with no fence, or a fence with no
|
|
64
64
|
* caption, draws no line under the picture anywhere. The HTML surfaces still
|
|
65
65
|
* carry the alt text as the `img` element's `alt` attribute, for the reader a
|
|
@@ -333,7 +333,7 @@ export function escapeHtml(text) {
|
|
|
333
333
|
* function, so the two cannot drift into styling the same directive through
|
|
334
334
|
* different class names.
|
|
335
335
|
*
|
|
336
|
-
* **No `<figcaption>` is emitted here.** A
|
|
336
|
+
* **No `<figcaption>` is emitted here.** A captioned item draws its own
|
|
337
337
|
* label and authored caption around whatever this returns; the alt text stays
|
|
338
338
|
* on the `img` element alone.
|
|
339
339
|
*
|
|
@@ -538,7 +538,7 @@ export function checkImages(body, file, { bodyLine = 1, bodyColumn = 1, lineMap,
|
|
|
538
538
|
report(
|
|
539
539
|
image.index,
|
|
540
540
|
`\`"${image.title}"\` is a title on an image, and no surface here draws ` +
|
|
541
|
-
"one — a caption
|
|
541
|
+
"one — write a leading caption before the image",
|
|
542
542
|
);
|
|
543
543
|
}
|
|
544
544
|
if (!image.block) {
|