@heroiclands/package-build 22.26.0 → 22.28.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 +3 -1
- package/assets/styles/content-base.css +46 -3
- package/bin/content-commands.mjs +53 -5
- package/ci/publish-npm.mjs +49 -0
- package/docs/api.md +44 -24
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +96 -149
- package/docs/content-format.md +7 -3
- package/docs/guides/government-migration.md +52 -0
- package/docs/guides/site.md +5 -5
- package/docs/reference/format-details.md +131 -230
- package/docs/reference/note-types.md +27 -29
- package/docs/reference/theme-pages.md +31 -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/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 +0 -5
- package/engine/content-images.mjs +3 -3
- package/engine/content-poetry.mjs +141 -0
- package/engine/content-spans.mjs +177 -0
- package/engine/folder-notes.mjs +2 -3
- 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 +165 -273
- package/engine/index.mjs +2 -2
- package/engine/infobox.mjs +1 -1
- package/engine/journals.mjs +8 -6
- package/engine/markdown-expressions.mjs +9 -3
- package/engine/metadata-index.mjs +2 -3
- package/engine/note-vocabulary.mjs +19 -15
- 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/stub-lint.mjs +20 -64
- package/engine/wikilinks.mjs +18 -4
- package/githooks/pre-push +1 -1
- 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 +3 -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/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 +59 -105
- package/types/engine/markdown-expressions.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +12 -4
- 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
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* General divs and the `:::secret` disclosure fence.
|
|
5
5
|
*
|
|
6
6
|
* ```
|
|
7
|
-
* :::
|
|
7
|
+
* :::secret {#route title="For the GM" .wide}
|
|
8
8
|
* The ford floods in Azlet.
|
|
9
9
|
* :::
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The three names differ in the class they carry and in nothing else, so a
|
|
18
|
-
* stylesheet owns their appearance. None is emitted with an inline `style`.
|
|
12
|
+
* The secret fence carries the `secret` class and a heading, defaulting to
|
|
13
|
+
* `Secret`. General divs carry only authored classes and ordinary attributes.
|
|
14
|
+
* The author supplies secret headings through the reserved `title` attribute.
|
|
15
|
+
* Neither construct adds inline styles.
|
|
19
16
|
*
|
|
20
17
|
* Each block stands on its own: a malformed one is a finding at its own line
|
|
21
18
|
* and the blocks around it still render.
|
|
@@ -25,11 +22,7 @@
|
|
|
25
22
|
* page in two and publish the rest with no wrapper around it at all.
|
|
26
23
|
*
|
|
27
24
|
|
|
28
|
-
* **One pass reads
|
|
29
|
-
* one meet the second's closers with nothing open, and report a block the author
|
|
30
|
-
* had not written. A construct this pass does not own — `:::figure` — is
|
|
31
|
-
* counted rather than claimed, so its closer is attributed to it and the pass
|
|
32
|
-
* that owns it still finds it in the markdown this one passes through.
|
|
25
|
+
* **One pass reads named blocks and general divs.**
|
|
33
26
|
*
|
|
34
27
|
* **A block's body is left as Markdown, not pre-rendered.** The wrapper is
|
|
35
28
|
* written with a blank line after the opening tag and before the closing one,
|
|
@@ -46,15 +39,12 @@
|
|
|
46
39
|
*/
|
|
47
40
|
|
|
48
41
|
import crypto from "node:crypto";
|
|
42
|
+
import { slugify } from "./content-slug.mjs";
|
|
49
43
|
import MarkdownIt from "markdown-it";
|
|
50
|
-
import footnotePlugin from "markdown-it-footnote";
|
|
51
|
-
import deflistPlugin from "markdown-it-deflist";
|
|
52
44
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
53
|
-
import {
|
|
45
|
+
import { scanPoetry, renderPoetry } from "./content-poetry.mjs";
|
|
54
46
|
import { WITHHELD_CLASS, parseHeadingLine, withheldSections } from "./heading-attributes.mjs";
|
|
55
47
|
|
|
56
|
-
const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlugin);
|
|
57
|
-
|
|
58
48
|
/**
|
|
59
49
|
* Titles carry emphasis and nothing else. `html: false` escapes any tag an
|
|
60
50
|
* author writes there, because a title is written into an element's first child
|
|
@@ -62,46 +52,22 @@ const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlu
|
|
|
62
52
|
*/
|
|
63
53
|
const titleParser = new MarkdownIt({ html: false });
|
|
64
54
|
|
|
65
|
-
const OPEN = /^:::([A-Za-z][A-Za-z0-9-]*)(?:[ \t]+(.*?))?[ \t]*$/;
|
|
66
|
-
const CLOSE = /^:::[ \t]*$/;
|
|
67
|
-
const INDENTED_POETRY_OPEN = /^ +:::poetry(?:[ \t]+(.*?))?[ \t]*$/;
|
|
68
|
-
const INDENTED_CLOSE = /^ *:::[ \t]*$/;
|
|
69
|
-
|
|
70
55
|
/** The blocks an author may open, with the heading each takes by default. */
|
|
71
56
|
export const BLOCK_NAMES = Object.freeze({
|
|
72
|
-
info: "Info",
|
|
73
57
|
secret: "Secret",
|
|
74
|
-
warn: "Warn",
|
|
75
58
|
});
|
|
76
59
|
|
|
77
60
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* A box inside a GM-only section is a real thing to write — the section is a
|
|
81
|
-
* container for whatever the GM reads, boxes included — so a `secret` holds one
|
|
82
|
-
* and the box stays inside it. A box inside a box is noise, so `info` and `warn`
|
|
83
|
-
* hold no named block and a second opener inside one is a finding.
|
|
61
|
+
* Container fences whose bodies are scanned and rendered recursively.
|
|
84
62
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
63
|
+
* A secret may hold general divs; a general div may hold another div or a
|
|
64
|
+
* secret. A secret nested directly within another secret is refused.
|
|
87
65
|
*
|
|
88
66
|
* @type {readonly string[]}
|
|
89
67
|
*/
|
|
90
|
-
export const BLOCK_CONTAINERS = Object.freeze(["secret"]);
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Constructs another pass owns. A `:::figure` is read by the figure pass, so
|
|
94
|
-
* this one **counts it and does not claim it**: the opener and its closer pass
|
|
95
|
-
* through untouched, and the count is what lets a closer be attributed to the
|
|
96
|
-
* innermost block actually open. Claiming one would report a block that does not
|
|
97
|
-
* exist and take the figure out of the note.
|
|
98
|
-
*/
|
|
99
|
-
const FOREIGN = Object.freeze(["figure"]);
|
|
100
|
-
|
|
101
|
-
/** `title` is the heading. Everything else an author writes becomes an attribute. */
|
|
102
|
-
const TITLE = "title";
|
|
68
|
+
export const BLOCK_CONTAINERS = Object.freeze(["secret", "div"]);
|
|
103
69
|
|
|
104
|
-
const names = () => [...Object.keys(BLOCK_NAMES), "
|
|
70
|
+
const names = () => [...Object.keys(BLOCK_NAMES), "div"].join(", ");
|
|
105
71
|
|
|
106
72
|
/** Text safe inside a double-quoted attribute. */
|
|
107
73
|
function escapeAttribute(value) {
|
|
@@ -122,197 +88,126 @@ function escapeAttribute(value) {
|
|
|
122
88
|
* @returns {{blocks: Array<{name: string, title: string, id: string, classes: string[], attributes: Record<string, string>, start: number, end: number, body: string}>, errors: Array<{line: number, column: number, message: string}>}}
|
|
123
89
|
*/
|
|
124
90
|
export function scanBlocks(source) {
|
|
125
|
-
const lines = String(source ?? "").split("\n")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let
|
|
130
|
-
// Blocks open inside this pass's own block, and blocks of another
|
|
131
|
-
// construct open outside it, counted so a `:::` closes the innermost thing
|
|
132
|
-
// rather than whatever this pass happens to have open.
|
|
133
|
-
let innerOpen = 0;
|
|
134
|
-
let foreignOutside = 0;
|
|
135
|
-
|
|
91
|
+
const lines = String(source ?? "").split("\n"),
|
|
92
|
+
blocks = [],
|
|
93
|
+
errors = [];
|
|
94
|
+
const stack = [];
|
|
95
|
+
let fence = null;
|
|
136
96
|
for (let i = 0; i < lines.length; i++) {
|
|
137
97
|
const line = lines[i];
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
98
|
+
const code = /^ *(`{3,}|~{3,})(.*)$/.exec(line);
|
|
99
|
+
if (fence) {
|
|
100
|
+
if (
|
|
101
|
+
code &&
|
|
102
|
+
code[1][0] === fence[0] &&
|
|
103
|
+
code[1].length >= fence.length &&
|
|
104
|
+
!code[2].trim()
|
|
105
|
+
)
|
|
106
|
+
fence = null;
|
|
143
107
|
continue;
|
|
144
108
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
109
|
+
if (code) {
|
|
110
|
+
fence = code[1];
|
|
147
111
|
continue;
|
|
148
112
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
errors.push({ line: i + 1, column: 1, message: "a ::: line closes no block" });
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (opening.rejected) {
|
|
166
|
-
opening = null;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const bodyLines = lines.slice(opening.start + 1, i);
|
|
170
|
-
const rawBody = bodyLines.join("\n");
|
|
171
|
-
const body = opening.name === "poetry" ? rawBody : rawBody.trim();
|
|
172
|
-
if (opening.name === "poetry") {
|
|
173
|
-
for (let offset = 0; offset < bodyLines.length; offset++) {
|
|
174
|
-
const column = bodyLines[offset].indexOf("\t");
|
|
175
|
-
if (column >= 0)
|
|
176
|
-
errors.push({
|
|
177
|
-
line: opening.start + offset + 2,
|
|
178
|
-
column: column + 1,
|
|
179
|
-
message: "tabs are not allowed in poetry",
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (!body.trim()) {
|
|
113
|
+
const close = /^ *:::\s*$/.test(line);
|
|
114
|
+
const named = /^ *:::([A-Za-z][A-Za-z0-9-]*)(?:\s+(.*))?$/.exec(line);
|
|
115
|
+
const div = /^ *:::\s+(\{.*)$/.exec(line);
|
|
116
|
+
if (close && stack.length) {
|
|
117
|
+
const block = stack.pop();
|
|
118
|
+
if (block.rejected) continue;
|
|
119
|
+
block.end = i;
|
|
120
|
+
block.body = lines
|
|
121
|
+
.slice(block.start + 1, i)
|
|
122
|
+
.join("\n")
|
|
123
|
+
.trim();
|
|
124
|
+
if (!block.body)
|
|
184
125
|
errors.push({
|
|
185
|
-
line:
|
|
126
|
+
line: block.start + 1,
|
|
186
127
|
column: 1,
|
|
187
|
-
message: `${
|
|
128
|
+
message: `${block.name} block is empty`,
|
|
188
129
|
});
|
|
189
|
-
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (opening.name === "poetry" && opening.attributes.syllables) {
|
|
193
|
-
const expected = opening.attributes.syllables.split(",").length;
|
|
194
|
-
const actual = verseLines(body).length;
|
|
195
|
-
if (expected !== actual)
|
|
196
|
-
errors.push({
|
|
197
|
-
line: opening.start + 1,
|
|
198
|
-
column: 1,
|
|
199
|
-
message: `poetry syllables= gives ${expected} counts for ${actual} verse lines`,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
// An H1, or an anchored heading at any level, starts a Foundry
|
|
203
|
-
// journal page — splitting the block's own page in two and
|
|
204
|
-
// publishing the rest with no wrapper around it, a GM-only
|
|
205
|
-
// section included. Refused rather than split: a lower heading
|
|
206
|
-
// with no anchor is still the ordinary way to structure a box.
|
|
207
|
-
// Poetry treats those same characters as verse text.
|
|
208
|
-
for (let at = opening.start + 1; opening.name !== "poetry" && at < i; at++) {
|
|
209
|
-
if (!parseHeadingLine(lines[at])?.startsPage) continue;
|
|
210
|
-
const article = /^[aeiou]/i.test(opening.name) ? "an" : "a";
|
|
211
|
-
errors.push({
|
|
212
|
-
line: at + 1,
|
|
213
|
-
column: 1,
|
|
214
|
-
message:
|
|
215
|
-
`a heading that starts a page cannot be written inside ${article} ` +
|
|
216
|
-
`${opening.name} block — keep an H1 or an anchored heading ` +
|
|
217
|
-
"at the top level, or drop the anchor and the level to stay inside it",
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
blocks.push({ ...opening, end: i, body });
|
|
221
|
-
opening = null;
|
|
130
|
+
if (!stack.length) blocks.push(block);
|
|
222
131
|
continue;
|
|
223
132
|
}
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
if (!open) continue;
|
|
228
|
-
const [, name, raw = ""] = open;
|
|
133
|
+
if (!close && !named && !div) continue;
|
|
134
|
+
const name = named?.[1] ?? "div",
|
|
135
|
+
raw = named?.[2] ?? div?.[1] ?? "";
|
|
229
136
|
const at = { line: i + 1, column: 1 };
|
|
230
|
-
if (
|
|
231
|
-
if (opening) innerOpen += 1;
|
|
232
|
-
else foreignOutside += 1;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (!Object.hasOwn(BLOCK_NAMES, name) && name !== "poetry") {
|
|
137
|
+
if (name !== "div" && !Object.hasOwn(BLOCK_NAMES, name)) {
|
|
236
138
|
errors.push({ ...at, message: `there is no ${name} block; the blocks are ${names()}` });
|
|
237
|
-
|
|
139
|
+
stack.push({ start: i, rejected: true });
|
|
238
140
|
continue;
|
|
239
141
|
}
|
|
240
|
-
if (
|
|
241
|
-
if (BLOCK_CONTAINERS.includes(opening.name) && !BLOCK_CONTAINERS.includes(name)) {
|
|
242
|
-
// Held by the block it is written in, and counted so the
|
|
243
|
-
// closers are attributed in the order they were opened. The
|
|
244
|
-
// body is rendered for its own blocks before it is rendered as
|
|
245
|
-
// markdown, so the inner block comes out as a block.
|
|
246
|
-
innerOpen += 1;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
// Not counted, unlike a block the outer one holds: the nesting is
|
|
250
|
-
// already reported, and counting it would leave the outer block
|
|
251
|
-
// looking unclosed as well — two findings for one mistake.
|
|
142
|
+
if (stack.length && name !== "div" && !["secret", "div"].includes(stack.at(-1).name)) {
|
|
252
143
|
errors.push({ ...at, message: `nested ${name} blocks are not supported` });
|
|
144
|
+
stack.push({ start: i, rejected: true });
|
|
253
145
|
continue;
|
|
254
146
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let title = BLOCK_NAMES[name];
|
|
260
|
-
const text = raw.trim();
|
|
261
|
-
if (text) {
|
|
262
|
-
if (!/^\{[^}\n]*\}$/.test(text)) {
|
|
263
|
-
errors.push({ ...at, message: "block attributes need {…} braces" });
|
|
264
|
-
opening = { start: i, rejected: true };
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const parsed = parseExtensionAttributes(text.slice(1, -1));
|
|
268
|
-
if (parsed.problems.length) {
|
|
269
|
-
for (const message of parsed.problems) errors.push({ ...at, message });
|
|
270
|
-
opening = { start: i, rejected: true };
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
id = parsed.id;
|
|
274
|
-
classes = parsed.classes;
|
|
275
|
-
const refused = refusedAttributes(parsed.values);
|
|
276
|
-
for (const message of refused) errors.push({ ...at, message });
|
|
277
|
-
if (refused.length) {
|
|
278
|
-
opening = { start: i, rejected: true };
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
for (const [key, value] of Object.entries(parsed.values)) {
|
|
282
|
-
if (name === "poetry") {
|
|
283
|
-
if (!["form", "meter", "rhyme", "syllables", "lang"].includes(key))
|
|
284
|
-
errors.push({ ...at, message: `${key}= is not a poetry attribute` });
|
|
285
|
-
else if (key === "syllables" && !/^[1-9]\d*(?:,[1-9]\d*)*$/.test(value))
|
|
286
|
-
errors.push({
|
|
287
|
-
...at,
|
|
288
|
-
message: "poetry syllables= needs positive comma-separated counts",
|
|
289
|
-
});
|
|
290
|
-
else if (key === "lang" && !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value))
|
|
291
|
-
errors.push({ ...at, message: "poetry lang= needs a language tag" });
|
|
292
|
-
}
|
|
293
|
-
if (key === TITLE) title = value;
|
|
294
|
-
else attributes[key] = value;
|
|
295
|
-
}
|
|
147
|
+
if (stack.length && name === "secret" && stack.at(-1).name === "secret") {
|
|
148
|
+
errors.push({ ...at, message: "nested secret blocks are not supported" });
|
|
149
|
+
stack.push({ start: i, rejected: true });
|
|
150
|
+
continue;
|
|
296
151
|
}
|
|
297
|
-
|
|
152
|
+
let parsed = { id: "", classes: [], values: {}, problems: [] };
|
|
153
|
+
if (raw.trim()) {
|
|
154
|
+
if (!/^\{[^}\n]*\}$/.test(raw.trim()))
|
|
155
|
+
parsed.problems.push("block attributes need {…} braces");
|
|
156
|
+
else parsed = parseExtensionAttributes(raw.trim().slice(1, -1));
|
|
157
|
+
}
|
|
158
|
+
for (const message of [...parsed.problems, ...refusedAttributes(parsed.values)])
|
|
159
|
+
errors.push({ ...at, message });
|
|
160
|
+
const attributes = { ...parsed.values };
|
|
161
|
+
let title = BLOCK_NAMES[name] ?? "";
|
|
162
|
+
if (name !== "div" && Object.hasOwn(attributes, "title")) {
|
|
163
|
+
title = attributes.title;
|
|
164
|
+
delete attributes.title;
|
|
165
|
+
}
|
|
166
|
+
stack.push({
|
|
298
167
|
start: i,
|
|
299
168
|
name,
|
|
300
169
|
title,
|
|
301
|
-
id,
|
|
302
|
-
classes,
|
|
170
|
+
id: parsed.id,
|
|
171
|
+
classes: parsed.classes,
|
|
303
172
|
attributes,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (opening && !opening.rejected) {
|
|
309
|
-
errors.push({
|
|
310
|
-
line: opening.start + 1,
|
|
311
|
-
column: 1,
|
|
312
|
-
message: `${opening.name} block needs a closing ::: line`,
|
|
173
|
+
rejected: parsed.problems.length > 0 || refusedAttributes(parsed.values).length > 0,
|
|
174
|
+
indent: /^ */.exec(line)[0],
|
|
313
175
|
});
|
|
314
176
|
}
|
|
315
|
-
|
|
177
|
+
for (const block of stack)
|
|
178
|
+
if (!block.rejected)
|
|
179
|
+
errors.push({
|
|
180
|
+
line: block.start + 1,
|
|
181
|
+
column: 1,
|
|
182
|
+
message: `${block.name} block needs a closing ::: line`,
|
|
183
|
+
});
|
|
184
|
+
for (const block of blocks) {
|
|
185
|
+
let literal = null;
|
|
186
|
+
for (let i = block.start + 1; i < block.end; i++) {
|
|
187
|
+
const marker = /^ *(`{3,}|~{3,})(.*)$/.exec(lines[i]);
|
|
188
|
+
if (literal) {
|
|
189
|
+
if (
|
|
190
|
+
marker &&
|
|
191
|
+
marker[1][0] === literal[0] &&
|
|
192
|
+
marker[1].length >= literal.length &&
|
|
193
|
+
!marker[2].trim()
|
|
194
|
+
)
|
|
195
|
+
literal = null;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (marker) {
|
|
199
|
+
literal = marker[1];
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (parseHeadingLine(lines[i])?.startsPage)
|
|
203
|
+
errors.push({
|
|
204
|
+
line: i + 1,
|
|
205
|
+
column: 1,
|
|
206
|
+
message: `a heading that starts a page cannot be written inside a ${block.name} block — keep an H1 or an anchored heading at the top level, or drop the anchor and the level to stay inside it`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { blocks, errors: [...errors, ...scanPoetry(source).errors] };
|
|
316
211
|
}
|
|
317
212
|
|
|
318
213
|
/**
|
|
@@ -323,7 +218,7 @@ export function scanBlocks(source) {
|
|
|
323
218
|
* remembered.
|
|
324
219
|
*/
|
|
325
220
|
function elementId(block, target) {
|
|
326
|
-
if (block.id) return block.id;
|
|
221
|
+
if (block.id) return slugify(block.id);
|
|
327
222
|
if (target !== "foundry" || block.name !== "secret") return "";
|
|
328
223
|
const digest = crypto
|
|
329
224
|
.createHash("sha256")
|
|
@@ -339,26 +234,12 @@ function attributeText(block, target, classes) {
|
|
|
339
234
|
const parts = [`class="${escapeAttribute(classes.join(" "))}"`];
|
|
340
235
|
if (id) parts.push(`id="${escapeAttribute(id)}"`);
|
|
341
236
|
for (const [key, value] of Object.entries(block.attributes)) {
|
|
342
|
-
const attribute =
|
|
237
|
+
const attribute = key;
|
|
343
238
|
parts.push(`${attribute}="${escapeAttribute(value)}"`);
|
|
344
239
|
}
|
|
345
240
|
return parts.join(" ");
|
|
346
241
|
}
|
|
347
242
|
|
|
348
|
-
/** Keep a poem in its list, without turning standalone readable indentation into code. */
|
|
349
|
-
function poetryWrapperIndent(lines, block) {
|
|
350
|
-
const indent = block.indent;
|
|
351
|
-
if (indent.length < 4) return indent;
|
|
352
|
-
for (let at = block.start - 1; at >= 0; at--) {
|
|
353
|
-
const line = lines[at];
|
|
354
|
-
if (!line.trim()) continue;
|
|
355
|
-
const leading = /^ */.exec(line)[0].length;
|
|
356
|
-
if (/^ *(?:[-+*]|\d+[.)]) +/.test(line) && leading < indent.length) return indent;
|
|
357
|
-
if (leading < indent.length) break;
|
|
358
|
-
}
|
|
359
|
-
return "";
|
|
360
|
-
}
|
|
361
|
-
|
|
362
243
|
/**
|
|
363
244
|
* Render named blocks for one publishing surface.
|
|
364
245
|
*
|
|
@@ -377,14 +258,16 @@ function poetryWrapperIndent(lines, block) {
|
|
|
377
258
|
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
|
|
378
259
|
*/
|
|
379
260
|
export function renderBlocks(source, target) {
|
|
380
|
-
const {
|
|
381
|
-
|
|
261
|
+
const { errors } = scanBlocks(source);
|
|
262
|
+
source = renderPoetry(source).markdown;
|
|
263
|
+
const renderedBlocks = scanBlocks(source).blocks;
|
|
264
|
+
if (!renderedBlocks.length) return { markdown: String(source ?? ""), errors };
|
|
382
265
|
const lines = String(source ?? "").split("\n");
|
|
383
266
|
const output = [];
|
|
384
267
|
let cursor = 0;
|
|
385
|
-
for (const block of
|
|
268
|
+
for (const block of renderedBlocks) {
|
|
386
269
|
output.push(...lines.slice(cursor, block.start));
|
|
387
|
-
const classes = [block.name, ...block.classes];
|
|
270
|
+
const classes = [...(block.name === "div" ? [] : [block.name]), ...block.classes];
|
|
388
271
|
const attributes = attributeText(block, target, classes);
|
|
389
272
|
// A container's body may hold a block of its own, rendered first so
|
|
390
273
|
// its own wrapper is already written when the outer one wraps it in
|
|
@@ -394,13 +277,8 @@ export function renderBlocks(source, target) {
|
|
|
394
277
|
BLOCK_CONTAINERS.includes(block.name) ?
|
|
395
278
|
renderBlocks(block.body, target).markdown
|
|
396
279
|
: block.body;
|
|
397
|
-
if (block.name === "
|
|
398
|
-
|
|
399
|
-
const verses = poetryMarkdown(held)
|
|
400
|
-
.split("\n")
|
|
401
|
-
.map((line) => (line ? `${indent}${line}` : line))
|
|
402
|
-
.join("\n");
|
|
403
|
-
output.push("", `${indent}<div ${attributes}>`, "", verses, "", `${indent}</div>`, "");
|
|
280
|
+
if (block.name === "div") {
|
|
281
|
+
output.push("", `<div ${attributes}>`, "", held, "", "</div>", "");
|
|
404
282
|
cursor = block.end + 1;
|
|
405
283
|
continue;
|
|
406
284
|
}
|
|
@@ -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.
|