@heroiclands/package-build 10.0.0 → 11.0.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 +306 -0
- package/CONTENT.md +218 -71
- package/MIGRATING.md +64 -0
- package/bin/content-build.mjs +59 -75
- package/docs/content-format.md +90 -67
- package/engine/base-compiler.mjs +7 -1
- package/engine/content-address.mjs +71 -18
- package/engine/content-format-check.mjs +1 -1
- package/engine/content-links.mjs +93 -112
- package/engine/content-lint.mjs +14 -10
- package/engine/content-slug.mjs +39 -105
- package/engine/diagnostics.mjs +16 -2
- package/engine/frontmatter-lint.mjs +27 -7
- package/engine/helpers.mjs +31 -68
- package/engine/homepage.mjs +131 -86
- package/engine/index.mjs +2 -5
- package/engine/manifest-emit.mjs +23 -4
- package/engine/note-vocabulary.mjs +58 -1
- package/engine/retired-fields.mjs +117 -6
- package/engine/site-build.mjs +182 -59
- package/engine/site-index.mjs +57 -102
- package/engine/web-wikilinks.mjs +183 -127
- package/engine/wikilink-syntax.mjs +174 -34
- package/engine/wikilinks.mjs +159 -117
- package/package.json +1 -1
- package/types/engine/base-compiler.d.mts +1 -1
- package/types/engine/content-address.d.mts +46 -14
- package/types/engine/content-links.d.mts +13 -17
- package/types/engine/content-slug.d.mts +11 -48
- package/types/engine/diagnostics.d.mts +14 -1
- package/types/engine/helpers.d.mts +4 -3
- package/types/engine/homepage.d.mts +96 -60
- package/types/engine/index.d.mts +0 -1
- package/types/engine/note-vocabulary.d.mts +43 -0
- package/types/engine/retired-fields.d.mts +78 -1
- package/types/engine/site-build.d.mts +70 -17
- package/types/engine/site-index.d.mts +19 -21
- package/types/engine/web-wikilinks.d.mts +29 -28
- package/types/engine/wikilink-syntax.d.mts +126 -40
- package/types/engine/wikilinks.d.mts +29 -24
- package/engine/abbreviations.mjs +0 -0
- package/engine/alias-index.mjs +0 -153
- package/types/engine/abbreviations.d.mts +0 -44
- package/types/engine/alias-index.d.mts +0 -122
package/engine/site-build.mjs
CHANGED
|
@@ -49,8 +49,8 @@ import path from "node:path";
|
|
|
49
49
|
import { createRequire } from "node:module";
|
|
50
50
|
import matter from "gray-matter";
|
|
51
51
|
|
|
52
|
-
import {
|
|
53
|
-
import { sectionOf } from "./content-address.mjs";
|
|
52
|
+
import { slugify } from "./content-slug.mjs";
|
|
53
|
+
import { addressSlug, sectionOf } from "./content-address.mjs";
|
|
54
54
|
import { protectCode } from "./code-fences.mjs";
|
|
55
55
|
import { expandContentTables } from "./content-tables.mjs";
|
|
56
56
|
import { buildSiteIndex, wikiContext } from "./site-index.mjs";
|
|
@@ -61,8 +61,8 @@ import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
|
61
61
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
62
62
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
63
63
|
import {
|
|
64
|
-
HOMEPAGE_DESTINATION,
|
|
65
64
|
checkHomepageCount,
|
|
65
|
+
homepageDestination,
|
|
66
66
|
homepageFrontmatter,
|
|
67
67
|
homepageTitle,
|
|
68
68
|
isHomepage,
|
|
@@ -75,10 +75,12 @@ const require = createRequire(import.meta.url);
|
|
|
75
75
|
* Every `.md` file under `dir`, depth-first in directory order.
|
|
76
76
|
*
|
|
77
77
|
* Deliberately *not* {@link walkMarkdownTree}, whose stack-based walk yields a
|
|
78
|
-
* tree in reverse. Order
|
|
79
|
-
*
|
|
80
|
-
* the walk silently
|
|
81
|
-
*
|
|
78
|
+
* tree in reverse. Order was load-bearing here when the address index carried
|
|
79
|
+
* first-writer-wins fallbacks for a page's name, filename and slug — reversing
|
|
80
|
+
* the walk silently changed which page an ambiguous name resolved to. Those
|
|
81
|
+
* fallbacks are gone with the bare `[[Name]]` form (#180), so this is now
|
|
82
|
+
* ordinary reading order rather than a dependency; it is kept because a site's
|
|
83
|
+
* emitted pages should not reorder for no reason.
|
|
82
84
|
*
|
|
83
85
|
* @param {string} dir - Directory to walk.
|
|
84
86
|
* @param {readonly string[]} skip - Directory names to ignore at any depth.
|
|
@@ -123,14 +125,25 @@ function readNote(file) {
|
|
|
123
125
|
* The content tree's pages, and what could not be addressed.
|
|
124
126
|
*
|
|
125
127
|
* @param {string} contentBase - Absolute path to the content tree.
|
|
126
|
-
* @param {object} ctx - `{ packages, contentPackage, skipDirectories,
|
|
127
|
-
* scheme }`. `contentPackage` is the package a note that declares none
|
|
128
|
-
* belongs to
|
|
129
|
-
*
|
|
128
|
+
* @param {object} ctx - `{ packages, contentPackage, skipDirectories, base,
|
|
129
|
+
* mount, scheme }`. `contentPackage` is the package a note that declares none
|
|
130
|
+
* belongs to; `base` is where the package is served and `mount` is where its
|
|
131
|
+
* content tree sits inside it.
|
|
132
|
+
* @returns {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}
|
|
130
133
|
*/
|
|
131
134
|
export function collectContentPages(contentBase, ctx) {
|
|
135
|
+
// Where an addressed page publishes. The section below is guarded so a note
|
|
136
|
+
// is never "written to `undefined/`"; the same reasoning applies here, and a
|
|
137
|
+
// missing `base` would put *every* page there rather than one. It is the
|
|
138
|
+
// caller's contract rather than a note's defect, so it throws instead of
|
|
139
|
+
// being collected as a finding (#195).
|
|
140
|
+
if (typeof ctx.base !== "string" || !ctx.base) {
|
|
141
|
+
throw new TypeError(
|
|
142
|
+
"collectContentPages: `ctx.base` must be a non-empty string — it is the package address every page's URL is built on",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
132
145
|
const pages = [];
|
|
133
|
-
const
|
|
146
|
+
const addressFindings = [];
|
|
134
147
|
const fmLinkFindings = [];
|
|
135
148
|
|
|
136
149
|
for (const file of walkSiteTree(contentBase, ctx.skipDirectories)) {
|
|
@@ -142,9 +155,10 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
142
155
|
// (#56).
|
|
143
156
|
const pkg = ctx.contentPackage;
|
|
144
157
|
if (!ctx.packages.has(pkg) || !fm.type) continue;
|
|
145
|
-
// A homepage is addressed
|
|
146
|
-
//
|
|
147
|
-
//
|
|
158
|
+
// A homepage is addressed like any other note (#182), but it is
|
|
159
|
+
// gathered by {@link collectHomepages} rather than here: it is the
|
|
160
|
+
// whole of a homepage-only build, which never walks the tree for
|
|
161
|
+
// content pages at all (#55).
|
|
148
162
|
if (isHomepage(fm)) continue;
|
|
149
163
|
|
|
150
164
|
for (const hit of frontmatterWikilinks(fm)) {
|
|
@@ -152,24 +166,40 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
const name = fm.name?.full ?? path.basename(file, ".md");
|
|
155
|
-
// The
|
|
156
|
-
//
|
|
157
|
-
//
|
|
169
|
+
// The page's address, and therefore its URL (#181). `name` is the
|
|
170
|
+
// display string and nothing else: it titles the page and labels an
|
|
171
|
+
// inbound link, and moving it moves no address.
|
|
158
172
|
let slug;
|
|
159
173
|
try {
|
|
160
|
-
slug =
|
|
174
|
+
slug = addressSlug(fm);
|
|
161
175
|
} catch (err) {
|
|
162
|
-
|
|
176
|
+
addressFindings.push({ file, reason: err.message });
|
|
163
177
|
continue;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
const base = path.basename(file);
|
|
167
181
|
const isReadme = base.toLowerCase() === "readme.md";
|
|
168
182
|
const sec = sectionOf(fm);
|
|
183
|
+
// A page's URL no longer contains its section, but the section is still
|
|
184
|
+
// what decides the directory the file is written to — and Hugo derives
|
|
185
|
+
// a page's section from that directory, not from its URL. So a note
|
|
186
|
+
// with none is still a note with nowhere to be published, and is
|
|
187
|
+
// reported rather than written to `undefined/`.
|
|
188
|
+
if (typeof sec !== "string" || !sec) {
|
|
189
|
+
addressFindings.push({
|
|
190
|
+
file,
|
|
191
|
+
reason: `type "${fm.type}" has no section, so there is nowhere to file the page`,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
169
195
|
const rel = path.relative(contentBase, file);
|
|
170
196
|
pages.push({
|
|
171
197
|
kind: "content",
|
|
172
198
|
fm,
|
|
199
|
+
// The note's own path on disk. Carried so a link finding can be
|
|
200
|
+
// reported as `file:line:column:` against the source an author
|
|
201
|
+
// edits, rather than against the page this build emits (#184).
|
|
202
|
+
file,
|
|
173
203
|
// The page's package, recorded once here so every consumer — the
|
|
174
204
|
// index's canonical keys, the table universe, the local-package set
|
|
175
205
|
// — reads one configured value and never frontmatter (#56).
|
|
@@ -189,11 +219,17 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
189
219
|
// authoring folder, for grouped landings.
|
|
190
220
|
folder: path.basename(path.dirname(file)),
|
|
191
221
|
sec,
|
|
192
|
-
|
|
222
|
+
// A landing page **is** its section, so it is addressed by the
|
|
223
|
+
// mount the section lives at; every other page is addressed by
|
|
224
|
+
// `(type, shortcode)` at the package root, which takes no mount
|
|
225
|
+
// (#181). The file is still written into `<sec>/` either way — see
|
|
226
|
+
// {@link pageDestination} — and the front matter carries this `url`
|
|
227
|
+
// so Hugo publishes it at its address rather than at its path.
|
|
228
|
+
url: isReadme ? `${ctx.mount}${sec}/` : `${ctx.base}${slug}/`,
|
|
193
229
|
isReadme,
|
|
194
230
|
});
|
|
195
231
|
}
|
|
196
|
-
return { pages,
|
|
232
|
+
return { pages, addressFindings, fmLinkFindings };
|
|
197
233
|
}
|
|
198
234
|
|
|
199
235
|
/**
|
|
@@ -234,6 +270,8 @@ export function collectTreePages(tree, ctx) {
|
|
|
234
270
|
kind: "tree",
|
|
235
271
|
tree,
|
|
236
272
|
fm,
|
|
273
|
+
// As above: the source file, for a located link diagnostic (#184).
|
|
274
|
+
file,
|
|
237
275
|
// The H1 is stripped: the page title renders it.
|
|
238
276
|
body: body.replace(/^\s*#\s+.*$\r?\n?/m, ""),
|
|
239
277
|
name,
|
|
@@ -266,22 +304,39 @@ export function collectTreePages(tree, ctx) {
|
|
|
266
304
|
* count is what {@link checkHomepageCount} judges (#52) — this walk reports
|
|
267
305
|
* what it found, and {@link buildSite} decides whether that is one.
|
|
268
306
|
*
|
|
307
|
+
* A homepage that declares no `shortcode` has no address (#182), and is
|
|
308
|
+
* reported rather than written: it is the same finding a content page's missing
|
|
309
|
+
* shortcode produces, and it has to be available in homepage-only mode, where
|
|
310
|
+
* no other gate runs.
|
|
311
|
+
*
|
|
312
|
+
* **It is still counted.** An unaddressable homepage is a homepage — dropping
|
|
313
|
+
* it from the list would make {@link checkHomepageCount} report a tree with one
|
|
314
|
+
* as having none, sending its author to write a second front page instead of a
|
|
315
|
+
* line of frontmatter.
|
|
316
|
+
*
|
|
269
317
|
* @param {string} contentBase - Absolute path to the content tree.
|
|
270
318
|
* @param {object} ctx - `{ skipDirectories }`.
|
|
271
|
-
* @returns {{pages: object[]}} The homepage notes,
|
|
319
|
+
* @returns {{pages: object[], addressFindings: object[]}} The homepage notes,
|
|
320
|
+
* in walk order, and the ones among them that could not be addressed.
|
|
272
321
|
*/
|
|
273
322
|
export function collectHomepages(contentBase, ctx) {
|
|
274
323
|
const pages = [];
|
|
324
|
+
const addressFindings = [];
|
|
275
325
|
for (const file of walkSiteTree(contentBase, ctx.skipDirectories)) {
|
|
276
326
|
const note = readNote(file);
|
|
277
327
|
if (!note || !isHomepage(note.fm)) continue;
|
|
328
|
+
try {
|
|
329
|
+
addressSlug(note.fm);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
addressFindings.push({ file, reason: err.message });
|
|
332
|
+
}
|
|
278
333
|
pages.push({ kind: "homepage", file, fm: note.fm, body: note.body });
|
|
279
334
|
}
|
|
280
|
-
return { pages };
|
|
335
|
+
return { pages, addressFindings };
|
|
281
336
|
}
|
|
282
337
|
|
|
283
338
|
/**
|
|
284
|
-
* Writes each homepage at the package's own root.
|
|
339
|
+
* Writes each homepage at its address, below the package's own root.
|
|
285
340
|
*
|
|
286
341
|
* Its own writer, deliberately small. A homepage is authored markdown published
|
|
287
342
|
* verbatim — no table expansion, no section landing and no link resolution — so
|
|
@@ -295,20 +350,31 @@ export function collectHomepages(contentBase, ctx) {
|
|
|
295
350
|
* {@link auditHomepageLinks} reads the `landing:` addresses and the body's
|
|
296
351
|
* markdown links, and reports a wikilink on the page rather than resolving one.
|
|
297
352
|
*
|
|
353
|
+
* **Its destination is no longer fixed** (#182). The file is written at the
|
|
354
|
+
* note's address, flat at the package's site root, and the page states that
|
|
355
|
+
* address as its `url` — the same separation of file from URL every other page
|
|
356
|
+
* has. Nothing is written at `/<package>/` itself: that becomes a redirect the
|
|
357
|
+
* package's own repository authors, which is a routing fact rather than a page.
|
|
358
|
+
*
|
|
298
359
|
* @param {string} outRoot - The package's site root — the configured `site.out`,
|
|
299
360
|
* one level above the content mount.
|
|
300
361
|
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
301
362
|
* @param {object} config - The resolved configuration, for the package name and
|
|
302
363
|
* the default title.
|
|
364
|
+
* @param {object} [options] - Options.
|
|
365
|
+
* @param {string} [options.base] - Where the package is served; defaults to the
|
|
366
|
+
* configured `site.base`, and to `/<contentPackage>/` below that.
|
|
303
367
|
* @returns {number} How many pages were written.
|
|
304
368
|
*/
|
|
305
|
-
export function writeHomepages(outRoot, pages, config) {
|
|
369
|
+
export function writeHomepages(outRoot, pages, config, { base } = {}) {
|
|
370
|
+
const at = base || config.site?.base || `/${config.contentPackage}/`;
|
|
306
371
|
for (const page of pages) {
|
|
307
372
|
const data = homepageFrontmatter(page.fm, {
|
|
308
373
|
contentPackage: config.contentPackage,
|
|
309
374
|
title: homepageTitle(page.fm, config),
|
|
375
|
+
base: at,
|
|
310
376
|
});
|
|
311
|
-
const dest = path.join(outRoot,
|
|
377
|
+
const dest = path.join(outRoot, homepageDestination(page.fm));
|
|
312
378
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
313
379
|
fs.writeFileSync(dest, matter.stringify(page.body, data));
|
|
314
380
|
}
|
|
@@ -324,15 +390,19 @@ export function writeHomepages(outRoot, pages, config) {
|
|
|
324
390
|
*
|
|
325
391
|
* - **Frontmatter wikilinks** first, because frontmatter is copied to the page
|
|
326
392
|
* verbatim and a link written in one reaches the reader as literal `[[…]]`.
|
|
327
|
-
* - **
|
|
328
|
-
*
|
|
393
|
+
* - **Addresses** next: a note that has no address — no shortcode to be
|
|
394
|
+
* addressed by, or no section to be filed under — would silently drop a page.
|
|
395
|
+
* There is no collision gate beside it: an address is `(type, shortcode)`,
|
|
396
|
+
* which is unique within a package by rule, so two pages cannot claim one URL
|
|
397
|
+
* (#181).
|
|
329
398
|
* - **Foreign manifests** last, in two steps. *Unusable* is a file this build
|
|
330
399
|
* cannot read; *unaddressable* is one it can read but cannot look anything up
|
|
331
400
|
* in — a distinction worth keeping, because the second surfaces as a pile of
|
|
332
401
|
* dead links blaming the notes that cite them rather than the file at fault.
|
|
333
402
|
*
|
|
334
403
|
* @param {object[]} pages - Every page, from both walks.
|
|
335
|
-
* @param {object} findings - `{
|
|
404
|
+
* @param {object} findings - `{ addressFindings, fmLinkFindings }` from
|
|
405
|
+
* collection.
|
|
336
406
|
* @param {object} options - `{ manifestDir }`.
|
|
337
407
|
* @returns {object} The gate results and, when they pass, the built index.
|
|
338
408
|
*/
|
|
@@ -343,8 +413,7 @@ export function siteGates(pages, findings, { manifestDir }) {
|
|
|
343
413
|
// reaching these gates (#52). Present so every caller reads one shape.
|
|
344
414
|
homepages: [],
|
|
345
415
|
frontmatterLinks: findings.fmLinkFindings ?? [],
|
|
346
|
-
|
|
347
|
-
collisions: [],
|
|
416
|
+
addressErrors: findings.addressFindings ?? [],
|
|
348
417
|
staleManifests: [],
|
|
349
418
|
unaddressable: [],
|
|
350
419
|
conflicts: [],
|
|
@@ -352,18 +421,9 @@ export function siteGates(pages, findings, { manifestDir }) {
|
|
|
352
421
|
foreign: null,
|
|
353
422
|
manifests: null,
|
|
354
423
|
};
|
|
355
|
-
if (out.frontmatterLinks.length || out.
|
|
424
|
+
if (out.frontmatterLinks.length || out.addressErrors.length) return out;
|
|
356
425
|
|
|
357
426
|
const content = pages.filter((p) => p.kind === "content");
|
|
358
|
-
out.collisions = findSlugCollisions(
|
|
359
|
-
content.map((p) => ({
|
|
360
|
-
sec: p.sec,
|
|
361
|
-
slug: p.slug,
|
|
362
|
-
src: `${p.tld}/${p.base}`,
|
|
363
|
-
})),
|
|
364
|
-
);
|
|
365
|
-
if (out.collisions.length) return out;
|
|
366
|
-
|
|
367
427
|
// Which packages are *local* is what decides which manifests are foreign,
|
|
368
428
|
// and that is only known once the tree is walked — reading it from a
|
|
369
429
|
// configured list instead silently discarded the manifest of any package
|
|
@@ -402,8 +462,7 @@ export function emptyGates() {
|
|
|
402
462
|
return {
|
|
403
463
|
homepages: [],
|
|
404
464
|
frontmatterLinks: [],
|
|
405
|
-
|
|
406
|
-
collisions: [],
|
|
465
|
+
addressErrors: [],
|
|
407
466
|
staleManifests: [],
|
|
408
467
|
unaddressable: [],
|
|
409
468
|
conflicts: [],
|
|
@@ -418,8 +477,7 @@ export function gatesFailed(gates) {
|
|
|
418
477
|
return Boolean(
|
|
419
478
|
gates.homepages.length ||
|
|
420
479
|
gates.frontmatterLinks.length ||
|
|
421
|
-
gates.
|
|
422
|
-
gates.collisions.length ||
|
|
480
|
+
gates.addressErrors.length ||
|
|
423
481
|
gates.staleManifests.length ||
|
|
424
482
|
gates.unaddressable.length ||
|
|
425
483
|
gates.conflicts.length,
|
|
@@ -490,12 +548,22 @@ export function sectionFrontmatter(meta) {
|
|
|
490
548
|
/**
|
|
491
549
|
* The frontmatter a page publishes with.
|
|
492
550
|
*
|
|
493
|
-
* An authored `aliases` is
|
|
551
|
+
* An authored `aliases` is retired (#180) and refused before a build reaches
|
|
552
|
+
* here, which makes this a guard rather than a working path. It was Obsidian's
|
|
553
|
+
* — a list of *names* a reader might call
|
|
494
554
|
* the note, which is vault addressing and stays in the vault. Hugo reads
|
|
495
555
|
* `aliases` as **URL redirects**, so passing them through would publish a
|
|
496
556
|
* redirect stub at each name. They are dropped, and this build emits no
|
|
497
557
|
* redirects of its own.
|
|
498
558
|
*
|
|
559
|
+
* A content page states its own **`url`**, which is its address rather than its
|
|
560
|
+
* path (#181). Hugo would otherwise publish it where the file sits — under the
|
|
561
|
+
* mount, inside its section directory — and the file sits there for a reason:
|
|
562
|
+
* Hugo derives a page's section from its directory, which is what gives the
|
|
563
|
+
* section its landing page, `.CurrentSection` and its per-section layout
|
|
564
|
+
* lookup. So the directory stays and the address is stated, and the two are
|
|
565
|
+
* free to differ.
|
|
566
|
+
*
|
|
499
567
|
* A content page carries the package the build **derived** (#65). No note
|
|
500
568
|
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
501
569
|
* would publish a page that does not say which package it belongs to. The
|
|
@@ -519,7 +587,13 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
519
587
|
// Spread after the note's own frontmatter. Guarded because
|
|
520
588
|
// `package: undefined` is not a value YAML can carry.
|
|
521
589
|
...(page.pkg ? { package: page.pkg } : {}),
|
|
590
|
+
// The address, stated. `slug` is written beside it because it is
|
|
591
|
+
// the last segment of that address and Hugo's own key for one; it
|
|
592
|
+
// decides nothing while `url` is present, but a page that carried
|
|
593
|
+
// only `url` would report a slug Hugo had inferred from the
|
|
594
|
+
// filename.
|
|
522
595
|
slug,
|
|
596
|
+
url: page.url,
|
|
523
597
|
title: fm.title ?? name,
|
|
524
598
|
kbfolder: page.folder,
|
|
525
599
|
};
|
|
@@ -546,7 +620,20 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
546
620
|
return data;
|
|
547
621
|
}
|
|
548
622
|
|
|
549
|
-
/**
|
|
623
|
+
/**
|
|
624
|
+
* Where a page is written, relative to the output root.
|
|
625
|
+
*
|
|
626
|
+
* **Into its section directory, which is not where it publishes** (#181). A
|
|
627
|
+
* content page's URL is its address — `/<package>/<type>-<shortcode>/` — and it
|
|
628
|
+
* is stated in the front matter; the file still goes to `<section>/`, because
|
|
629
|
+
* Hugo reads a page's section from its path and nothing else. Flattening the
|
|
630
|
+
* tree to match the URL would take the section landings, `.CurrentSection` and
|
|
631
|
+
* every per-section layout with it.
|
|
632
|
+
*
|
|
633
|
+
* The filename is the address rather than the section-relative half of it, so
|
|
634
|
+
* two sections cannot fight over one file: a `doc` note routes by its `subType`,
|
|
635
|
+
* which may be spelled the same as another note's `type`.
|
|
636
|
+
*/
|
|
550
637
|
export function pageDestination(page) {
|
|
551
638
|
if (page.kind === "content") {
|
|
552
639
|
return page.isReadme ?
|
|
@@ -582,7 +669,6 @@ export function renderPages(pages, options) {
|
|
|
582
669
|
outRoot,
|
|
583
670
|
index,
|
|
584
671
|
foreign,
|
|
585
|
-
manifests,
|
|
586
672
|
universe,
|
|
587
673
|
pass = {},
|
|
588
674
|
readmeSections,
|
|
@@ -598,10 +684,10 @@ export function renderPages(pages, options) {
|
|
|
598
684
|
const src = page.rel ?? `${page.sec}/${page.base}`;
|
|
599
685
|
const ctx = wikiContext(index, {
|
|
600
686
|
src,
|
|
687
|
+
file: page.file,
|
|
601
688
|
type: page.fm.type ?? null,
|
|
602
689
|
errors: wikiErrors,
|
|
603
690
|
foreignIndex: foreign.index,
|
|
604
|
-
manifestsComplete: manifests.complete,
|
|
605
691
|
});
|
|
606
692
|
|
|
607
693
|
const resolve = (text) => {
|
|
@@ -841,9 +927,9 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
841
927
|
// Homepage-only has no content mount, so the package's root *is*
|
|
842
928
|
// the output root and `--out` redirects the whole of it.
|
|
843
929
|
: outBase;
|
|
844
|
-
// The homepage publishes at `/<contentPackage>/`,
|
|
845
|
-
// own root — one level above the content mount,
|
|
846
|
-
// homepage-only mode.
|
|
930
|
+
// The homepage publishes at `/<contentPackage>/<type>-<shortcode>/`, so its
|
|
931
|
+
// file goes at the package's own root — one level above the content mount,
|
|
932
|
+
// and the same directory in homepage-only mode.
|
|
847
933
|
const homeRoot = publishesContent ? outBase : out;
|
|
848
934
|
|
|
849
935
|
const packages = new Set(site.packages.length ? site.packages : [resolved.contentPackage]);
|
|
@@ -854,11 +940,16 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
854
940
|
// retired, so this is the only source of it (#56).
|
|
855
941
|
contentPackage: resolved.contentPackage,
|
|
856
942
|
skipDirectories: resolved.skipDirectories,
|
|
943
|
+
// Where the package is served, which is where an addressed page
|
|
944
|
+
// publishes: an address is `(type, shortcode)`, a package-wide
|
|
945
|
+
// identity that takes no content mount (#181).
|
|
946
|
+
base,
|
|
857
947
|
mount,
|
|
858
948
|
scheme,
|
|
859
949
|
};
|
|
860
950
|
|
|
861
|
-
const
|
|
951
|
+
const collected = collectHomepages(resolved.paths.content, ctx);
|
|
952
|
+
const homepages = collected.pages;
|
|
862
953
|
|
|
863
954
|
// Exactly one homepage, and checked here — before the output tree is
|
|
864
955
|
// cleared and before either mode branches (#52). Before the clear, because
|
|
@@ -866,10 +957,24 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
866
957
|
// bad tree. Before the branch, because the requirement does not vary by
|
|
867
958
|
// mode: `publish.site` chooses whether the *content* surfaces are
|
|
868
959
|
// published, and the homepage is the floor beneath both.
|
|
869
|
-
|
|
960
|
+
//
|
|
961
|
+
// The count is judged first, and alone when it fires: a tree with two
|
|
962
|
+
// homepages does not need to be told about each one's address as well, and
|
|
963
|
+
// a tree with none has no address to report.
|
|
964
|
+
const counted = checkHomepageCount(homepages, {
|
|
870
965
|
contentBase: resolved.paths.content,
|
|
871
966
|
contentPackage: resolved.contentPackage,
|
|
872
967
|
});
|
|
968
|
+
// A homepage that cannot be addressed is reported in the same place, and
|
|
969
|
+
// reaches homepage-only mode — which runs no other gate at all (#182).
|
|
970
|
+
const homepageFindings =
|
|
971
|
+
counted.length ? counted : (
|
|
972
|
+
collected.addressFindings.map((f) => ({
|
|
973
|
+
file: f.file,
|
|
974
|
+
severity: "error",
|
|
975
|
+
message: `${f.reason}, so there is no page to publish at \`${base}\``,
|
|
976
|
+
}))
|
|
977
|
+
);
|
|
873
978
|
if (homepageFindings.length) {
|
|
874
979
|
return {
|
|
875
980
|
gates: { ...emptyGates(), homepages: homepageFindings },
|
|
@@ -894,7 +999,7 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
894
999
|
tableErrors: [],
|
|
895
1000
|
wikiErrors: [],
|
|
896
1001
|
stats: {
|
|
897
|
-
homepages: writeHomepages(homeRoot, homepages, resolved),
|
|
1002
|
+
homepages: writeHomepages(homeRoot, homepages, resolved, { base }),
|
|
898
1003
|
landings: 0,
|
|
899
1004
|
out: homeRoot,
|
|
900
1005
|
},
|
|
@@ -905,6 +1010,25 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
905
1010
|
const pages = [...content.pages];
|
|
906
1011
|
const fmLinkFindings = [...content.fmLinkFindings];
|
|
907
1012
|
|
|
1013
|
+
// The homepage is **indexed but not rendered** (#182). Now that it has an
|
|
1014
|
+
// address, `[[homepage-root|Text]]` is an ordinary wikilink and has to
|
|
1015
|
+
// resolve to the page the build publishes — which means the address index
|
|
1016
|
+
// must hold it. It still takes no part in `renderPages`: a homepage is
|
|
1017
|
+
// authored markdown published verbatim, with no table expansion and no link
|
|
1018
|
+
// resolution of its own, and routing it through that pipeline would buy it
|
|
1019
|
+
// a pass it has no input for.
|
|
1020
|
+
const homepageEntries = homepages.map((page) => ({
|
|
1021
|
+
kind: "content",
|
|
1022
|
+
fm: page.fm,
|
|
1023
|
+
pkg: resolved.contentPackage,
|
|
1024
|
+
name: page.fm.name?.full ?? homepageTitle(page.fm, resolved),
|
|
1025
|
+
slug: addressSlug(page.fm),
|
|
1026
|
+
base: path.basename(page.file),
|
|
1027
|
+
sec: sectionOf(page.fm),
|
|
1028
|
+
url: `${base}${addressSlug(page.fm)}/`,
|
|
1029
|
+
isReadme: false,
|
|
1030
|
+
}));
|
|
1031
|
+
|
|
908
1032
|
const trees = site.trees.map((t) => ({
|
|
909
1033
|
...t,
|
|
910
1034
|
from: path.resolve(resolved.rootDir, t.from),
|
|
@@ -917,7 +1041,7 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
917
1041
|
}
|
|
918
1042
|
|
|
919
1043
|
const gates = siteGates(
|
|
920
|
-
pages,
|
|
1044
|
+
[...pages, ...homepageEntries],
|
|
921
1045
|
{ ...content, fmLinkFindings },
|
|
922
1046
|
{ manifestDir: resolved.paths.manifests },
|
|
923
1047
|
);
|
|
@@ -940,7 +1064,6 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
940
1064
|
outRoot: out,
|
|
941
1065
|
index: gates.index,
|
|
942
1066
|
foreign: gates.foreign,
|
|
943
|
-
manifests: gates.manifests,
|
|
944
1067
|
universe: tableUniverse(pages),
|
|
945
1068
|
pass,
|
|
946
1069
|
readmeSections: site.readmeSections,
|
|
@@ -963,7 +1086,7 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
963
1086
|
|
|
964
1087
|
// Last, and outside the mount: the package's front page is not part of the
|
|
965
1088
|
// content tree it introduces.
|
|
966
|
-
const homepagesWritten = writeHomepages(homeRoot, homepages, resolved);
|
|
1089
|
+
const homepagesWritten = writeHomepages(homeRoot, homepages, resolved, { base });
|
|
967
1090
|
|
|
968
1091
|
return {
|
|
969
1092
|
gates,
|