@heroiclands/package-build 25.6.2 → 25.7.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 +23 -0
- package/README.md +19 -1
- package/bin/content-commands.mjs +64 -79
- package/bin/package-build.mjs +25 -10
- package/ci/wip-workflows.test.mjs +252 -0
- package/content-config.mjs +118 -8
- package/docs/api.md +237 -47
- package/docs/commands.md +99 -35
- package/docs/configuration.md +93 -44
- package/docs/guides/release-and-deploy.md +26 -14
- package/docs/guides/site.md +53 -0
- package/engine/cdn-publish.mjs +164 -6
- package/engine/content-index.mjs +71 -19
- package/engine/content-spans.mjs +18 -1
- package/engine/content-styles.mjs +85 -0
- package/engine/derived-sections.mjs +4 -3
- package/engine/event-views.mjs +6 -4
- package/engine/helpers.mjs +33 -9
- package/engine/index.mjs +1 -0
- package/engine/note-cache.mjs +1107 -0
- package/engine/note-page.mjs +424 -0
- package/engine/pack-config.mjs +47 -4
- package/engine/pathnames.mjs +22 -7
- package/engine/prepared-only.mjs +32 -0
- package/engine/preview-index.mjs +55 -13
- package/engine/site-build.mjs +34 -448
- package/engine/site-config.mjs +33 -4
- package/engine/site-maps.mjs +16 -24
- package/engine/site-origins.mjs +23 -0
- package/engine/site-page.mjs +648 -0
- package/engine/site-render.mjs +142 -10
- package/engine/site-serve.mjs +1 -1
- package/engine/sql-tables.mjs +12 -3
- package/package.json +6 -1
- package/site-theme/static/css/style.css +14 -0
- package/types/content-config.d.mts +34 -0
- package/types/engine/cdn-publish.d.mts +22 -0
- package/types/engine/content-index.d.mts +36 -2
- package/types/engine/content-styles.d.mts +20 -0
- package/types/engine/derived-sections.d.mts +3 -3
- package/types/engine/event-views.d.mts +4 -3
- package/types/engine/helpers.d.mts +18 -1
- package/types/engine/index.d.mts +1 -0
- package/types/engine/note-cache.d.mts +149 -0
- package/types/engine/note-page.d.mts +92 -0
- package/types/engine/pack-config.d.mts +30 -0
- package/types/engine/prepared-only.d.mts +14 -0
- package/types/engine/preview-index.d.mts +35 -0
- package/types/engine/site-build.d.mts +5 -100
- package/types/engine/site-config.d.mts +3 -2
- package/types/engine/site-maps.d.mts +7 -11
- package/types/engine/site-origins.d.mts +9 -0
- package/types/engine/site-page.d.mts +261 -0
- package/types/engine/site-render.d.mts +20 -1
- package/types/engine/sql-tables.d.mts +8 -4
package/engine/preview-index.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { embedRole } from "./content-embeds.mjs";
|
|
|
31
31
|
import { renderImageFigures } from "./content-images.mjs";
|
|
32
32
|
import { runContext } from "./content-scripts.mjs";
|
|
33
33
|
import { eventNoteIndex } from "./event-fields.mjs";
|
|
34
|
-
import { isHomepage } from "./homepage.mjs";
|
|
34
|
+
import { homepageTitle, isHomepage } from "./homepage.mjs";
|
|
35
35
|
import { authoredFrontmatter, isNoteRecord, isStub } from "./index-records.mjs";
|
|
36
36
|
import { noContentIndexPackages } from "./metadata-index.mjs";
|
|
37
37
|
import { isGmNote } from "./note-vocabulary.mjs";
|
|
@@ -57,32 +57,56 @@ import { siteBase } from "../content-config.mjs";
|
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* What a page of the package resolves against, from its content-index records.
|
|
61
|
+
*
|
|
62
|
+
* The site build reads the same answers from its whole tree; this derives
|
|
63
|
+
* them from the records alone, so a single note renders as its page does
|
|
64
|
+
* without the tree. Each note that publishes a page is an entry at the URL
|
|
65
|
+
* the site gives it: the homepage at the package root, a note
|
|
66
|
+
* `site.sections` names at its section, and every other at its address.
|
|
61
67
|
*
|
|
62
68
|
* @param {object} options
|
|
63
69
|
* @param {object} options.config - The resolved package configuration.
|
|
64
|
-
* @param {object[]} options.records - The content index's records
|
|
65
|
-
* and every asset.
|
|
70
|
+
* @param {object[]} options.records - The content index's records.
|
|
66
71
|
* @param {object} [options.foreign] - The dependencies' indexes, as
|
|
67
72
|
* {@link module:engine/metadata-index.loadForeignIndexes} returns them.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
73
|
+
* @param {(record: object) => object[]|undefined} [options.anchorsOf] - A
|
|
74
|
+
* note's anchors as its page emits them, where they differ from the
|
|
75
|
+
* record's: those of its body with its query answers and generated sections
|
|
76
|
+
* expanded. Absent, or answering nothing, the record's own.
|
|
77
|
+
* @returns {{site: object, assets: object, events: Map<string, object>, runs: object,
|
|
78
|
+
* pages: Map<string, object>}} The link index, the asset address space,
|
|
79
|
+
* every note's events, the package's marked-text context, and the record
|
|
80
|
+
* behind each page by its URL.
|
|
70
81
|
*/
|
|
71
|
-
export function
|
|
82
|
+
export function recordSiteContext({ config, records, foreign, anchorsOf = () => undefined }) {
|
|
72
83
|
const base = siteBase(config);
|
|
84
|
+
const sections = new Map(
|
|
85
|
+
Object.entries(config.site?.sections ?? {}).map(([name, address]) => [address, name]),
|
|
86
|
+
);
|
|
73
87
|
const entries = [];
|
|
88
|
+
const pages = new Map();
|
|
74
89
|
for (const record of records) {
|
|
75
90
|
if (!isNoteRecord(record) || isStub(record)) continue;
|
|
76
91
|
const fm = authoredFrontmatter(record);
|
|
77
92
|
if (isGmNote(fm) || typeof fm.shortcode !== "string" || !fm.shortcode) continue;
|
|
93
|
+
const slug = addressSlug(fm);
|
|
94
|
+
const home = isHomepage(fm);
|
|
95
|
+
const url =
|
|
96
|
+
home ? base
|
|
97
|
+
: sections.has(slug) ? `${base}${sections.get(slug)}/`
|
|
98
|
+
: `${base}${slug}/`;
|
|
99
|
+
pages.set(url, record);
|
|
78
100
|
entries.push({
|
|
79
101
|
kind: "content",
|
|
80
102
|
fm,
|
|
81
103
|
body: "",
|
|
82
|
-
anchors: record.anchors ?? [],
|
|
104
|
+
anchors: anchorsOf(record) ?? record.anchors ?? [],
|
|
83
105
|
pkg: record.package ?? config.contentPackage,
|
|
84
|
-
name:
|
|
85
|
-
|
|
106
|
+
name:
|
|
107
|
+
fm.name?.full ??
|
|
108
|
+
(home ? homepageTitle(fm, config) : (record.file?.name ?? fm.shortcode)),
|
|
109
|
+
url,
|
|
86
110
|
});
|
|
87
111
|
}
|
|
88
112
|
const site = buildSiteIndex(entries, {
|
|
@@ -99,6 +123,26 @@ export function previewIndex({ config, records, foreign }) {
|
|
|
99
123
|
foreignIndex: foreign?.index,
|
|
100
124
|
dates: site.dateContext,
|
|
101
125
|
});
|
|
126
|
+
const runs = runContext(config, records, {
|
|
127
|
+
referenceName: (target) => resolveInfoboxRef(site, target)?.name,
|
|
128
|
+
});
|
|
129
|
+
return { site, assets, events, runs, pages };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build the object a preview with an index renders from.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} options
|
|
136
|
+
* @param {object} options.config - The resolved package configuration.
|
|
137
|
+
* @param {object[]} options.records - The content index's records: every note
|
|
138
|
+
* and every asset.
|
|
139
|
+
* @param {object} [options.foreign] - The dependencies' indexes, as
|
|
140
|
+
* {@link module:engine/metadata-index.loadForeignIndexes} returns them.
|
|
141
|
+
* Omitted, the package links only to itself.
|
|
142
|
+
* @returns {PreviewIndex} The index.
|
|
143
|
+
*/
|
|
144
|
+
export function previewIndex({ config, records, foreign }) {
|
|
145
|
+
const { site, assets, events, runs } = recordSiteContext({ config, records, foreign });
|
|
102
146
|
const resolveRole = (address) => embedRole(assets, address);
|
|
103
147
|
const webSrc = (src) =>
|
|
104
148
|
pathnameProblem(src) ? src : (resolvePathname(src, config)?.web ?? src);
|
|
@@ -107,9 +151,7 @@ export function previewIndex({ config, records, foreign }) {
|
|
|
107
151
|
return key ? assets?.byPath?.get(key) : undefined;
|
|
108
152
|
};
|
|
109
153
|
return {
|
|
110
|
-
runs
|
|
111
|
-
referenceName: (target) => resolveInfoboxRef(site, target)?.name,
|
|
112
|
-
}),
|
|
154
|
+
runs,
|
|
113
155
|
icons: config.icons,
|
|
114
156
|
lookupAsset,
|
|
115
157
|
resolveSrc: webSrc,
|
package/engine/site-build.mjs
CHANGED
|
@@ -51,61 +51,49 @@
|
|
|
51
51
|
* @module
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import { scanPageBreaks } from "./page-breaks.mjs";
|
|
55
|
-
import { scanAlerts } from "./content-alerts.mjs";
|
|
56
|
-
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
57
|
-
import { decodeNoteAddresses, noteAddressContext } from "./note-addresses.mjs";
|
|
58
54
|
import fs from "node:fs";
|
|
59
55
|
import path from "node:path";
|
|
60
|
-
import { createRequire } from "node:module";
|
|
61
|
-
import { readMatter } from "./note-matter.mjs";
|
|
62
56
|
|
|
63
57
|
import { addressSlug } from "./content-address.mjs";
|
|
64
|
-
import { expandContent } from "./content-expand.mjs";
|
|
65
58
|
import { eventNoteIndex } from "./event-fields.mjs";
|
|
66
59
|
import { expandContentTables } from "./content-tables.mjs";
|
|
67
|
-
import {
|
|
68
|
-
import { runContext, scanRuns } from "./content-scripts.mjs";
|
|
69
|
-
import { scanBlocks } from "./content-blocks.mjs";
|
|
70
|
-
import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
|
|
60
|
+
import { runContext } from "./content-scripts.mjs";
|
|
71
61
|
import { scanFigures } from "./content-figures.mjs";
|
|
72
|
-
import {
|
|
73
|
-
import { markupAnchorFindings } from "./anchors.mjs";
|
|
74
|
-
import { checkImages, renderImageFigures } from "./content-images.mjs";
|
|
75
|
-
import { isAddressTuple } from "./address.mjs";
|
|
76
|
-
import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
62
|
+
import { resolvePathname } from "./pathnames.mjs";
|
|
77
63
|
import { buildSiteIndex, figureIndexKeys, resolveInfoboxRef } from "./site-index.mjs";
|
|
78
|
-
import { frontmatterWikilinks } from "./wikilinks.mjs";
|
|
79
64
|
import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
|
|
80
|
-
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
81
65
|
import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
|
|
82
66
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
83
|
-
import { routerFor } from "./pack-router.mjs";
|
|
84
67
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
85
68
|
// The corpus, from the one pass that derives it.
|
|
86
69
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
87
|
-
// A being's computed `age` — see `engine/being-age.mjs`. `pdf-build.mjs`
|
|
88
|
-
// reuses `collectContentPages`'s pages, so wiring it here reaches the book too.
|
|
89
|
-
import { applyComputedBeingAge } from "./being-age.mjs";
|
|
90
70
|
import { normalizeCurrentDate } from "./current-date.mjs";
|
|
91
71
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
92
|
-
import { resolvedDateFields } from "./note-dates.mjs";
|
|
93
72
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
94
|
-
// The one statement of what an empty body means, shared with the index.
|
|
95
|
-
import { isStubNote } from "./note-state.mjs";
|
|
96
73
|
import { isGmNote } from "./note-vocabulary.mjs";
|
|
97
|
-
import {
|
|
74
|
+
import { artPathname, assetAddressIndex } from "./art-fields.mjs";
|
|
98
75
|
import { embedRole } from "./content-embeds.mjs";
|
|
99
76
|
import { checkHomepageCount, homepageFrontmatter, homepageTitle, isHomepage } from "./homepage.mjs";
|
|
100
77
|
import { publishesContentPages, siteBase } from "../content-config.mjs";
|
|
101
78
|
import { seeAlsoPages } from "./see-also.mjs";
|
|
102
79
|
import { homepageLinkTargets, relatedPages } from "./related-pages.mjs";
|
|
103
|
-
import {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
80
|
+
import { drawSiteMaps } from "./site-maps.mjs";
|
|
81
|
+
// One page's render, shared with the single-note render of `./note-page`.
|
|
82
|
+
// A being's computed `age` is applied there — see `engine/being-age.mjs`;
|
|
83
|
+
// `pdf-build.mjs` reuses `collectContentPages`'s pages, so it reaches the book too.
|
|
84
|
+
import {
|
|
85
|
+
contentPage,
|
|
86
|
+
isPlainObject,
|
|
87
|
+
pageFrontmatter,
|
|
88
|
+
pageTitle,
|
|
89
|
+
parseNote,
|
|
90
|
+
renderSitePage,
|
|
91
|
+
resolveArtFields,
|
|
92
|
+
resolveSitePass,
|
|
93
|
+
sitePageDecorator,
|
|
94
|
+
} from "./site-page.mjs";
|
|
107
95
|
|
|
108
|
-
|
|
96
|
+
export { pageFrontmatter, renderSitePage, resolveSitePass, sitePageDecorator };
|
|
109
97
|
|
|
110
98
|
/**
|
|
111
99
|
* The content-tree files this build publishes from, in the order it emits them.
|
|
@@ -150,33 +138,13 @@ function siteCorpusFiles(contentBase, ctx) {
|
|
|
150
138
|
* @returns {{fm: object, body: string}|null}
|
|
151
139
|
*/
|
|
152
140
|
function readNote(file, ctx) {
|
|
153
|
-
let parsed;
|
|
154
141
|
let source;
|
|
155
142
|
try {
|
|
156
143
|
source = fs.readFileSync(file, "utf8");
|
|
157
|
-
parsed = readMatter(source);
|
|
158
144
|
} catch {
|
|
159
145
|
return null;
|
|
160
146
|
}
|
|
161
|
-
|
|
162
|
-
...noteAddressContext(ctx.config ?? {}),
|
|
163
|
-
package: ctx.contentPackage ?? ctx.config?.contentPackage,
|
|
164
|
-
};
|
|
165
|
-
try {
|
|
166
|
-
return {
|
|
167
|
-
fm: decodeNoteAddresses(parsed.data, context),
|
|
168
|
-
body: parsed.content,
|
|
169
|
-
bodyLine: source.slice(0, source.length - parsed.content.length).split("\n").length,
|
|
170
|
-
};
|
|
171
|
-
} catch (error) {
|
|
172
|
-
const position = positionOfYamlPath(parsed.matter, error.keyPath ?? [], {
|
|
173
|
-
key: error.addressKey,
|
|
174
|
-
});
|
|
175
|
-
if (position.line) position.line++;
|
|
176
|
-
error.file = file;
|
|
177
|
-
error.position = position;
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
147
|
+
return parseNote(source, file, ctx);
|
|
180
148
|
}
|
|
181
149
|
|
|
182
150
|
/**
|
|
@@ -219,76 +187,19 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
219
187
|
if (ctx.only && file !== ctx.only) continue;
|
|
220
188
|
const note = readNote(file, ctx);
|
|
221
189
|
if (!note) continue;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// The configuration's, never a note's: a note declares no `package:`, so every
|
|
225
|
-
// note in the tree belongs to the package this repository compiles.
|
|
226
|
-
const pkg = ctx.contentPackage;
|
|
227
|
-
if (!ctx.packages.has(pkg) || !fm.type) continue;
|
|
228
|
-
// A homepage is addressed like any other note, but it is
|
|
229
|
-
// gathered by {@link collectHomepages} rather than here: it is the
|
|
230
|
-
// whole of a homepage-only build, which never walks the tree for
|
|
231
|
-
// content pages at all.
|
|
232
|
-
if (isHomepage(fm)) continue;
|
|
233
|
-
// A stub has no body, so it has no page. Asked of the note's own text
|
|
234
|
-
// rather than of the absent address the index derived, so the site and
|
|
235
|
-
// the index apply one rule from one module and a note that cannot be
|
|
236
|
-
// addressed for some *other* reason still reaches the finding below
|
|
237
|
-
// that says so.
|
|
238
|
-
if (isStubNote(fm, body) || isGmNote(fm)) continue;
|
|
239
|
-
|
|
240
|
-
for (const hit of frontmatterWikilinks(fm)) {
|
|
241
|
-
fmLinkFindings.push({ file, ...hit });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const name = fm.name?.full ?? path.basename(file, ".md");
|
|
245
|
-
// The page's address, and therefore its URL. `name` is the
|
|
246
|
-
// display string and nothing else: it titles the page and labels an
|
|
247
|
-
// inbound link, and moving it moves no address.
|
|
248
|
-
let slug;
|
|
249
|
-
try {
|
|
250
|
-
slug = addressSlug(fm);
|
|
251
|
-
} catch (err) {
|
|
252
|
-
addressFindings.push({ file, reason: err.message });
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const base = path.basename(file);
|
|
257
|
-
const rel = path.relative(contentBase, file);
|
|
258
|
-
pages.push({
|
|
259
|
-
kind: "content",
|
|
260
|
-
fm,
|
|
261
|
-
// The note's own path on disk. Carried so a link finding can be
|
|
262
|
-
// reported as `file:line:column:` against the source an author
|
|
263
|
-
// edits, rather than against the page this build emits.
|
|
190
|
+
const made = contentPage(note, {
|
|
191
|
+
...ctx,
|
|
264
192
|
file,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// — reads one configured value and never frontmatter.
|
|
268
|
-
pkg,
|
|
269
|
-
body,
|
|
270
|
-
bodyLine,
|
|
271
|
-
name,
|
|
272
|
-
slug,
|
|
273
|
-
base,
|
|
274
|
-
// Top-level content directory ("Rules", "Skills", …). Wikilinks do
|
|
275
|
-
// not use it: a note is addressed as `type/shortcode` wherever it
|
|
276
|
-
// is filed.
|
|
277
|
-
tld: rel.split(path.sep)[0],
|
|
278
|
-
// Location below the content root, POSIX-separated.
|
|
279
|
-
relPath: rel.split(path.sep).join("/"),
|
|
280
|
-
// The immediate source subfolder — the page's `kbfolder`.
|
|
193
|
+
fileBase: path.basename(file),
|
|
194
|
+
relPath: path.relative(contentBase, file).split(path.sep).join("/"),
|
|
281
195
|
folder: path.basename(path.dirname(file)),
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
// A note `site.sections` names is the page of its section, and is
|
|
286
|
-
// published at the section rather than at its own address.
|
|
287
|
-
url:
|
|
288
|
-
ctx.sectionPages?.has(slug) ?
|
|
289
|
-
`${ctx.base}${ctx.sectionPages.get(slug)}/`
|
|
290
|
-
: `${ctx.base}${slug}/`,
|
|
196
|
+
present,
|
|
197
|
+
dates,
|
|
291
198
|
});
|
|
199
|
+
if (!made) continue;
|
|
200
|
+
fmLinkFindings.push(...made.fmLinks);
|
|
201
|
+
if (made.addressFinding) addressFindings.push(made.addressFinding);
|
|
202
|
+
else pages.push(made.page);
|
|
292
203
|
}
|
|
293
204
|
return { pages, addressFindings, fmLinkFindings };
|
|
294
205
|
}
|
|
@@ -574,281 +485,6 @@ export function tableUniverse(pages) {
|
|
|
574
485
|
return byPackage;
|
|
575
486
|
}
|
|
576
487
|
|
|
577
|
-
/**
|
|
578
|
-
* The frontmatter a page publishes with.
|
|
579
|
-
*
|
|
580
|
-
* A content page carries the package the build **derived**. No note
|
|
581
|
-
* declares one, so the note's frontmatter alone would publish a page that does
|
|
582
|
-
* not say which package it belongs to. Writing the derived value keeps a page
|
|
583
|
-
* self-describing and makes sweeping the field out of a content tree
|
|
584
|
-
* output-preserving for a site as it already is for the packs.
|
|
585
|
-
*
|
|
586
|
-
* **`related` is derived, never authored.** What links to a page is a fact
|
|
587
|
-
* about every other page, known only once the whole tree has resolved — see
|
|
588
|
-
* {@link module:engine/related-pages} — so an authored value is dropped, and
|
|
589
|
-
* {@link renderPages} sets the derived block once it holds the graph.
|
|
590
|
-
*
|
|
591
|
-
* @param {object} page - The page.
|
|
592
|
-
* @param {object} options
|
|
593
|
-
* @param {(data: object, page: object) => void} [options.decorate] - Called
|
|
594
|
-
* with each page's frontmatter, for whatever a consumer's own pass adds.
|
|
595
|
-
* @param {(value: unknown, type: string, accepts?: Iterable<string>) => string|null} [options.artSrc] -
|
|
596
|
-
* Translates an art address into the pathname the website resolver takes, or
|
|
597
|
-
* `null` where nothing answers it.
|
|
598
|
-
* @param {(src: string) => string} [options.webSrc] - Translates an authored
|
|
599
|
-
* pathname into the address the website serves. Every artwork field goes
|
|
600
|
-
* through it, so a page's `img:` and its body images name the same file the
|
|
601
|
-
* same way.
|
|
602
|
-
* @returns {object} The frontmatter.
|
|
603
|
-
*/
|
|
604
|
-
export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
605
|
-
const { fm, slug } = page;
|
|
606
|
-
const data = {
|
|
607
|
-
...fm,
|
|
608
|
-
...(isPlainObject(fm.data) ? { data: { ...fm.data } } : {}),
|
|
609
|
-
// Spread after the note's own frontmatter. Guarded because
|
|
610
|
-
// `package: undefined` is not a value YAML can carry.
|
|
611
|
-
...(page.pkg ? { package: page.pkg } : {}),
|
|
612
|
-
// The address, stated site-root relative; `slug` is its last segment.
|
|
613
|
-
slug,
|
|
614
|
-
url: `/${slug}/`,
|
|
615
|
-
title: pageTitle(page),
|
|
616
|
-
kbfolder: page.folder,
|
|
617
|
-
};
|
|
618
|
-
if (decorate) decorate(data, page);
|
|
619
|
-
delete data.related;
|
|
620
|
-
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
621
|
-
return data;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* The title a content page publishes under — the note's name.
|
|
626
|
-
*
|
|
627
|
-
* One rule, read by the page's own front matter and by every `related` entry
|
|
628
|
-
* that names the page, so a card lists a page by exactly the title its heading
|
|
629
|
-
* shows.
|
|
630
|
-
*
|
|
631
|
-
* @param {object} page - The page, from {@link collectContentPages}.
|
|
632
|
-
* @returns {string} The title.
|
|
633
|
-
*/
|
|
634
|
-
function pageTitle(page) {
|
|
635
|
-
return page.name;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Rewrite a page's artwork addresses into the URLs the website serves.
|
|
640
|
-
*
|
|
641
|
-
* The slots are read from {@link module:engine/art-fields.ART_SLOTS}, so a slot
|
|
642
|
-
* added to the vocabulary reaches the page without anyone remembering this
|
|
643
|
-
* function exists. Only an authored **string** is touched: `null` is a note
|
|
644
|
-
* naming no art and `""` is one naming none on purpose, and neither is an
|
|
645
|
-
* address to resolve. A slot's declining word (`banner: none`) is emitted as
|
|
646
|
-
* a blank, the value a theme reads as "draw no image".
|
|
647
|
-
*
|
|
648
|
-
* **An address nothing answers is dropped**, not emitted as written. The theme
|
|
649
|
-
* renders nothing where a value is absent — its silent-disappear convention —
|
|
650
|
-
* and a raw address left in place would reach the reader as a broken image
|
|
651
|
-
* source with nothing saying why.
|
|
652
|
-
*
|
|
653
|
-
* @param {object} data - The frontmatter being emitted, rewritten in place.
|
|
654
|
-
* @param {(src: string) => string} webSrc - The website's pathname resolver.
|
|
655
|
-
* @param {(value: unknown, type: string, accepts?: Iterable<string>) => string|null} artSrc -
|
|
656
|
-
* The address resolver, yielding the pathname `webSrc` takes.
|
|
657
|
-
* @returns {void}
|
|
658
|
-
*/
|
|
659
|
-
function resolveArtFields(data, webSrc, artSrc) {
|
|
660
|
-
for (const { key, type, accepts, declines } of ART_SLOTS) {
|
|
661
|
-
const holder = isPlainObject(data.data) ? data.data : null;
|
|
662
|
-
if (!holder) continue;
|
|
663
|
-
const value = holder[key];
|
|
664
|
-
if (declines !== undefined && value === declines) {
|
|
665
|
-
holder[key] = "";
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
if ((typeof value !== "string" && !isAddressTuple(value)) || value === "") continue;
|
|
669
|
-
const pathname = artSrc(value, type, accepts);
|
|
670
|
-
if (pathname === null) delete holder[key];
|
|
671
|
-
else holder[key] = webSrc(pathname);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* @param {unknown} value - Anything.
|
|
677
|
-
* @returns {boolean} Whether it is a mapping a field may be read out of.
|
|
678
|
-
*/
|
|
679
|
-
function isPlainObject(value) {
|
|
680
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/** Transform one content page. */
|
|
684
|
-
export function renderSitePage(
|
|
685
|
-
page,
|
|
686
|
-
{
|
|
687
|
-
index,
|
|
688
|
-
foreign,
|
|
689
|
-
pass = {},
|
|
690
|
-
decorate,
|
|
691
|
-
sqlTables,
|
|
692
|
-
config,
|
|
693
|
-
artIndex,
|
|
694
|
-
figuresByAddress = new Map(),
|
|
695
|
-
events,
|
|
696
|
-
drawings = new Map(),
|
|
697
|
-
runs = runContext(config, undefined, {
|
|
698
|
-
referenceName: (target) => resolveInfoboxRef(index, target)?.name,
|
|
699
|
-
}),
|
|
700
|
-
},
|
|
701
|
-
) {
|
|
702
|
-
const secretErrors = [];
|
|
703
|
-
const captionErrors = [];
|
|
704
|
-
const headingErrors = [];
|
|
705
|
-
const footnoteErrors = [];
|
|
706
|
-
const imageErrors = [];
|
|
707
|
-
const embedErrors = [];
|
|
708
|
-
const src = page.relPath ?? page.base;
|
|
709
|
-
|
|
710
|
-
const seenImages = new Map();
|
|
711
|
-
const webSrc = (src) => {
|
|
712
|
-
const occurrence = (seenImages.get(src) ?? 0) + 1;
|
|
713
|
-
seenImages.set(src, occurrence);
|
|
714
|
-
const problem = pathnameProblem(src);
|
|
715
|
-
if (problem) {
|
|
716
|
-
imageErrors.push({ file: page.file, src, occurrence, message: problem });
|
|
717
|
-
return src;
|
|
718
|
-
}
|
|
719
|
-
const forms = resolvePathname(src, config);
|
|
720
|
-
if (!forms || forms.web !== null) return forms?.web ?? src;
|
|
721
|
-
imageErrors.push({
|
|
722
|
-
file: page.file,
|
|
723
|
-
src,
|
|
724
|
-
occurrence,
|
|
725
|
-
message:
|
|
726
|
-
`\`${src}\` names a file the \`${forms.package}\` package ships, and ` +
|
|
727
|
-
"no asset host is configured to serve it from — set `site.assets` in " +
|
|
728
|
-
"package-build.config.yaml. Emitted as authored, the address resolves " +
|
|
729
|
-
"against the page's own URL, which is nowhere",
|
|
730
|
-
});
|
|
731
|
-
return src;
|
|
732
|
-
};
|
|
733
|
-
const artSrc = (value, type, accepts) => artPathname(artIndex, value, type, accepts).pathname;
|
|
734
|
-
// The role lookup a captioned item's `map` counter reaches through — see
|
|
735
|
-
// `engine/content-figures.mjs`'s `resolveRole`. It numbers the page's
|
|
736
|
-
// figures once, in expansion, and the render below reads that numbering.
|
|
737
|
-
const resolveRole = (address) => embedRole(artIndex, address);
|
|
738
|
-
// The picture's role and pixel size, by the address it resolved to — read
|
|
739
|
-
// before `webSrc` translates it to the page's own host. A body image is
|
|
740
|
-
// free to write the bare, own-package form, so the address is normalized
|
|
741
|
-
// to the pathname the asset index keys by first.
|
|
742
|
-
const lookupAsset = (src) => {
|
|
743
|
-
const key = assetPathnameKey(src, config);
|
|
744
|
-
return key ? artIndex?.byPath?.get(key) : undefined;
|
|
745
|
-
};
|
|
746
|
-
// A generated **From here** section names the drawing the build made, which
|
|
747
|
-
// the asset host does not serve: it is set inline, so its place names stay
|
|
748
|
-
// links. A drawing this build did not make is left for the image pass,
|
|
749
|
-
// which reports it.
|
|
750
|
-
const inlineDrawings = (text) =>
|
|
751
|
-
text.replace(new RegExp(IMAGE_PATTERN.source, "g"), (whole, alt, src) => {
|
|
752
|
-
const name = generatedDrawing(src);
|
|
753
|
-
const drawing = name ? drawings.get(name) : undefined;
|
|
754
|
-
if (!drawing) return whole;
|
|
755
|
-
return inlineDrawingFigure(fs.readFileSync(drawing.file, "utf8"), alt);
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
// The frontmatter's art resolves before the body's pictures, so a picture
|
|
759
|
-
// named in both is counted in that order.
|
|
760
|
-
const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
|
|
761
|
-
const expanded = expandContent(page.body, {
|
|
762
|
-
target: "web",
|
|
763
|
-
index,
|
|
764
|
-
note: { fm: page.fm, file: page.file, source: src, bodyLine: page.bodyLine },
|
|
765
|
-
prepared: {
|
|
766
|
-
sqlTables: sqlTables?.get(page.file),
|
|
767
|
-
pageLists: sqlTables?.pageLists?.get(page.file),
|
|
768
|
-
inline: sqlTables?.inline?.get(page.file),
|
|
769
|
-
},
|
|
770
|
-
events,
|
|
771
|
-
resolveRole,
|
|
772
|
-
figuresByAddress,
|
|
773
|
-
links: {
|
|
774
|
-
foreign: foreign.index,
|
|
775
|
-
assets: artIndex,
|
|
776
|
-
beforeLinks: pass.beforeLinks ? (text) => pass.beforeLinks(text, page) : undefined,
|
|
777
|
-
afterLinks: (text) => renderImageFigures(inlineDrawings(text), webSrc, lookupAsset),
|
|
778
|
-
},
|
|
779
|
-
});
|
|
780
|
-
const { lineMap } = expanded;
|
|
781
|
-
const tableErrors = expanded.findings.tables;
|
|
782
|
-
const expressionErrors = expanded.findings.expressions;
|
|
783
|
-
const wikiErrors = expanded.findings.links;
|
|
784
|
-
// The author's text and the sections the build generated after it are
|
|
785
|
-
// told apart by a comment, so the site writer can leave the generated
|
|
786
|
-
// links out of what makes a page reachable.
|
|
787
|
-
const resolvedBody =
|
|
788
|
-
expanded.generated ?
|
|
789
|
-
`${expanded.authored}\n\n${GENERATED_MARK}\n\n${expanded.generated}`
|
|
790
|
-
: expanded.markdown;
|
|
791
|
-
for (const error of expanded.findings.captions)
|
|
792
|
-
captionErrors.push({
|
|
793
|
-
file: page.file,
|
|
794
|
-
line: (page.bodyLine ?? 1) + (lineMap[error.line - 1]?.line ?? error.line - 1),
|
|
795
|
-
column: error.column,
|
|
796
|
-
message: error.message,
|
|
797
|
-
});
|
|
798
|
-
for (const error of [
|
|
799
|
-
...scanBlocks(page.body).errors,
|
|
800
|
-
...scanAlerts(page.body).errors,
|
|
801
|
-
...scanPageBreaks(page.body).errors,
|
|
802
|
-
...scanSpans(page.body).errors,
|
|
803
|
-
...scanRuns(page.body, runs).errors,
|
|
804
|
-
...markupAnchorFindings(page.body),
|
|
805
|
-
])
|
|
806
|
-
secretErrors.push({
|
|
807
|
-
file: error.file ?? page.file,
|
|
808
|
-
line: error.file ? error.line : (page.bodyLine ?? 1) + error.line - 1,
|
|
809
|
-
column: error.column,
|
|
810
|
-
message: error.message,
|
|
811
|
-
});
|
|
812
|
-
for (const error of scanHeadingAttributes(page.body, page.bodyLine ?? 1).errors)
|
|
813
|
-
headingErrors.push({ file: page.file, ...error });
|
|
814
|
-
for (const error of withheldSections(page.body, page.bodyLine ?? 1).errors)
|
|
815
|
-
headingErrors.push({ file: page.file, ...error });
|
|
816
|
-
for (const error of footnoteFindings(page.body))
|
|
817
|
-
footnoteErrors.push({
|
|
818
|
-
file: error.file ?? page.file,
|
|
819
|
-
line: error.file ? error.line : (page.bodyLine ?? 1) + error.line - 1,
|
|
820
|
-
column: error.column,
|
|
821
|
-
message: error.message,
|
|
822
|
-
});
|
|
823
|
-
// An image sharing its paragraph with other text, or an address or title
|
|
824
|
-
// the lint already refuses — asked here too, so a site build run on its
|
|
825
|
-
// own fails on the same input `renderImageFigures` otherwise renders as
|
|
826
|
-
// though the directive were absent.
|
|
827
|
-
for (const error of checkImages(page.body, page.file, { bodyLine: page.bodyLine ?? 1 }))
|
|
828
|
-
embedErrors.push(error);
|
|
829
|
-
return {
|
|
830
|
-
page,
|
|
831
|
-
// The expanded body and its numbered figures, for the renderer that
|
|
832
|
-
// writes the whole page.
|
|
833
|
-
markdown: resolvedBody,
|
|
834
|
-
figures: expanded.figures.figures,
|
|
835
|
-
data,
|
|
836
|
-
// The links a generated section makes join no page's Related card:
|
|
837
|
-
// that card is what the authors link, and the sections already list
|
|
838
|
-
// what they derive.
|
|
839
|
-
resolved: expanded.resolved.slice(0, expanded.authoredLinks),
|
|
840
|
-
tableErrors,
|
|
841
|
-
expressionErrors,
|
|
842
|
-
secretErrors,
|
|
843
|
-
captionErrors,
|
|
844
|
-
headingErrors,
|
|
845
|
-
footnoteErrors,
|
|
846
|
-
wikiErrors,
|
|
847
|
-
imageErrors,
|
|
848
|
-
embedErrors,
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
|
|
852
488
|
/**
|
|
853
489
|
* Renders every page.
|
|
854
490
|
*
|
|
@@ -1004,7 +640,10 @@ export function renderPages(pages, options) {
|
|
|
1004
640
|
artIndex,
|
|
1005
641
|
figuresByAddress,
|
|
1006
642
|
events,
|
|
1007
|
-
|
|
643
|
+
drawing: (name) => {
|
|
644
|
+
const drawn = maps.get(name);
|
|
645
|
+
return drawn ? fs.readFileSync(drawn.file, "utf8") : undefined;
|
|
646
|
+
},
|
|
1008
647
|
runs,
|
|
1009
648
|
});
|
|
1010
649
|
const externalRunFindings = new Set();
|
|
@@ -1081,59 +720,6 @@ export function renderPages(pages, options) {
|
|
|
1081
720
|
};
|
|
1082
721
|
}
|
|
1083
722
|
|
|
1084
|
-
/**
|
|
1085
|
-
* The named pass bundles a configuration may select.
|
|
1086
|
-
*
|
|
1087
|
-
* `site.pass` is the one part of the site contract that is *code* — a
|
|
1088
|
-
* repository's own body rewrites — so a configuration names a bundle instead of
|
|
1089
|
-
* supplying one, exactly as `itemBuilders` names an item registry. Required
|
|
1090
|
-
* lazily, so importing this module does not drag a package-specific half of the
|
|
1091
|
-
* toolchain in behind it.
|
|
1092
|
-
*
|
|
1093
|
-
* @type {Readonly<Record<string, () => Function>>}
|
|
1094
|
-
*/
|
|
1095
|
-
const SITE_PASSES = Object.freeze({
|
|
1096
|
-
sohlKb: () => require("../sohl/kb-passes.mjs").sohlKbPass,
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Resolves `site.pass` to its bundle.
|
|
1101
|
-
*
|
|
1102
|
-
* @param {string|undefined} name - The configured name.
|
|
1103
|
-
* @param {object} options - The configured options, plus `repoRoot`.
|
|
1104
|
-
* @returns {{beforeLinks?: Function}} The bundle.
|
|
1105
|
-
*/
|
|
1106
|
-
export function resolveSitePass(name, options) {
|
|
1107
|
-
if (!name) return {};
|
|
1108
|
-
const factory = SITE_PASSES[name];
|
|
1109
|
-
if (!factory) {
|
|
1110
|
-
throw new Error(
|
|
1111
|
-
`unknown site pass ${JSON.stringify(name)} — expected one of ` +
|
|
1112
|
-
`${Object.keys(SITE_PASSES).join(", ")}`,
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
return factory()(options);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
/** The site frontmatter decoration shared by the site writer and page preview. */
|
|
1119
|
-
export function sitePageDecorator(config, index) {
|
|
1120
|
-
const router = routerFor(config);
|
|
1121
|
-
return (data, page) => {
|
|
1122
|
-
// A being's dates reach the page; a note's events reach no page.
|
|
1123
|
-
const { events: _events, ...resolvedDates } = resolvedDateFields(
|
|
1124
|
-
page.fm,
|
|
1125
|
-
index.dateContext,
|
|
1126
|
-
);
|
|
1127
|
-
if (Object.keys(resolvedDates).length) data.resolvedDates = resolvedDates;
|
|
1128
|
-
data.infoboxes = noteInfoboxes(page.fm, {
|
|
1129
|
-
resolve: (ref, hint) => resolveInfoboxRef(index, ref, hint),
|
|
1130
|
-
router,
|
|
1131
|
-
dates: index.dateContext,
|
|
1132
|
-
contentPackage: index.contentPackage,
|
|
1133
|
-
});
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
723
|
/**
|
|
1138
724
|
* Collects, checks and renders a content tree's pages, and reports what it found.
|
|
1139
725
|
*
|