@heroiclands/package-build 22.15.1 → 22.16.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 +95 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +631 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4420 -0
- package/docs/reference/note-types.md +531 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +745 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/markdown-expressions.mjs +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +205 -93
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +4 -2
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
package/engine/content-html.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Raw HTML in a note's prose, reported.
|
|
16
16
|
*
|
|
17
17
|
* **A note is markdown.** What markdown cannot say, a note does not say — it
|
|
18
|
-
* gets a construct every surface can render, the way `:icon
|
|
18
|
+
* gets a construct every surface can render, the way `:icon …:` replaces a
|
|
19
19
|
* pasted glyph.
|
|
20
20
|
*
|
|
21
21
|
* ## There is no route from `<p>` to a book
|
package/engine/content-icons.mjs
CHANGED
|
@@ -12,61 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* They are also the worst characters in the corpus to typeset. Of the eight
|
|
26
|
-
* book faces probed, **none** carries `✕ ✗ ✎ ☆ ⚗ ➕`; in a Libertinus
|
|
27
|
-
* setting `✕` resolves to macOS LastResort, which draws a tofu box.
|
|
28
|
-
*
|
|
29
|
-
* **Neither obvious fix works.** Keeping the dingbats pins the book to some
|
|
30
|
-
* icon-capable font forever, which is the coupling this exists to remove.
|
|
31
|
-
* Pasting Font Awesome's own codepoints is worse: they live in the Private Use
|
|
32
|
-
* Area, which is unassigned by definition, so they break search, copy-paste and
|
|
33
|
-
* screen readers, and no charset check can validate them.
|
|
34
|
-
*
|
|
35
|
-
* So a note **names** an icon and never contains one. `:icon-star-outline:` is
|
|
36
|
-
* ASCII, it is greppable, it survives a charset check, and it degrades to
|
|
37
|
-
* visible literal text on any surface that has not been taught to render it —
|
|
38
|
-
* which is the failure mode you want, because you can see it.
|
|
39
|
-
*
|
|
40
|
-
* **Why a registry rather than the Font Awesome classes.** Three surfaces need
|
|
41
|
-
* three different artefacts from one name: the journals and the website want
|
|
42
|
-
* `<i class="fa-solid fa-star">`, and the PDF wants a font file and a glyph.
|
|
43
|
-
* Only a mapping serves both. It also means a Font Awesome major version that
|
|
44
|
-
* renames an icon — `fa-trash-o` became `fa-trash-can` — costs one line here
|
|
45
|
-
* rather than a sweep of the corpus, and it lets an unknown name be *reported*
|
|
46
|
-
* instead of passing silently through as literal text.
|
|
47
|
-
*
|
|
48
|
-
* **The codepoint is deliberately not here.** A renderer that embeds Font
|
|
49
|
-
* Awesome has to read the font to subset it, and the font's own `cmap` is the
|
|
50
|
-
* only trustworthy source for which glyph a name resolves to. Writing the
|
|
51
|
-
* codepoints out by hand would be a second copy of that table, wrong the first
|
|
52
|
-
* time Font Awesome renumbers anything, and wrong silently. This module states
|
|
53
|
-
* the style and the name; the renderer resolves them against the file it ships.
|
|
54
|
-
*
|
|
55
|
-
* **Licence.** Font Awesome Free's icons are CC BY 4.0 and its fonts SIL OFL
|
|
56
|
-
* 1.1, so a distributed PDF may embed the subset it uses. Attribution belongs
|
|
57
|
-
* in the book's colophon, not in every note.
|
|
58
|
-
*
|
|
59
|
-
* **Every finding here is a `warning`**, so nothing this module says can fail a
|
|
60
|
-
* build — `reportFindings` fails on an error and not on a warning. An
|
|
61
|
-
* undeclared name renders as its own literal text, which is visible on the page
|
|
62
|
-
* and wrong in a way a reader will notice; that deserves to be reported and
|
|
63
|
-
* does not deserve to stop a build that is otherwise correct.
|
|
15
|
+
* Resolve named inline font glyphs from a package's icon registry.
|
|
16
|
+
*
|
|
17
|
+
* A note writes `:icon warning:` or `:icon warning:{size=2x}`. Registry keys
|
|
18
|
+
* use lowercase letters, digits and hyphens. The package supplies the font,
|
|
19
|
+
* stylesheet, glyph name and accessible label. Unknown names remain literal
|
|
20
|
+
* and produce a warning; malformed syntax and attributes produce errors.
|
|
21
|
+
*
|
|
22
|
+
* The registry stores glyph names rather than font codepoints. Print reads the
|
|
23
|
+
* font file's character map, while HTML uses the declared style classes.
|
|
64
24
|
*
|
|
65
25
|
* @module
|
|
66
26
|
*/
|
|
67
27
|
|
|
68
28
|
import fs from "node:fs";
|
|
69
29
|
import path from "node:path";
|
|
30
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
70
31
|
|
|
71
32
|
/**
|
|
72
33
|
* A **family** is an icon font, and a consumer declares the ones it ships.
|
|
@@ -117,7 +78,7 @@ import path from "node:path";
|
|
|
117
78
|
*
|
|
118
79
|
* Empty rather than a starter set, because a starter set is a promise about
|
|
119
80
|
* fonts this package does not ship. A tree with no `icons:` configured names no
|
|
120
|
-
* icons, and `:icon
|
|
81
|
+
* icons, and `:icon star:` in one of its notes renders as its own literal text
|
|
121
82
|
* and is reported — which is the visible failure, not a silent one.
|
|
122
83
|
*
|
|
123
84
|
* @type {IconRegistry}
|
|
@@ -203,22 +164,11 @@ export function parseIconAttributes(raw) {
|
|
|
203
164
|
const problems = [];
|
|
204
165
|
if (!raw || !raw.trim()) return { attrs, problems };
|
|
205
166
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
.filter(Boolean)) {
|
|
212
|
-
const colon = part.indexOf(":");
|
|
213
|
-
if (colon === -1) {
|
|
214
|
-
problems.push(
|
|
215
|
-
`\`${part}\` is not a \`key: value\` attribute — an icon takes ` +
|
|
216
|
-
`${Object.keys(ICON_ATTRIBUTES).join(", ")}`,
|
|
217
|
-
);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
const key = part.slice(0, colon).trim();
|
|
221
|
-
const value = part.slice(colon + 1).trim();
|
|
167
|
+
const parsed = parseExtensionAttributes(raw);
|
|
168
|
+
problems.push(...parsed.problems);
|
|
169
|
+
if (parsed.id) problems.push("an icon does not accept an id");
|
|
170
|
+
if (parsed.classes.length) problems.push("an icon does not accept classes");
|
|
171
|
+
for (const [key, value] of Object.entries(parsed.values)) {
|
|
222
172
|
const spec =
|
|
223
173
|
Object.prototype.hasOwnProperty.call(ICON_ATTRIBUTES, key) ?
|
|
224
174
|
ICON_ATTRIBUTES[key]
|
|
@@ -232,7 +182,7 @@ export function parseIconAttributes(raw) {
|
|
|
232
182
|
}
|
|
233
183
|
if (!spec.values.includes(value)) {
|
|
234
184
|
problems.push(
|
|
235
|
-
`\`${key}
|
|
185
|
+
`\`${key}=${value}\` is not one of ${spec.values.join(", ")} — ` +
|
|
236
186
|
`${key} says ${spec.describe}`,
|
|
237
187
|
);
|
|
238
188
|
continue;
|
|
@@ -245,25 +195,16 @@ export function parseIconAttributes(raw) {
|
|
|
245
195
|
/**
|
|
246
196
|
* The shape a note writes, and the one this module claims.
|
|
247
197
|
*
|
|
248
|
-
* The `icon
|
|
249
|
-
*
|
|
250
|
-
* and
|
|
251
|
-
* exists. Names are lowercase, digits and hyphens — the charset an address
|
|
252
|
-
* segment already uses, so nothing new has to be explained.
|
|
253
|
-
*
|
|
254
|
-
* Not `:name[content]`. That is remark-directive syntax, and this toolchain
|
|
255
|
-
* parses with markdown-it; a directive would render as its own literal text.
|
|
198
|
+
* The `icon` keyword distinguishes inline glyphs from asset Addresses and
|
|
199
|
+
* other colon-based shorthand. Names use lowercase letters, digits and
|
|
200
|
+
* hyphens, and each name resolves in the package's declared icon registry.
|
|
256
201
|
*
|
|
257
|
-
* An optional
|
|
202
|
+
* An optional brace carries attributes, for example:
|
|
258
203
|
*
|
|
259
|
-
* :icon
|
|
204
|
+
* :icon affiliation:{size=2x}
|
|
260
205
|
*
|
|
261
|
-
* `key
|
|
262
|
-
*
|
|
263
|
-
* rather than one this module invented. Attributes rather than a bare value
|
|
264
|
-
* because `size` is merely the first one anybody needed: a fixed-width flag, a
|
|
265
|
-
* rotation, a title override are the same shape of thing, and a syntax that
|
|
266
|
-
* could only ever express size would have to be replaced to gain any of them.
|
|
206
|
+
* Attributes are whitespace-separated `key=value` pairs. The closed `size`
|
|
207
|
+
* values are declared in {@link ICON_SIZES}.
|
|
267
208
|
*
|
|
268
209
|
* One inline rule consumes the token **and** its brace, so there is no state in
|
|
269
210
|
* which the icon resolves and the brace is left stranded on the page. An
|
|
@@ -271,12 +212,13 @@ export function parseIconAttributes(raw) {
|
|
|
271
212
|
*
|
|
272
213
|
* @type {RegExp}
|
|
273
214
|
*/
|
|
274
|
-
export const ICON_PATTERN = /:icon
|
|
215
|
+
export const ICON_PATTERN = /:icon ([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/g;
|
|
216
|
+
const OBSOLETE_ICON_PATTERN = /:icon-([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/g;
|
|
275
217
|
|
|
276
218
|
/**
|
|
277
219
|
* Look one name up.
|
|
278
220
|
*
|
|
279
|
-
* @param {string} name - The name written between the colons, without `icon
|
|
221
|
+
* @param {string} name - The name written between the colons, without `icon `.
|
|
280
222
|
* @param {IconRegistry} [registry] - The package's registry.
|
|
281
223
|
* @returns {object|null} The entry, or `null` when the registry does not
|
|
282
224
|
* declare it.
|
|
@@ -355,17 +297,28 @@ export function iconsIn(text) {
|
|
|
355
297
|
* Report every icon a tree names that its registry does not declare.
|
|
356
298
|
*
|
|
357
299
|
* The whole point of a registry is that a typo is answerable, so this is the
|
|
358
|
-
* half that makes `:icon
|
|
300
|
+
* half that makes `:icon stra:` a finding rather than three words of literal
|
|
359
301
|
* text nobody notices in a rendered page.
|
|
360
302
|
*
|
|
361
303
|
* @param {string} text - The file's contents.
|
|
362
304
|
* @param {string} file - Path to report.
|
|
363
305
|
* @param {IconRegistry} [registry] - The package's registry.
|
|
364
306
|
* @returns {Array<{file: string, line: number, column: number,
|
|
365
|
-
* severity: "warning", message: string}>} The
|
|
307
|
+
* severity: "warning"|"error", message: string}>} The findings.
|
|
366
308
|
*/
|
|
367
309
|
export function lintIcons(text, file, registry = EMPTY_ICON_REGISTRY) {
|
|
368
310
|
const findings = [];
|
|
311
|
+
for (const match of text.matchAll(OBSOLETE_ICON_PATTERN)) {
|
|
312
|
+
const index = match.index ?? 0;
|
|
313
|
+
const before = text.slice(0, index);
|
|
314
|
+
findings.push({
|
|
315
|
+
file,
|
|
316
|
+
line: before.split("\n").length,
|
|
317
|
+
column: index - (before.lastIndexOf("\n") + 1) + 1,
|
|
318
|
+
severity: "error",
|
|
319
|
+
message: `\`${match[0]}\` needs the form \`:icon ${match[1]}:\``,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
369
322
|
for (const { name, index, raw, problems } of iconsIn(text)) {
|
|
370
323
|
const before = text.slice(0, index);
|
|
371
324
|
const line = before.split("\n").length;
|
|
@@ -380,7 +333,7 @@ export function lintIcons(text, file, registry = EMPTY_ICON_REGISTRY) {
|
|
|
380
333
|
...at,
|
|
381
334
|
message:
|
|
382
335
|
`\`${raw}\` names an icon the registry does not declare` +
|
|
383
|
-
(suggestion ? `; did you mean \`:icon
|
|
336
|
+
(suggestion ? `; did you mean \`:icon ${suggestion}:\`?` : "") +
|
|
384
337
|
` — an undeclared name renders as its own literal text`,
|
|
385
338
|
});
|
|
386
339
|
// The name is the bigger fault; reporting its attributes as well
|
|
@@ -392,7 +345,7 @@ export function lintIcons(text, file, registry = EMPTY_ICON_REGISTRY) {
|
|
|
392
345
|
// and renders differently from what was asked for, which is the case
|
|
393
346
|
// nobody notices without being told.
|
|
394
347
|
for (const problem of problems) {
|
|
395
|
-
findings.push({ ...at, message: `\`${raw}\`: ${problem}` });
|
|
348
|
+
findings.push({ ...at, severity: "error", message: `\`${raw}\`: ${problem}` });
|
|
396
349
|
}
|
|
397
350
|
}
|
|
398
351
|
return findings;
|
|
@@ -612,11 +565,11 @@ export function lintContentIcons(contentBase, { skipDirectories = [], registry }
|
|
|
612
565
|
}
|
|
613
566
|
|
|
614
567
|
/**
|
|
615
|
-
* A markdown-it plugin rendering `:icon
|
|
568
|
+
* A markdown-it plugin rendering `:icon name:` inline.
|
|
616
569
|
*
|
|
617
570
|
* An unknown name is left **exactly as written** rather than dropped. The name
|
|
618
571
|
* is reported by {@link lintIcons}, and a rendered page that still shows
|
|
619
|
-
* `:icon
|
|
572
|
+
* `:icon stra:` is how the author finds it without reading a log.
|
|
620
573
|
*
|
|
621
574
|
* **A function is accepted as well as a table**, and resolved per render. The
|
|
622
575
|
* shared markdown-it instance is a module-level constant, so it is built before
|
|
@@ -645,7 +598,7 @@ export function iconPlugin(registry = EMPTY_ICON_REGISTRY) {
|
|
|
645
598
|
if (state.src.charCodeAt(start) !== 0x3a /* : */) return false;
|
|
646
599
|
// Anchored at the cursor, so the scan is O(token) rather than a
|
|
647
600
|
// search of the remaining source at every colon in the paragraph.
|
|
648
|
-
const re = /^:icon
|
|
601
|
+
const re = /^:icon ([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/;
|
|
649
602
|
const m = re.exec(state.src.slice(start));
|
|
650
603
|
if (!m) return false;
|
|
651
604
|
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
* no way to say. A directive in the curly-attribute convention Pandoc and
|
|
17
17
|
* Kramdown use closes that:
|
|
18
18
|
*
|
|
19
|
-
* {float
|
|
19
|
+
* {float=top-left}
|
|
20
20
|
* {.full-width}
|
|
21
21
|
*
|
|
22
22
|
* ## Three closed vocabularies, and closed is the point
|
|
23
23
|
*
|
|
24
24
|
* **Width is a class**, and the ordinary width carries no marker at all — the
|
|
25
25
|
* simple case needs no spelling. {@link IMAGE_CLASSES} holds the one class
|
|
26
|
-
* there is. **Position is `float
|
|
27
|
-
* values it takes. **Named size is `size
|
|
26
|
+
* there is. **Position is `float`**, and {@link IMAGE_FLOATS} holds the five
|
|
27
|
+
* values it takes. **Named size is `size`**, and {@link IMAGE_SIZES} holds its
|
|
28
28
|
* values. The HTML surfaces receive a size class; the book receives a print
|
|
29
29
|
* measure.
|
|
30
30
|
*
|
|
@@ -77,6 +77,7 @@ import fs from "node:fs";
|
|
|
77
77
|
import path from "node:path";
|
|
78
78
|
|
|
79
79
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
80
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
80
81
|
import { positionInBody } from "./diagnostics.mjs";
|
|
81
82
|
import { foundryAddressProblem, pathnameProblem, servesFoundry } from "./pathnames.mjs";
|
|
82
83
|
|
|
@@ -117,7 +118,7 @@ export const IMAGE_SIZES = Object.freeze([
|
|
|
117
118
|
]);
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
* The `float
|
|
121
|
+
* The `float` positions an image may take, and where each puts it.
|
|
121
122
|
*
|
|
122
123
|
* `align` is the Typst alignment the float is placed at. **Print cannot wrap
|
|
123
124
|
* text around an arbitrary shape**: a Typst float occupies the column measure,
|
|
@@ -235,7 +236,6 @@ export function parseImageDirective(raw) {
|
|
|
235
236
|
/** @type {string[]} */
|
|
236
237
|
const classes = [];
|
|
237
238
|
let size = "auto";
|
|
238
|
-
let sizeSeen = false;
|
|
239
239
|
let float = "";
|
|
240
240
|
/** @type {string[]} */
|
|
241
241
|
const problems = [];
|
|
@@ -245,75 +245,23 @@ export function parseImageDirective(raw) {
|
|
|
245
245
|
.replace(/\}$/, "");
|
|
246
246
|
if (!inner.trim()) return { classes, size, float, problems };
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (!Object.prototype.hasOwnProperty.call(IMAGE_CLASSES, name)) {
|
|
257
|
-
problems.push(
|
|
258
|
-
`\`.${name}\` is not a width an image has — the width there is ` +
|
|
259
|
-
`is \`.${Object.keys(IMAGE_CLASSES).join("`, `.")}\`, and an image ` +
|
|
260
|
-
"with no class at all is the ordinary width",
|
|
261
|
-
);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (classes.includes(name)) {
|
|
265
|
-
problems.push(`\`.${name}\` is written twice, and an image has one width`);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
classes.push(name);
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const colon = part.indexOf(":");
|
|
273
|
-
if (colon === -1) {
|
|
274
|
-
problems.push(
|
|
275
|
-
`\`${part}\` is neither a width class nor \`size: <name>\` or ` +
|
|
276
|
-
"`float: <position>` — an image states its width as a class and its position as `float:`, " +
|
|
277
|
-
"and it states no dimensions at all",
|
|
278
|
-
);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const key = part.slice(0, colon).trim();
|
|
282
|
-
const value = part.slice(colon + 1).trim();
|
|
248
|
+
const parsed = parseExtensionAttributes(inner);
|
|
249
|
+
problems.push(...parsed.problems);
|
|
250
|
+
if (parsed.id) problems.push("an image does not accept an id");
|
|
251
|
+
for (const name of parsed.classes) {
|
|
252
|
+
if (!Object.hasOwn(IMAGE_CLASSES, name)) problems.push(`.${name} is not an image width`);
|
|
253
|
+
else classes.push(name);
|
|
254
|
+
}
|
|
255
|
+
for (const [key, value] of Object.entries(parsed.values)) {
|
|
283
256
|
if (key === "size") {
|
|
284
|
-
if (!IMAGE_SIZES.includes(value))
|
|
285
|
-
problems.push(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
size = value;
|
|
295
|
-
sizeSeen = true;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (key !== "float") {
|
|
299
|
-
problems.push(
|
|
300
|
-
`\`${key}\` is not an image attribute — use \`size\` or \`float\`, ` +
|
|
301
|
-
"and width is a class rather than an attribute",
|
|
302
|
-
);
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (!Object.prototype.hasOwnProperty.call(IMAGE_FLOATS, value)) {
|
|
306
|
-
problems.push(
|
|
307
|
-
`\`float: ${value}\` is not a position — the ones there are: ` +
|
|
308
|
-
`${Object.keys(IMAGE_FLOATS).join(", ")}`,
|
|
309
|
-
);
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (float) {
|
|
313
|
-
problems.push("`float:` is written twice, and an image sits in one place");
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
float = value;
|
|
257
|
+
if (!IMAGE_SIZES.includes(value))
|
|
258
|
+
problems.push(`size=${value} is not a named image size`);
|
|
259
|
+
else size = value;
|
|
260
|
+
} else if (key === "float") {
|
|
261
|
+
if (!Object.hasOwn(IMAGE_FLOATS, value))
|
|
262
|
+
problems.push(`float=${value} is not an image position`);
|
|
263
|
+
else float = value;
|
|
264
|
+
} else problems.push(`${key} is not an image attribute`);
|
|
317
265
|
}
|
|
318
266
|
|
|
319
267
|
if (classes.length > 1) {
|
|
@@ -666,7 +614,7 @@ export function renderImageFigures(body, resolveSrc = (src) => src) {
|
|
|
666
614
|
* A markdown-it plugin that reads an image's directive and renders its figure.
|
|
667
615
|
*
|
|
668
616
|
* **A core rule, not an inline one.** markdown-it's own `image` rule consumes
|
|
669
|
-
* `` and leaves `{float
|
|
617
|
+
* `` and leaves `{float=top-left}` behind as text, and a rule
|
|
670
618
|
* running before it would have to re-implement link parsing to find the brace.
|
|
671
619
|
* Reading the token stream afterwards costs one pass and re-implements nothing.
|
|
672
620
|
*
|
package/engine/content-index.mjs
CHANGED
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
* one type is ordinary authoring. Any format that fixes a column set would turn
|
|
34
34
|
* that authoring into a schema migration, so nothing here selects, flattens, or
|
|
35
35
|
* renames — a reader addresses `sohl.body.weight.base` because that is what the
|
|
36
|
-
* note says, which
|
|
37
|
-
* writes.
|
|
36
|
+
* note says, which a SQL query can address directly.
|
|
38
37
|
*
|
|
39
38
|
* **JSON Lines rather than a database.** The artifact has to survive its build
|
|
40
39
|
* and be usable by anything — a person with `jq`, an editor, a CI check,
|
|
@@ -46,7 +45,7 @@
|
|
|
46
45
|
* nested access — whereas a stored schema would forfeit the open shape.
|
|
47
46
|
*
|
|
48
47
|
* **Byte-stable, because it is meant to be rebuilt.** {@link emitContentIndex}
|
|
49
|
-
* is reachable on its own (`
|
|
48
|
+
* is reachable on its own (`package-build content-index`) and costs a
|
|
50
49
|
* frontmatter parse, not a build, so the honest expectation is that anyone
|
|
51
50
|
* regenerates it whenever they want rather than treating it as precious. That
|
|
52
51
|
* only holds if two runs over an unchanged tree produce an identical file, so
|
|
@@ -152,6 +151,7 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
152
151
|
// a record is built, so the index carries it without the note being written to.
|
|
153
152
|
import { applyComputedBeingAge, presentAmongFrontmatters } from "./being-age.mjs";
|
|
154
153
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
154
|
+
import { resolvedDateFields } from "./note-dates.mjs";
|
|
155
155
|
|
|
156
156
|
export {
|
|
157
157
|
authoredFrontmatter,
|
|
@@ -441,6 +441,8 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
|
|
|
441
441
|
* @param {number} [options.bodyLine] - The 1-based file line the body starts on.
|
|
442
442
|
* @param {object} [options.manifest] - The package manifest, which the Foundry
|
|
443
443
|
* entries are derived against.
|
|
444
|
+
* @param {object} [options.addressContext] - Address resolution context.
|
|
445
|
+
* @param {object} [options.dateContext] - Calendar conversion context.
|
|
444
446
|
* @returns {Record<string, any>} The record, keys sorted at every depth. A
|
|
445
447
|
* **stub** — a note with an empty body, on a type an empty body suppresses —
|
|
446
448
|
* carries `address` and `anchors` as `null`: it publishes no page, so it
|
|
@@ -461,6 +463,7 @@ export function buildIndexRecord({
|
|
|
461
463
|
bodyLine,
|
|
462
464
|
manifest,
|
|
463
465
|
addressContext,
|
|
466
|
+
dateContext,
|
|
464
467
|
}) {
|
|
465
468
|
assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage);
|
|
466
469
|
|
|
@@ -494,11 +497,13 @@ export function buildIndexRecord({
|
|
|
494
497
|
// its file is what the author edits and what every diagnostic names, and
|
|
495
498
|
// its `id` is the identity every diagnostic and every table reads.
|
|
496
499
|
const stub = isStubNote(frontmatter, body);
|
|
500
|
+
const resolvedDates = dateContext ? resolvedDateFields(frontmatter, dateContext) : {};
|
|
497
501
|
|
|
498
502
|
return /** @type {Record<string, any>} */ (
|
|
499
503
|
decodeIndexAddresses(
|
|
500
504
|
{
|
|
501
505
|
...frontmatter,
|
|
506
|
+
...(Object.keys(resolvedDates).length ? { resolvedDates } : {}),
|
|
502
507
|
package: contentPackage,
|
|
503
508
|
address: stub ? null : address,
|
|
504
509
|
nameAscii: asciiName(frontmatter?.name?.full),
|
|
@@ -609,6 +614,7 @@ export function indexRecordsForNote({
|
|
|
609
614
|
contentPackage,
|
|
610
615
|
manifest,
|
|
611
616
|
addressContext,
|
|
617
|
+
dateContext,
|
|
612
618
|
}) {
|
|
613
619
|
resolveNoteId(frontmatter, { pkg: contentPackage });
|
|
614
620
|
const record = buildIndexRecord({
|
|
@@ -620,6 +626,7 @@ export function indexRecordsForNote({
|
|
|
620
626
|
contentPackage,
|
|
621
627
|
manifest,
|
|
622
628
|
addressContext,
|
|
629
|
+
dateContext,
|
|
623
630
|
});
|
|
624
631
|
const records = [decodeIndexAddresses(record, addressContext ?? { package: contentPackage })];
|
|
625
632
|
const address = noteAddress(frontmatter, contentPackage);
|
|
@@ -663,6 +670,7 @@ export function indexRecordsForNote({
|
|
|
663
670
|
* anyway.
|
|
664
671
|
* @param {object} [options.manifest] - The package manifest, which the Foundry
|
|
665
672
|
* entries are derived against.
|
|
673
|
+
* @param {object} [options.addressContext] - Address resolution context.
|
|
666
674
|
* @param {object[]} [options.problems] - Supplied by a **reader**: a note that
|
|
667
675
|
* cannot be recorded is pushed here as a diagnostic and skipped. Omitted, the
|
|
668
676
|
* note throws — the contract the emitter needs, since an index missing a note
|
|
@@ -710,6 +718,7 @@ export function collectContentIndex(
|
|
|
710
718
|
bodyLine,
|
|
711
719
|
manifest,
|
|
712
720
|
addressContext,
|
|
721
|
+
dateContext: dates,
|
|
713
722
|
}),
|
|
714
723
|
);
|
|
715
724
|
} catch (err) {
|
package/engine/content-links.mjs
CHANGED
|
@@ -78,8 +78,8 @@ import { foundryAddressProblem, servesFoundry } from "./pathnames.mjs";
|
|
|
78
78
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
79
79
|
import { ownDocumentSystem } from "./address.mjs";
|
|
80
80
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
81
|
+
import { isGmNote } from "./note-vocabulary.mjs";
|
|
81
82
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
82
|
-
import { searchableFrontmatter } from "./note-package.mjs";
|
|
83
83
|
import {
|
|
84
84
|
blockSystem,
|
|
85
85
|
canonicalKey,
|
|
@@ -301,18 +301,8 @@ export function buildLinkIndex(
|
|
|
301
301
|
foreign: foreign.index,
|
|
302
302
|
};
|
|
303
303
|
|
|
304
|
-
/** The searchable universe a `dataview` table draws its rows from. */
|
|
305
|
-
const tableDocs = notes.map((n) => ({
|
|
306
|
-
// Package present for a `WHERE … package = "…"` clause, synthesised
|
|
307
|
-
// rather than authored — see {@link searchableFrontmatter}.
|
|
308
|
-
fm: searchableFrontmatter(n.fm, pkg),
|
|
309
|
-
path: n.rel,
|
|
310
|
-
tld: n.rel.split("/")[0],
|
|
311
|
-
folder: path.dirname(n.rel).split("/").pop(),
|
|
312
|
-
}));
|
|
313
|
-
|
|
314
304
|
/**
|
|
315
|
-
* One note's body with its
|
|
305
|
+
* One note's body with its SQL tables expanded.
|
|
316
306
|
*
|
|
317
307
|
* The body every body-level check reads, so a link and an embed in one note
|
|
318
308
|
* are found in the same text — a generated table is as free to carry either
|
|
@@ -323,12 +313,8 @@ export function buildLinkIndex(
|
|
|
323
313
|
*/
|
|
324
314
|
function expandedBody(note) {
|
|
325
315
|
const body = note.body;
|
|
326
|
-
if (!/^[ \t]*(?:`{3,}|~{3,})[ \t]*
|
|
316
|
+
if (!/^[ \t]*(?:`{3,}|~{3,})[ \t]*sql\b/im.test(body)) return body;
|
|
327
317
|
return expandContentTables(body, {
|
|
328
|
-
// Unfiltered: every note in the tree is this package's, so
|
|
329
|
-
// there is no other package's note to exclude.
|
|
330
|
-
docs: tableDocs,
|
|
331
|
-
linkable: (d) => Boolean(d.fm.shortcode),
|
|
332
318
|
source: note.file,
|
|
333
319
|
// A `sql` table's links are checked like an authored one's, so
|
|
334
320
|
// its rows are prepared ahead of this walk — see
|
|
@@ -392,7 +378,7 @@ export function buildLinkIndex(
|
|
|
392
378
|
}
|
|
393
379
|
|
|
394
380
|
/**
|
|
395
|
-
* Every wikilink in a note body, with its
|
|
381
|
+
* Every wikilink in a note body, with its SQL tables expanded.
|
|
396
382
|
*
|
|
397
383
|
* An `![[…]]` embed is not one: it names a file rather than a note, and
|
|
398
384
|
* {@link module:engine/wikilink-syntax.WIKILINK} excludes it so that no
|
|
@@ -779,9 +765,8 @@ function readAddress(url, packages) {
|
|
|
779
765
|
*
|
|
780
766
|
* **Why the homepage needs its own audit at all.** Every other note addresses
|
|
781
767
|
* the corpus with wikilinks, which {@link auditLinks} resolves. A homepage does
|
|
782
|
-
* not and cannot: it is published *verbatim*
|
|
783
|
-
*
|
|
784
|
-
* tree is never walked and there is no index for a wikilink to resolve against.
|
|
768
|
+
* not and cannot: it is published *verbatim* even when the tree contains other
|
|
769
|
+
* notes. Its body never runs through the wikilink resolver.
|
|
785
770
|
* So a homepage addresses the web the way the web does — markdown links in its
|
|
786
771
|
* body — and this is what looks at those. A dead link on the page a reader
|
|
787
772
|
* arrives at is the one nothing else would report.
|
|
@@ -796,8 +781,7 @@ function readAddress(url, packages) {
|
|
|
796
781
|
* is why every one is reported — including a bare `/<package>/`, which names
|
|
797
782
|
* another package's front page. A front page's address *is* its package
|
|
798
783
|
* prefix, so `/<package>/` is the absolute URL with the host struck off —
|
|
799
|
-
* host-free, emitted verbatim, and needing no index
|
|
800
|
-
* hold in homepage-only mode where the tree is never walked.
|
|
784
|
+
* host-free, emitted verbatim, and needing no index.
|
|
801
785
|
* - A **wikilink**, which nothing on this page will ever resolve.
|
|
802
786
|
*
|
|
803
787
|
* **What is not checkable, and is not attempted.** Whether an external URL
|
|
@@ -842,7 +826,7 @@ export function auditHomepageLinks(index) {
|
|
|
842
826
|
all,
|
|
843
827
|
at(all),
|
|
844
828
|
`wikilink ${all} on the package homepage — a homepage is ` +
|
|
845
|
-
`published verbatim
|
|
829
|
+
`published verbatim, so nothing ` +
|
|
846
830
|
`resolves it; write a markdown link, package-relative`,
|
|
847
831
|
);
|
|
848
832
|
}
|
|
@@ -1004,7 +988,13 @@ export function auditLinks(index) {
|
|
|
1004
988
|
deadAddresses.push({ ...at, reason: "not-an-address" });
|
|
1005
989
|
continue;
|
|
1006
990
|
}
|
|
1007
|
-
|
|
991
|
+
const resolved = index.resolveAddress(target);
|
|
992
|
+
if (resolved) {
|
|
993
|
+
if ((isGmNote(resolved.fm) || resolved.gm) && !isGmNote(note.fm)) {
|
|
994
|
+
deadAddresses.push({ ...at, reason: "gm" });
|
|
995
|
+
}
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
1008
998
|
// A stub is in the index, so the refusal can name it. Asked before
|
|
1009
999
|
// the foreign manifests, because a local note is what the author
|
|
1010
1000
|
// meant and reporting it as an unresolved foreign address would
|
|
@@ -1019,7 +1009,12 @@ export function auditLinks(index) {
|
|
|
1019
1009
|
// a fully qualified target names one package, so there is nothing
|
|
1020
1010
|
// left to disambiguate, and no ambiguity finding this resolver can
|
|
1021
1011
|
// report.
|
|
1022
|
-
|
|
1012
|
+
const manifest = manifestHit(target);
|
|
1013
|
+
if (manifest) {
|
|
1014
|
+
if (manifest.gm && !isGmNote(note.fm)) {
|
|
1015
|
+
deadAddresses.push({ ...at, reason: "gm" });
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1023
1018
|
usedManifest.add(encodeAddresses(target).toLowerCase());
|
|
1024
1019
|
continue;
|
|
1025
1020
|
}
|
package/engine/content-lint.mjs
CHANGED
|
@@ -394,11 +394,7 @@ export function lintContentTree(
|
|
|
394
394
|
// thing this guard needs them to do. A tree holding notes is therefore a
|
|
395
395
|
// tree; only a tree holding none is the absent one.
|
|
396
396
|
//
|
|
397
|
-
//
|
|
398
|
-
// by the package rather than by a slug — so a `publish.site: homepage`
|
|
399
|
-
// package had a tree with exactly one note and no key at all. It carries an
|
|
400
|
-
// address like every other note now; the guard is unchanged, because
|
|
401
|
-
// what it reads was never the key count.
|
|
397
|
+
// A tree containing only a homepage still contains one note.
|
|
402
398
|
if (notes.length === 0) {
|
|
403
399
|
findings.push({
|
|
404
400
|
file: path.relative(process.cwd(), contentBase) || contentBase,
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import markdownit from "markdown-it";
|
|
7
|
+
import footnotePlugin from "markdown-it-footnote";
|
|
8
|
+
import deflistPlugin from "markdown-it-deflist";
|
|
7
9
|
|
|
8
|
-
const webMarkdown = markdownit({ html: true });
|
|
10
|
+
const webMarkdown = markdownit({ html: true }).use(footnotePlugin).use(deflistPlugin);
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Render whole-line `:::secret` blocks for one publishing surface.
|