vowel 0.7.1-beta.0 → 0.7.3
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/LICENSE +21 -0
- package/README.md +41 -239
- package/bin.js +91 -31
- package/cli.js +32 -0
- package/config.js +12 -0
- package/extractDate.js +1 -1
- package/package.json +8 -34
- package/pathDate.js +69 -0
- package/plugins/assets/index.js +1 -1
- package/plugins/atproto/body.js +3 -1
- package/plugins/atproto/index.js +5 -45
- package/plugins/atproto/publish.js +42 -11
- package/plugins/atproto/record.js +47 -4
- package/plugins/deploy-cloudflare/index.js +4 -1
- package/plugins/export/index.js +131 -0
- package/plugins/html/bundle/index.js +101 -44
- package/plugins/html/editor/Editor.svelte +9 -5
- package/plugins/html/editor/create-session.js +3 -1
- package/plugins/html/editor/directives.js +12 -0
- package/plugins/html/editor/frontmatter.js +77 -12
- package/plugins/html/editor/ingest.js +29 -31
- package/plugins/html/editor/main.js +5 -1
- package/plugins/html/editor/serialize.js +8 -4
- package/plugins/html/editor/socket.js +34 -4
- package/plugins/html/image.js +88 -27
- package/plugins/html/index.js +237 -109
- package/plugins/html/partials.js +41 -17
- package/plugins/html/socialLinks.js +13 -11
- package/plugins/html/writeRules.js +234 -42
- package/plugins/images/favicon.png +0 -0
- package/plugins/images/index.js +69 -8
- package/plugins/markdown/colorScheme.js +70 -14
- package/plugins/markdown/frontmatter.js +18 -8
- package/plugins/markdown/index.js +94 -25
- package/plugins/markdown/metadata.js +132 -32
- package/plugins/markdown/typography.js +11 -11
- package/plugins/styles/index.js +4 -4
- package/plugins/xml/index.js +17 -10
- package/stylesheets/DefaultStyles.css +163 -108
- package/stylesheets/TypographyStyles.css +148 -81
- package/utils.js +74 -21
package/plugins/html/index.js
CHANGED
|
@@ -13,15 +13,17 @@ import { gfmTable } from 'micromark-extension-gfm-table'
|
|
|
13
13
|
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
|
|
14
14
|
import { h } from 'hastscript'
|
|
15
15
|
import { readFileSync } from "fs"
|
|
16
|
-
import { testURL, testHashtags, createHashtagPage,
|
|
16
|
+
import { testURL, testHashtags, createHashtagPage, labelFromName, isPageWithoutFile, pageClasses, hashtagRegexSingle, imageSizes } from "./../../utils.js"
|
|
17
17
|
import extractDate from "./../../extractDate.js"
|
|
18
18
|
import { reservedProperties, hiddenProperties } from "./../markdown/metadata.js"
|
|
19
19
|
import { toString as hastToString } from 'hast-util-to-string'
|
|
20
20
|
import { unified } from "unified"
|
|
21
21
|
import toc from "@jsdevtools/rehype-toc"
|
|
22
22
|
import slug from "rehype-slug"
|
|
23
|
-
import createDynamicImage from "./image.js"
|
|
23
|
+
import createDynamicImage, { dominantStyle, imageURL } from "./image.js"
|
|
24
24
|
import { listPages } from "./../../utils.js"
|
|
25
|
+
import { pageRecordURI } from "../atproto/record.js"
|
|
26
|
+
import { STATUSES } from "../markdown/frontmatter.js"
|
|
25
27
|
import { themeStylesheets, isVowelStylesheet } from "../styles/theme.js"
|
|
26
28
|
import { displayPath } from "../../secretPaths.js"
|
|
27
29
|
import { socialLinksNav } from "./socialLinks.js"
|
|
@@ -75,8 +77,9 @@ function siteName(settings, api) {
|
|
|
75
77
|
* @param {string} url
|
|
76
78
|
* @param {import("votive").PluginAPI} api
|
|
77
79
|
* @param {Votive.VotiveConfig} config
|
|
80
|
+
* @param {string} [source] - the page's source path, for its classes
|
|
78
81
|
*/
|
|
79
|
-
function makeHeader(metadata, url, api, config) {
|
|
82
|
+
function makeHeader(metadata, url, api, config, source) {
|
|
80
83
|
|
|
81
84
|
const treeMainHead = []
|
|
82
85
|
|
|
@@ -89,9 +92,8 @@ function makeHeader(metadata, url, api, config) {
|
|
|
89
92
|
treeMainHead.push(makeTime(date))
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
const metaImage = metadata.image || metadata.first_image
|
|
96
|
+
if (metaImage) {
|
|
95
97
|
treeMainHead.push(
|
|
96
98
|
createDynamicImage(metaImage, api, null, true)
|
|
97
99
|
)
|
|
@@ -106,9 +108,41 @@ function makeHeader(metadata, url, api, config) {
|
|
|
106
108
|
)
|
|
107
109
|
)
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
// A card carries its page's classes, as the page's <body> does, and is
|
|
112
|
+
// an item of its own: the itemprops inside it (date, description,
|
|
113
|
+
// image) describe that page, not the page it's listed on. A page with
|
|
114
|
+
// no file (`html_file: false`) has no url, so its card links nowhere.
|
|
115
|
+
// Its image's dominant colour is the card's, for the card's own CSS.
|
|
116
|
+
const article = h('article', {
|
|
117
|
+
className: pageClasses(metadata.prettyURL, source),
|
|
118
|
+
itemscope: true,
|
|
119
|
+
style: dominantStyle(metaImage, api)
|
|
120
|
+
}, treeMainHead)
|
|
121
|
+
return url ? h('a', { href: url }, article) : article
|
|
122
|
+
}
|
|
110
123
|
|
|
111
|
-
|
|
124
|
+
/**
|
|
125
|
+
* How many links a hast tree holds.
|
|
126
|
+
* @param {any} node
|
|
127
|
+
* @returns {number}
|
|
128
|
+
*/
|
|
129
|
+
function linkCount(node) {
|
|
130
|
+
const own = node.tagName === "a" ? 1 : 0
|
|
131
|
+
const children = node.children ?? []
|
|
132
|
+
return children.reduce((sum, child) => sum + linkCount(child), own)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The nav, or null when it has fewer than two links: a nav is only
|
|
137
|
+
* rendered when there is somewhere else to go (per Sam). A one-entry
|
|
138
|
+
* menu, a trail that is only the home crumb, a table of contents with
|
|
139
|
+
* one heading are left out of the page, rather than hidden with CSS.
|
|
140
|
+
* Social links are exempt: one link is still a `rel="me"` verification.
|
|
141
|
+
* @param {any} nav - a hast element, or null
|
|
142
|
+
*/
|
|
143
|
+
function navWithChoices(nav) {
|
|
144
|
+
if (!nav) return null
|
|
145
|
+
return linkCount(nav) > 1 ? nav : null
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
/**
|
|
@@ -187,35 +221,88 @@ function makeValue(value) {
|
|
|
187
221
|
* @param {import("votive").PluginAPI} api
|
|
188
222
|
* @param {Votive.VotiveConfig} config
|
|
189
223
|
*/
|
|
190
|
-
|
|
191
|
-
|
|
224
|
+
/**
|
|
225
|
+
* An element with one more class. Frontmatter properties carry their key
|
|
226
|
+
* this way (`fm_date`, `fm_description`), which is how the editor tells
|
|
227
|
+
* them from the same properties written in the text: those have the
|
|
228
|
+
* itemprop and no class.
|
|
229
|
+
* @param {any} node - a hast element, or nothing
|
|
230
|
+
* @param {string} name
|
|
231
|
+
*/
|
|
232
|
+
function withClass(node, name) {
|
|
233
|
+
if (!node) return node
|
|
234
|
+
const className = [...(node.properties?.className ?? []), name]
|
|
235
|
+
return { ...node, properties: { ...node.properties, className } }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Natural metadata, lifted out of the content: the date line, image and
|
|
240
|
+
* description the author wrote in the text, which the read marked
|
|
241
|
+
* (`data-metadata`, metadata.js) and the write walk has already turned
|
|
242
|
+
* into a <time>, a <picture> and a paragraph. Each becomes a direct
|
|
243
|
+
* child of <main>, like its frontmatter equivalent but without the
|
|
244
|
+
* `fm_` class; the rule that closed a description renders as nothing.
|
|
245
|
+
* The source is untouched - only where the output puts them.
|
|
246
|
+
* @param {any} tree - the walked content; the marked blocks are removed from it
|
|
247
|
+
* @returns {any[]}
|
|
248
|
+
*/
|
|
249
|
+
function naturalMetadata(tree) {
|
|
250
|
+
const isMarked = (node) => Boolean(node.properties?.dataMetadata)
|
|
251
|
+
const marked = tree.children.filter(isMarked)
|
|
252
|
+
tree.children = tree.children.filter(node => !isMarked(node))
|
|
253
|
+
|
|
254
|
+
return marked.flatMap(node => {
|
|
255
|
+
const { dataMetadata: kind, ...properties } = node.properties
|
|
256
|
+
if (kind === "end") return []
|
|
257
|
+
if (kind === "description") return [{ ...node, properties }]
|
|
258
|
+
// A captioned image is a <figure> (writeRules.js) and is lifted
|
|
259
|
+
// whole. The itemprop goes on its image, not the figure: an image is
|
|
260
|
+
// the property, and the caption is not part of it.
|
|
261
|
+
if (node.tagName === "figure") {
|
|
262
|
+
const [image, ...caption] = node.children
|
|
263
|
+
return [{ ...node, properties, children: [{ ...image, properties: { ...image.properties, itemprop: kind } }, ...caption] }]
|
|
264
|
+
}
|
|
265
|
+
// A date or an image paragraph only wrapped the one element that is
|
|
266
|
+
// the metadata: the <time>, or the <picture> (an <img> until the
|
|
267
|
+
// image has been read).
|
|
268
|
+
const element = node.children.find(child => child.type === "element")
|
|
269
|
+
if (!element) return []
|
|
270
|
+
return [{ ...element, properties: { ...element.properties, itemprop: kind } }]
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function makeFrontmatter(metadata, api, config, natural = []) {
|
|
275
|
+
const handled = ["title", "slogan", "date", "image", "description"]
|
|
192
276
|
|
|
193
277
|
// hiddenProperties never render: the editor takes frontmatter from
|
|
194
278
|
// the source file, so nothing is lost by leaving a key off the page.
|
|
279
|
+
// `status` is shown only when it's the author's own word ("evergreen");
|
|
280
|
+
// vowel's four (published, hidden, …) are instructions, not content.
|
|
281
|
+
const ownStatus = !STATUSES.includes(metadata.fm_status)
|
|
195
282
|
const properties = (metadata.frontmatter_keys || []).filter(key => {
|
|
196
|
-
return !handled.includes(key) && !hiddenProperties.includes(key)
|
|
283
|
+
return !handled.includes(key) && !hiddenProperties.includes(key) && (key !== "status" || ownStatus)
|
|
197
284
|
})
|
|
198
285
|
|
|
199
286
|
const known = []
|
|
200
287
|
|
|
201
288
|
if (metadata.fm_date) {
|
|
202
|
-
known.push(makeTime(new Date(metadata.fm_date)))
|
|
289
|
+
known.push(withClass(makeTime(new Date(metadata.fm_date)), "fm_date"))
|
|
203
290
|
}
|
|
204
291
|
|
|
205
292
|
if (metadata.fm_image) {
|
|
206
|
-
known.push(createDynamicImage(metadata.fm_image, api, null, true))
|
|
293
|
+
known.push(withClass(createDynamicImage(metadata.fm_image, api, null, true), "fm_image"))
|
|
207
294
|
}
|
|
208
295
|
|
|
209
296
|
if (metadata.fm_description) {
|
|
210
|
-
known.push(h("p", { itemprop: "description" }, metadata.fm_description))
|
|
297
|
+
known.push(h("p.fm_description", { itemprop: "description" }, metadata.fm_description))
|
|
211
298
|
}
|
|
212
299
|
|
|
213
300
|
const generic = properties.map(key => {
|
|
214
301
|
const name = reservedProperties.includes(key) ? key : "fm_" + key
|
|
215
|
-
return h(
|
|
302
|
+
return h(`dl.fm_${key}`, [h("dt", key), h("dd", makeValue(metadata[name]))])
|
|
216
303
|
})
|
|
217
304
|
|
|
218
|
-
return [...known, ...generic]
|
|
305
|
+
return [...known, ...natural, ...generic]
|
|
219
306
|
}
|
|
220
307
|
|
|
221
308
|
/** @type {Votive.ProcessorWrite} */
|
|
@@ -289,11 +376,26 @@ function writeFile(target, { settings, api, config }) {
|
|
|
289
376
|
// setting); the root's decides which theme gets the plain file names.
|
|
290
377
|
const themeSheets = themeStylesheets(settings.lastNonNull("theme"), settings.raw("theme")?.[0]?.at(-1))
|
|
291
378
|
|
|
379
|
+
// The sheets a `styles:` names: each settings.md's along the chain,
|
|
380
|
+
// root first, then the page's own frontmatter. A path is a URL path
|
|
381
|
+
// from the root (frontmatter.js resolves it); a URL - a web font's
|
|
382
|
+
// stylesheet - is linked as written. A path to no file is skipped,
|
|
383
|
+
// with a warning: the <link> would 404. api.target() tracks it, so
|
|
384
|
+
// the file appearing later relinks the page.
|
|
385
|
+
const listed = [...new Set([...(settings.flat("fm_styles") ?? []), ...(metadata.fm_styles ?? [])])]
|
|
386
|
+
const external = listed.filter(entry => /^[a-z][a-z0-9+.-]*:|^\/\//i.test(entry))
|
|
387
|
+
const named = listed
|
|
388
|
+
.filter(entry => !external.includes(entry))
|
|
389
|
+
.map(entry => ({ entry, sheet: entry.replace(/^\/+/, "").split("/").join(path.sep) }))
|
|
390
|
+
const missing = named.filter(({ sheet }) => !api.target(sheet))
|
|
391
|
+
missing.forEach(({ entry }) => config.log?.("warn", `${target.path}: styles names ${entry}, which isn't in the project`))
|
|
392
|
+
const namedSheets = named.filter(({ sheet }) => !missing.some(gone => gone.sheet === sheet)).map(({ sheet }) => sheet)
|
|
393
|
+
|
|
292
394
|
// A stub stylesheet has a source path like any other source, so the
|
|
293
395
|
// `sheet.source` test alone no longer separates vowel's sheets from the
|
|
294
396
|
// project's - exclude them by name instead, or every theme sheet would
|
|
295
397
|
// be linked twice.
|
|
296
|
-
const sheets = [...themeSheets, ...projectSheets.map(sheet => sheet.path)
|
|
398
|
+
const sheets = [...new Set([...themeSheets, ...projectSheets.map(sheet => sheet.path), ...namedSheets])]
|
|
297
399
|
const projectHash = new Map(projectSheets.map(sheet => [sheet.path, sheet.hash]))
|
|
298
400
|
|
|
299
401
|
sheets.forEach(sheet => {
|
|
@@ -321,25 +423,70 @@ function writeFile(target, { settings, api, config }) {
|
|
|
321
423
|
})
|
|
322
424
|
)
|
|
323
425
|
})
|
|
426
|
+
external.forEach(href => treeStyleSheets.push(h('link', { rel: "stylesheet", href })))
|
|
324
427
|
|
|
325
|
-
// The document title: the page's own title, then every
|
|
326
|
-
// the page's
|
|
327
|
-
// hyphen-delimited - "Hats - Shop - Site - Warm heads".
|
|
328
|
-
// the site's front door and shows the site's name and
|
|
428
|
+
// The document title: the page's own title, then every folder from
|
|
429
|
+
// the page's up to the root, then the site's name, then the page's
|
|
430
|
+
// own slogan, hyphen-delimited - "Hats - Shop - Site - Warm heads".
|
|
431
|
+
// The homepage is the site's front door and shows the site's name and
|
|
432
|
+
// its slogan.
|
|
329
433
|
function createTitle() {
|
|
330
434
|
if (isRoot) {
|
|
331
|
-
return [site || metadata?.title, metadata?.
|
|
435
|
+
return [site || metadata?.title, metadata?.fm_slogan]
|
|
332
436
|
.filter(a => a)
|
|
333
437
|
.join(" - ")
|
|
334
438
|
}
|
|
335
439
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
440
|
+
// Every folder between the page and the root, leaf first, by the
|
|
441
|
+
// label its breadcrumb shows: its index page's. `name:` is the
|
|
442
|
+
// site's, so the root (index 0) is left to `site`.
|
|
443
|
+
const folders = ancestorFolders
|
|
444
|
+
.map((folder, index) => index === 0 ? null : folderCrumb(folder, index).label)
|
|
445
|
+
.slice(1)
|
|
446
|
+
.reverse()
|
|
447
|
+
const parts = [metadata.title, ...folders, site, metadata.fm_slogan].filter(a => a)
|
|
339
448
|
return parts.length ? parts.join(" - ") : "Website"
|
|
340
449
|
}
|
|
341
450
|
|
|
342
451
|
|
|
452
|
+
/**
|
|
453
|
+
* A folder's crumb, deduced from the path at render time rather than
|
|
454
|
+
* read from a `breadcrumbs` setting the folder pass used to write.
|
|
455
|
+
*
|
|
456
|
+
* The folder pass is gone, and this is a better place for it anyway:
|
|
457
|
+
* the lookup is tracked, so renaming a section's index page restales
|
|
458
|
+
* every page whose breadcrumb names it - which the setting never did.
|
|
459
|
+
*
|
|
460
|
+
* A folder with an index page is a link and takes that page's own
|
|
461
|
+
* breadcrumb. A folder without one is just a label: vowel no longer
|
|
462
|
+
* generates a page per folder, so linking there would 404.
|
|
463
|
+
*/
|
|
464
|
+
// A secret folder's target segment is a hash, so its readable name has
|
|
465
|
+
// to come from the page's own *source* path - de-salted, because the
|
|
466
|
+
// source keeps the salt and the salt must never reach the page. The
|
|
467
|
+
// page has one ancestor per target segment and one per source segment,
|
|
468
|
+
// aligned by depth, which is what lets the two be zipped.
|
|
469
|
+
const sourceFolderNames = target.source
|
|
470
|
+
? displayPath(target.source).split(path.sep).slice(0, -1)
|
|
471
|
+
: []
|
|
472
|
+
|
|
473
|
+
function folderCrumb(folderPath, index) {
|
|
474
|
+
if (!folderPath) {
|
|
475
|
+
const home = api.target("index.html")
|
|
476
|
+
return { label: home?.metadata?.breadcrumb || "Home", href: "/" }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// A folder's index is `<folder>.html`, from an authored `<folder>.md`
|
|
480
|
+
// beside it or the generated stub.
|
|
481
|
+
const indexTarget = api.target(`${folderPath}.html`)
|
|
482
|
+
// index 0 is the root, so ancestor i is source segment i - 1.
|
|
483
|
+
const sourceName = sourceFolderNames[index - 1]
|
|
484
|
+
const label = indexTarget?.metadata?.breadcrumb
|
|
485
|
+
|| labelFromName(sourceName ?? folderPath.split(path.sep).at(-1))
|
|
486
|
+
// folderPath is a stored folder (path.sep); an href is a url.
|
|
487
|
+
return { label, href: indexTarget ? "/" + folderPath.split(path.sep).join("/") : null }
|
|
488
|
+
}
|
|
489
|
+
|
|
343
490
|
const site = siteName(settings, api)
|
|
344
491
|
const title = createTitle()
|
|
345
492
|
|
|
@@ -365,6 +512,9 @@ function writeFile(target, { settings, api, config }) {
|
|
|
365
512
|
property: "og:description",
|
|
366
513
|
content: metadata.description,
|
|
367
514
|
}),
|
|
515
|
+
// The plain description most search engines read; og: is for
|
|
516
|
+
// link previews.
|
|
517
|
+
...(metadata.description ? [h('meta', { name: "description", content: metadata.description })] : []),
|
|
368
518
|
...treeStyleSheets,
|
|
369
519
|
])
|
|
370
520
|
|
|
@@ -391,20 +541,28 @@ function writeFile(target, { settings, api, config }) {
|
|
|
391
541
|
// The page's AT Protocol record, for readers that verify a document
|
|
392
542
|
// against its site: standard.site's convention, so only for a
|
|
393
543
|
// site.standard.document, and only for a page that can be one - it
|
|
394
|
-
// needs a title and a date (its `publishedAt`).
|
|
395
|
-
//
|
|
396
|
-
|
|
544
|
+
// needs a title and a date (its `publishedAt`). Worked out here, from
|
|
545
|
+
// the settings this write reads and so is rebuilt by, rather than
|
|
546
|
+
// stored by a transform that only reruns when the page is re-read.
|
|
547
|
+
const record = pageRecordURI(target, settings)
|
|
548
|
+
if (record.warning) config.log?.("warn", `${target.path}: ${record.warning}`)
|
|
549
|
+
const standardDocument = typeof record.uri === "string" && record.uri.split("/")[3] === "site.standard.document"
|
|
397
550
|
if (standardDocument && metadata.title && metadata.date) {
|
|
398
551
|
treeHead.children.push(h("link", {
|
|
399
552
|
rel: "site.standard.document",
|
|
400
|
-
href:
|
|
553
|
+
href: record.uri
|
|
401
554
|
}))
|
|
402
555
|
}
|
|
403
556
|
|
|
404
|
-
|
|
557
|
+
// The page's own image, else the site's `fallback_image` (a setting,
|
|
558
|
+
// so a folder can have its own), so a link preview always has one.
|
|
559
|
+
// The largest derivative of a png or jpeg (image.js): the original
|
|
560
|
+
// is not published.
|
|
561
|
+
const ogImage = imageURL(metadata.image || settings.lastNonNull("fm_fallback_image"), api, imageSizes.at(-1))
|
|
562
|
+
if (ogImage) {
|
|
405
563
|
treeHead.children.push(h("meta", {
|
|
406
564
|
property: "og:image",
|
|
407
|
-
content:
|
|
565
|
+
content: ogImage
|
|
408
566
|
}))
|
|
409
567
|
}
|
|
410
568
|
|
|
@@ -420,15 +578,16 @@ function writeFile(target, { settings, api, config }) {
|
|
|
420
578
|
// `icon` is a path, everywhere (Sam, Sept 19) - a URL path from the
|
|
421
579
|
// root, resolved at read like `logo` (frontmatter.js). The type is
|
|
422
580
|
// taken from its extension; a browser ignores a wrong one, but a
|
|
423
|
-
// right one lets it pick between several.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
581
|
+
// right one lets it pick between several. Without one, the project's
|
|
582
|
+
// own favicon.png, else Vowel's placeholder (plugins/images). A
|
|
583
|
+
// tracked read, so a favicon.png that appears later is linked.
|
|
584
|
+
const icon = imageURL(settings.lastNonNull("fm_icon"), api, imageSizes[0])
|
|
585
|
+
?? (api.target("favicon.png") ? "/favicon.png" : "/favicon-placeholder.png")
|
|
586
|
+
treeHead.children.push(h("link", {
|
|
587
|
+
href: icon,
|
|
588
|
+
rel: "icon",
|
|
589
|
+
type: iconType(icon)
|
|
590
|
+
}))
|
|
432
591
|
|
|
433
592
|
// The header nav is a partial: the lists for this page's folder chain,
|
|
434
593
|
// computed once per pass by createPartialStubs and rendered once by
|
|
@@ -436,55 +595,18 @@ function writeFile(target, { settings, api, config }) {
|
|
|
436
595
|
// beside shop/) takes that folder's chain, which ends in its own
|
|
437
596
|
// list; otherwise its folder's. The first lookup may miss - votive
|
|
438
597
|
// tracks the miss, so the page is rebuilt if that folder appears.
|
|
439
|
-
const treeNav = navPartial ? withCurrent(navPartial.metadata.hast, metadata.prettyURL) :
|
|
598
|
+
const treeNav = navWithChoices(navPartial ? withCurrent(navPartial.metadata.hast, metadata.prettyURL) : null)
|
|
440
599
|
|
|
441
600
|
let treeBreadcrumbs = []
|
|
442
601
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
*
|
|
447
|
-
* The folder pass is gone, and this is a better place for it anyway:
|
|
448
|
-
* the lookup is tracked, so renaming a section's index page restales
|
|
449
|
-
* every page whose breadcrumb names it - which the setting never did.
|
|
450
|
-
*
|
|
451
|
-
* A folder with an index page is a link and takes that page's own
|
|
452
|
-
* breadcrumb. A folder without one is just a label: vowel no longer
|
|
453
|
-
* generates a page per folder, so linking there would 404.
|
|
454
|
-
*/
|
|
455
|
-
// A secret folder's target segment is a hash, so its readable name has
|
|
456
|
-
// to come from the page's own *source* path - de-salted, because the
|
|
457
|
-
// source keeps the salt and the salt must never reach the page. The
|
|
458
|
-
// page has one ancestor per target segment and one per source segment,
|
|
459
|
-
// aligned by depth, which is what lets the two be zipped.
|
|
460
|
-
const sourceFolderNames = target.source
|
|
461
|
-
? displayPath(target.source).split(path.sep).slice(0, -1)
|
|
462
|
-
: []
|
|
463
|
-
|
|
464
|
-
function folderCrumb(folderPath, index) {
|
|
465
|
-
if (!folderPath) {
|
|
466
|
-
const home = api.target("index.html")
|
|
467
|
-
return { label: home?.metadata?.breadcrumb || "Home", href: "/" }
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// A folder's index is `<folder>.html`, from an authored `<folder>.md`
|
|
471
|
-
// beside it or the generated stub.
|
|
472
|
-
const indexTarget = api.target(`${folderPath}.html`)
|
|
473
|
-
// index 0 is the root, so ancestor i is source segment i - 1.
|
|
474
|
-
const sourceName = sourceFolderNames[index - 1]
|
|
475
|
-
const label = indexTarget?.metadata?.breadcrumb
|
|
476
|
-
|| toTitleCase(sourceName ?? folderPath.split(path.sep).at(-1))
|
|
477
|
-
// folderPath is a stored folder (path.sep); an href is a url.
|
|
478
|
-
return { label, href: indexTarget ? "/" + folderPath.split(path.sep).join("/") : null }
|
|
479
|
-
}
|
|
480
|
-
|
|
602
|
+
// Always an <a>, with or without an href: the breadcrumb's CSS styles
|
|
603
|
+
// its links, and a <span> among them broke it. A crumb has no href
|
|
604
|
+
// only when its folder has no index page to go to.
|
|
481
605
|
treeBreadcrumbs.push(
|
|
482
606
|
...ancestorFolders
|
|
483
607
|
.map(folderCrumb)
|
|
484
608
|
.filter(crumb => crumb.label)
|
|
485
|
-
.map(crumb => crumb.href
|
|
486
|
-
? h('a', { href: crumb.href }, crumb.label)
|
|
487
|
-
: h('span', crumb.label))
|
|
609
|
+
.map(crumb => h('a', crumb.href ? { href: crumb.href } : {}, crumb.label))
|
|
488
610
|
)
|
|
489
611
|
|
|
490
612
|
if (!isRoot) {
|
|
@@ -500,8 +622,10 @@ function writeFile(target, { settings, api, config }) {
|
|
|
500
622
|
const homeLink = []
|
|
501
623
|
|
|
502
624
|
|
|
503
|
-
|
|
504
|
-
|
|
625
|
+
// The smallest derivative of a png or jpeg (image.js): the original
|
|
626
|
+
// is not published. An SVG is linked as it is.
|
|
627
|
+
const logo = imageURL(settings.lastNonNull("logo"), api, imageSizes[0])
|
|
628
|
+
const wordmark = imageURL(settings.lastNonNull("fm_wordmark"), api, imageSizes[0])
|
|
505
629
|
|
|
506
630
|
const identityElements = []
|
|
507
631
|
|
|
@@ -509,7 +633,7 @@ function writeFile(target, { settings, api, config }) {
|
|
|
509
633
|
identityElements.push(h("img#logo", {
|
|
510
634
|
src: logo,
|
|
511
635
|
"aria-label": "Logo",
|
|
512
|
-
style: `--logo
|
|
636
|
+
style: `--url-logo: url("${logo}")`,
|
|
513
637
|
alt: ""
|
|
514
638
|
}))
|
|
515
639
|
}
|
|
@@ -538,7 +662,7 @@ function writeFile(target, { settings, api, config }) {
|
|
|
538
662
|
const treeHeader = h('header', [
|
|
539
663
|
identity,
|
|
540
664
|
treeNav,
|
|
541
|
-
socialLinksNav(settings)
|
|
665
|
+
socialLinksNav(settings, api)
|
|
542
666
|
])
|
|
543
667
|
|
|
544
668
|
|
|
@@ -560,10 +684,6 @@ function writeFile(target, { settings, api, config }) {
|
|
|
560
684
|
|
|
561
685
|
const treeTableOfContents = treeContentSlugged.children.shift()
|
|
562
686
|
|
|
563
|
-
const treeMainHead = makeFrontmatter(metadata, api, config)
|
|
564
|
-
|
|
565
|
-
treeMainHead.push(treeTableOfContents)
|
|
566
|
-
|
|
567
687
|
// One walk for everything that renders against other targets, settings
|
|
568
688
|
// or the url store: relative links, wikilinks, icons, code highlighting,
|
|
569
689
|
// images, directives, link previews and alerts. See writeRules.js.
|
|
@@ -571,6 +691,13 @@ function writeFile(target, { settings, api, config }) {
|
|
|
571
691
|
const walked = { target, api, config, settings, makeHeader, makeTable, highlighted: false }
|
|
572
692
|
writeWalk(abstract, walked)
|
|
573
693
|
|
|
694
|
+
// After the walk, so a natural image is already its <picture>.
|
|
695
|
+
const natural = naturalMetadata(abstract)
|
|
696
|
+
const treeMainHead = makeFrontmatter(metadata, api, config, natural)
|
|
697
|
+
|
|
698
|
+
const tableOfContents = navWithChoices(treeTableOfContents)
|
|
699
|
+
if (tableOfContents) treeMainHead.push(tableOfContents)
|
|
700
|
+
|
|
574
701
|
if (walked.highlighted) {
|
|
575
702
|
// syntax-highlighting.css is always declared by the styles processor
|
|
576
703
|
// (a stub); a page only decides whether to link it.
|
|
@@ -587,28 +714,33 @@ function writeFile(target, { settings, api, config }) {
|
|
|
587
714
|
|
|
588
715
|
const treeMain = h('main',
|
|
589
716
|
{
|
|
590
|
-
itemscope: true
|
|
717
|
+
itemscope: true,
|
|
718
|
+
// The page image's dominant colour, for anything in <main> to use.
|
|
719
|
+
style: dominantStyle(metadata.image, api)
|
|
591
720
|
},
|
|
592
721
|
[
|
|
593
722
|
// The title is main's first child: the one piece of data hoisted out
|
|
594
723
|
// of the content regardless of where the author placed it.
|
|
595
|
-
...(metadata.title ? [h("h1", metadata.title)] : []),
|
|
596
|
-
// A
|
|
597
|
-
|
|
598
|
-
h(
|
|
724
|
+
...(metadata.title ? [h("h1", metadata.fm_title ? { className: ["fm_title"] } : {}, metadata.title)] : []),
|
|
725
|
+
// A slogan is the page's, not the site's: under its title. An
|
|
726
|
+
// itemprop, not an id: a listing can hold several, one per card.
|
|
727
|
+
...(metadata.fm_slogan ? [h("p.fm_slogan", { itemprop: "slogan" }, metadata.fm_slogan)] : []),
|
|
728
|
+
navWithChoices(h('nav', {
|
|
599
729
|
'aria-label': 'Breadcrumbs'
|
|
600
|
-
}, treeBreadcrumbs),
|
|
730
|
+
}, treeBreadcrumbs)),
|
|
601
731
|
treeMainHead,
|
|
602
732
|
h('section#content', abstract),
|
|
603
733
|
treeBacklinks
|
|
604
734
|
].filter(Boolean))
|
|
605
735
|
|
|
606
|
-
// The aside tree is one partial, the same on every page.
|
|
736
|
+
// The aside tree is one partial, the same on every page. Without a
|
|
737
|
+
// nav worth rendering there is no aside at all.
|
|
607
738
|
const asidePartial = api.target(ASIDE_PARTIAL)
|
|
608
|
-
const
|
|
739
|
+
const asideNav = navWithChoices(asidePartial ? asidePartial.metadata.hast : null)
|
|
740
|
+
const treeAside = asideNav ? h("aside", asideNav) : null
|
|
609
741
|
|
|
610
742
|
const treeFooter = h('footer', [
|
|
611
|
-
socialLinksNav(settings),
|
|
743
|
+
socialLinksNav(settings, api),
|
|
612
744
|
h('section#copyright', `© ${new Date().getFullYear()}`),
|
|
613
745
|
h('section#shoutout', [
|
|
614
746
|
"Made with ",
|
|
@@ -618,14 +750,7 @@ function writeFile(target, { settings, api, config }) {
|
|
|
618
750
|
])
|
|
619
751
|
])
|
|
620
752
|
|
|
621
|
-
const
|
|
622
|
-
.split("/")
|
|
623
|
-
.filter(a => a)
|
|
624
|
-
.map(a => a.replace("_", ""))
|
|
625
|
-
.join("_")
|
|
626
|
-
|| "home"
|
|
627
|
-
|
|
628
|
-
const treeBody = h(`body.${pageClass}`, [
|
|
753
|
+
const treeBody = h("body", { className: pageClasses(target.metadata.prettyURL, target.source) }, [
|
|
629
754
|
treeHeader,
|
|
630
755
|
treeMain,
|
|
631
756
|
treeAside,
|
|
@@ -682,6 +807,7 @@ function makeTable(classes, properties, targets, api) {
|
|
|
682
807
|
|
|
683
808
|
const cell = (target, name) => {
|
|
684
809
|
const value = valueOf(target.metadata, name)
|
|
810
|
+
if (name === "title" && isPageWithoutFile(target)) return h("td", value ?? "")
|
|
685
811
|
if (name === "title") return h("td", h("a", { href: target.metadata.prettyURL }, value ?? target.metadata.prettyURL))
|
|
686
812
|
if (value === undefined || value === null) return h("td")
|
|
687
813
|
if (name === "image") return h("td", createDynamicImage(String(value), api, "") ?? String(value))
|
|
@@ -690,10 +816,12 @@ function makeTable(classes, properties, targets, api) {
|
|
|
690
816
|
return h("td", String(value))
|
|
691
817
|
}
|
|
692
818
|
|
|
693
|
-
|
|
819
|
+
// In a <figure>, like a markdown table (markdown/index.js). The
|
|
820
|
+
// directive's parameters stay on the <table>, where the editor reads them.
|
|
821
|
+
return h("figure", h("table", { class: classes }, [
|
|
694
822
|
h("thead", h("tr", properties.map(name => h("th", name)))),
|
|
695
823
|
h("tbody", targets.map(target => h("tr", properties.map(name => cell(target, name)))))
|
|
696
|
-
])
|
|
824
|
+
]))
|
|
697
825
|
}
|
|
698
826
|
|
|
699
827
|
/**
|