vowel 0.6.3 → 0.6.4
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/README.md +22 -16
- package/bin.js +41 -4
- package/config.js +28 -9
- package/package.json +5 -1
- package/plugins/deploy-cloudflare/index.js +78 -0
- package/plugins/fonts/index.js +2 -2
- package/plugins/html/image.js +78 -0
- package/plugins/html/index.js +788 -0
- package/plugins/images/index.js +5 -5
- package/plugins/markdown/index.js +141 -1070
- package/plugins/markdown/metadata.js +147 -0
- package/plugins/markdown/robots.js +18 -0
- package/plugins/styles/index.js +6 -14
- package/plugins/urls/index.js +91 -0
- package/plugins/vectors/index.js +1 -1
- package/plugins/xml/index.js +25 -14
- package/stylesheets/SyntaxHighlightingStyles.css +237 -0
- package/utils.js +2 -2
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import openSocket from "voot/client.js"
|
|
3
|
+
import rehypeHighlight from "rehype-highlight"
|
|
4
|
+
import rehypePresetMinify from "rehype-preset-minify"
|
|
5
|
+
import rehypeStringify from "rehype-stringify"
|
|
6
|
+
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
7
|
+
import { frontmatter } from "micromark-extension-frontmatter"
|
|
8
|
+
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
|
|
9
|
+
import { gfmFootnote } from "micromark-extension-gfm-footnote"
|
|
10
|
+
import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"
|
|
11
|
+
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
|
|
12
|
+
import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough'
|
|
13
|
+
import { gfmTable } from 'micromark-extension-gfm-table'
|
|
14
|
+
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
|
|
15
|
+
import { h } from 'hastscript'
|
|
16
|
+
import { readFileSync } from "fs"
|
|
17
|
+
import { remove } from "unist-util-remove"
|
|
18
|
+
import { testURL, testHashtags, createHashtagPage, toTitleCase, hashtagRegexSingle, createImagePaths, imageSizes, imageExts } from "./../../utils.js"
|
|
19
|
+
import { toString as hastToString } from 'hast-util-to-string'
|
|
20
|
+
import { unified } from "unified"
|
|
21
|
+
import { EXIT, SKIP, visit } from "unist-util-visit"
|
|
22
|
+
import toc from "@jsdevtools/rehype-toc"
|
|
23
|
+
import slug from "rehype-slug"
|
|
24
|
+
import createDynamicImage from "./image.js"
|
|
25
|
+
import { isExternalLinkParagraph } from "../urls/index.js"
|
|
26
|
+
|
|
27
|
+
/** @import * as Votive from "votive" */
|
|
28
|
+
/** @import * as Vowel from "./../../index.js" */
|
|
29
|
+
|
|
30
|
+
const VOWEL_DIR = path.normalize(path.join(import.meta.dirname, "../../"))
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {array} array
|
|
34
|
+
* @param {number} num
|
|
35
|
+
*/
|
|
36
|
+
function getLast(ancestorArrays, num = 1) {
|
|
37
|
+
if (!ancestorArrays) return
|
|
38
|
+
if (num > ancestorArrays.length) return
|
|
39
|
+
const level = ancestorArrays.at(ancestorArrays.length - num)
|
|
40
|
+
if (level && level.length) return level.at(-1)
|
|
41
|
+
return getLast(ancestorArrays, num + 1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} metadata
|
|
47
|
+
* @param {string} url
|
|
48
|
+
* @param {import("votive").PluginAPI} api
|
|
49
|
+
* @param {Votive.VotiveConfig} config
|
|
50
|
+
*/
|
|
51
|
+
function makeHeader(metadata, url, api, config) {
|
|
52
|
+
|
|
53
|
+
const treeMainHead = []
|
|
54
|
+
|
|
55
|
+
if (metadata.title) treeMainHead.push(
|
|
56
|
+
h("h1", metadata.title)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (metadata.date) {
|
|
60
|
+
const date = new Date(metadata.date)
|
|
61
|
+
treeMainHead.push(
|
|
62
|
+
h("time",
|
|
63
|
+
{
|
|
64
|
+
datetime: date.toISOString(),
|
|
65
|
+
itemprop: "date"
|
|
66
|
+
},
|
|
67
|
+
date.toLocaleDateString("en-US", {
|
|
68
|
+
year: "numeric",
|
|
69
|
+
month: "long",
|
|
70
|
+
day: "numeric"
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (metadata.image || (metadata.first_image && url)) {
|
|
77
|
+
const metaImage = metadata.image || metadata.first_image
|
|
78
|
+
|
|
79
|
+
treeMainHead.push(
|
|
80
|
+
createDynamicImage(metaImage, api, null, true)
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (metadata.fm_description) treeMainHead.push(
|
|
85
|
+
h("p",
|
|
86
|
+
{
|
|
87
|
+
itemprop: "description"
|
|
88
|
+
},
|
|
89
|
+
metadata.fm_description
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (url) return h('a', { href: url }, treeMainHead)
|
|
94
|
+
|
|
95
|
+
return treeMainHead
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @type {Votive.ProcessorWrite} */
|
|
99
|
+
function writeFile(target, settings, api, config) {
|
|
100
|
+
|
|
101
|
+
/** @param {string} relativePath */
|
|
102
|
+
function resolvePath(relativePath) {
|
|
103
|
+
if (relativePath.startsWith("./")) {
|
|
104
|
+
const dir = path.dirname(target.source)
|
|
105
|
+
const sourcePath = path.normalize(path.join(dir, relativePath))
|
|
106
|
+
const targetFile = api.targetBySource(sourcePath)
|
|
107
|
+
return targetFile.metadata.prettyURL
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
visit(target.metadata.hastAbstract, { tagName: "a" }, (n, i, p) => {
|
|
112
|
+
const resolvedPath = resolvePath(n.properties.href)
|
|
113
|
+
if(resolvedPath) n.properties.href = resolvedPath
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const isRoot = target.path === "index.html"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if (target.metadata.type === "tag") {
|
|
120
|
+
if (!target.metadata.tag) return false
|
|
121
|
+
|
|
122
|
+
const pages = api.targets({
|
|
123
|
+
recursive: true,
|
|
124
|
+
query: {
|
|
125
|
+
tags: target.metadata.tag
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if (!pages.length) return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { metadata, ...rest } = target
|
|
133
|
+
const abstract = metadata.hastAbstract
|
|
134
|
+
|
|
135
|
+
// Highlights fenced code blocks in place (adds an `hljs` class plus
|
|
136
|
+
// per-token spans to any <code class="language-x">) - run early, before
|
|
137
|
+
// treeStyleSheets below, so the stylesheet link can be added only for
|
|
138
|
+
// pages that actually end up with a highlighted block.
|
|
139
|
+
unified().use(rehypeHighlight).runSync(abstract)
|
|
140
|
+
|
|
141
|
+
let hasHighlightedCode = false
|
|
142
|
+
visit(abstract, { tagName: "code" }, (node) => {
|
|
143
|
+
if (node.properties?.className?.includes("hljs")) {
|
|
144
|
+
hasHighlightedCode = true
|
|
145
|
+
return EXIT
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
/** @param {string} filePath */
|
|
150
|
+
function listFolders(filePath) {
|
|
151
|
+
if (!filePath) return []
|
|
152
|
+
|
|
153
|
+
const pathInfo = path.parse(filePath)
|
|
154
|
+
const dir = pathInfo.dir && pathInfo.dir
|
|
155
|
+
return [...listFolders(
|
|
156
|
+
dir
|
|
157
|
+
), filePath]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const parsedPath = path.parse("" + target.path)
|
|
161
|
+
|
|
162
|
+
const targetAsDir = path.relative("", path.format({
|
|
163
|
+
dir: parsedPath.dir,
|
|
164
|
+
name: parsedPath.name
|
|
165
|
+
}))
|
|
166
|
+
|
|
167
|
+
const ancestorFolders = listFolders(rest.dir)
|
|
168
|
+
ancestorFolders.unshift("")
|
|
169
|
+
|
|
170
|
+
const family = [...ancestorFolders, targetAsDir].flatMap(folder => {
|
|
171
|
+
// FIXME typing
|
|
172
|
+
return api.targets({
|
|
173
|
+
folder: Array.isArray(folder) ? path.join(...folder) : folder,
|
|
174
|
+
recursive: false,
|
|
175
|
+
query: {
|
|
176
|
+
"!": {
|
|
177
|
+
"|": {
|
|
178
|
+
local_menu_item: 0, // FIXME: Change to boolean,
|
|
179
|
+
html_file: 0
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}).filter(({ path, dir }) => {
|
|
185
|
+
return path && path !== "tags.html" && dir !== "tags"
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const treeStyleSheets = []
|
|
189
|
+
|
|
190
|
+
Object.values(settings.stylesheets).forEach(file => {
|
|
191
|
+
if (file) {
|
|
192
|
+
file.forEach(sheet => {
|
|
193
|
+
const cacheBuster = Math.random().toString(36).slice(2, 10);
|
|
194
|
+
treeStyleSheets.push(
|
|
195
|
+
h('link', {
|
|
196
|
+
rel: "stylesheet",
|
|
197
|
+
href: `/${sheet}?${cacheBuster}`
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
if (hasHighlightedCode) {
|
|
205
|
+
if (!api.target("syntax-highlighting.css")) {
|
|
206
|
+
const syntaxHighlightingStylesPath = path.join(VOWEL_DIR, "stylesheets", "SyntaxHighlightingStyles.css")
|
|
207
|
+
const syntaxHighlightingStyles = readFileSync(syntaxHighlightingStylesPath, "utf-8")
|
|
208
|
+
|
|
209
|
+
api.createTarget({
|
|
210
|
+
path: "syntax-highlighting.css",
|
|
211
|
+
abstract: { css: syntaxHighlightingStyles },
|
|
212
|
+
metadata: {},
|
|
213
|
+
extension: "css"
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
treeStyleSheets.push(
|
|
218
|
+
h('link', {
|
|
219
|
+
rel: "stylesheet",
|
|
220
|
+
href: "/syntax-highlighting.css"
|
|
221
|
+
})
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function createTitle() {
|
|
226
|
+
if (isRoot) {
|
|
227
|
+
const title = [settings?.title?.[0]?.at(-1) || metadata?.title, settings?.fm_tagline?.[0]?.at(-1)]
|
|
228
|
+
.filter(a => a)
|
|
229
|
+
.join(" - ")
|
|
230
|
+
|
|
231
|
+
return title
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// FIXME Check that this works properly
|
|
235
|
+
if (metadata.title && settings.title) {
|
|
236
|
+
const titles = [metadata.title, ...Object.values(settings.title).flatMap(a => a).reverse()]
|
|
237
|
+
return titles.join(" - ")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (metadata.title || settings.title) {
|
|
241
|
+
return metadata.title || getLast(settings.title)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return "Website"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
const title = createTitle()
|
|
249
|
+
|
|
250
|
+
const treeHead = h('head', [
|
|
251
|
+
h('meta', {
|
|
252
|
+
charset: "UTF-8",
|
|
253
|
+
}),
|
|
254
|
+
h("meta", {
|
|
255
|
+
name: "viewport",
|
|
256
|
+
content: "width=device-width, initial-scale=1.0"
|
|
257
|
+
}),
|
|
258
|
+
h("meta", {
|
|
259
|
+
"http-equiv": "X-UA-Compatible",
|
|
260
|
+
content: "ie-edge"
|
|
261
|
+
}),
|
|
262
|
+
h('title', title),
|
|
263
|
+
h('meta', {
|
|
264
|
+
property: "og:title",
|
|
265
|
+
content: title
|
|
266
|
+
}),
|
|
267
|
+
h('meta', {
|
|
268
|
+
property: "og:description",
|
|
269
|
+
content: metadata.description,
|
|
270
|
+
}),
|
|
271
|
+
...treeStyleSheets,
|
|
272
|
+
])
|
|
273
|
+
|
|
274
|
+
if (settings.fm_domain?.[0]?.length) {
|
|
275
|
+
const settingsDomain = settings.fm_domain[0].at(-1)
|
|
276
|
+
const domain = settingsDomain.startsWith("http")
|
|
277
|
+
? settingsDomain
|
|
278
|
+
: "https://" + settingsDomain
|
|
279
|
+
|
|
280
|
+
const { href } = new URL(metadata.prettyURL, domain)
|
|
281
|
+
|
|
282
|
+
treeHead.children.push(
|
|
283
|
+
h("meta", {
|
|
284
|
+
property: "og:url",
|
|
285
|
+
content: href
|
|
286
|
+
}),
|
|
287
|
+
h("link", {
|
|
288
|
+
rel: "canonical",
|
|
289
|
+
href
|
|
290
|
+
})
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (metadata.image) {
|
|
295
|
+
treeHead.children.push(h("meta", {
|
|
296
|
+
property: "og:image",
|
|
297
|
+
content: metadata.image
|
|
298
|
+
}))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
/* FIXME this could be a section title */
|
|
303
|
+
if (settings.title) {
|
|
304
|
+
treeHead.children.push(h("meta", {
|
|
305
|
+
property: "og:site_name",
|
|
306
|
+
content: getLast(settings.fm_title)
|
|
307
|
+
}))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* FIXME Properly handle this image */
|
|
311
|
+
if (settings.icon) {
|
|
312
|
+
treeHead.children.push(h("link", {
|
|
313
|
+
href: "/" + getLast(settings.icon),
|
|
314
|
+
rel: "icon",
|
|
315
|
+
type: "image/png"
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function treeNavItems(navItem) {
|
|
320
|
+
return h('li', h('a', {
|
|
321
|
+
href: navItem.metadata.prettyURL,
|
|
322
|
+
"aria-current": metadata.prettyURL === navItem.metadata.prettyURL ? 'page' : null
|
|
323
|
+
}, navItem.metadata.breadcrumb))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function navItemFilter(nav_item) {
|
|
327
|
+
return !nav_item.metadata.date
|
|
328
|
+
&& nav_item.path !== "index.html"
|
|
329
|
+
&& nav_item.path !== "404.html"
|
|
330
|
+
&& nav_item.extension === ".html"
|
|
331
|
+
&& nav_item.path
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function sort_items(a, b) {
|
|
335
|
+
if (typeof a === "number" && typeof b === "number") return a - b
|
|
336
|
+
if (typeof b === "number") return -1
|
|
337
|
+
if (typeof a === "number") return 1
|
|
338
|
+
if (a.metadata.breadcrumb && b.metadata.breadcrumb) return String(a.metadata.breadcrumb).localeCompare(String(b.metadata.breadcrumb))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function treeNavFolder(navFolder) {
|
|
342
|
+
const sorted = navFolder
|
|
343
|
+
.filter(navItemFilter)
|
|
344
|
+
.toSorted(sort_items)
|
|
345
|
+
|
|
346
|
+
return h('ul', sorted.map(treeNavItems))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
const groupedNavs = Object.groupBy(family, ({ dir }) => dir)
|
|
351
|
+
|
|
352
|
+
const treeNav = h('nav', Object.entries(groupedNavs)
|
|
353
|
+
.sort(([a], [b]) => a.length - b.length)
|
|
354
|
+
.map(([k, v]) => treeNavFolder(v))
|
|
355
|
+
.filter(folder => folder.children.length)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
let treeBreadcrumbs = []
|
|
359
|
+
|
|
360
|
+
const breadcrumbs = ancestorFolders
|
|
361
|
+
.map((folderPath, index) => [folderPath, settings.breadcrumbs?.[index]?.at(-1)])
|
|
362
|
+
.filter(([, label]) => label != null)
|
|
363
|
+
|
|
364
|
+
treeBreadcrumbs.push(
|
|
365
|
+
...breadcrumbs.map(([folderPath, label]) => {
|
|
366
|
+
return h('a', {
|
|
367
|
+
href: folderPath ? "/" + folderPath : "/"
|
|
368
|
+
}, label)
|
|
369
|
+
})
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if (!isRoot) {
|
|
373
|
+
treeBreadcrumbs.push(
|
|
374
|
+
h('a', {
|
|
375
|
+
href: target.metadata.prettyURL,
|
|
376
|
+
'aria-current': 'page'
|
|
377
|
+
}, metadata.breadcrumb)
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const headerElements = []
|
|
382
|
+
|
|
383
|
+
const homeLink = []
|
|
384
|
+
|
|
385
|
+
if (settings.fm_logo?.[0]?.length) {
|
|
386
|
+
headerElements.push(
|
|
387
|
+
h('a#logo', {
|
|
388
|
+
href: "/",
|
|
389
|
+
"aria-label": "logo",
|
|
390
|
+
rel: "home",
|
|
391
|
+
"style": `--logo-url: url("/${getLast(settings.fm_logo)}")`
|
|
392
|
+
}, h("img", {
|
|
393
|
+
src: "/" + getLast(settings.fm_logo),
|
|
394
|
+
alt: ""
|
|
395
|
+
}))
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (settings.fm_wordmark && getLast(settings.fm_wordmark)) {
|
|
400
|
+
headerElements.push(h("a#wordmark", {
|
|
401
|
+
href: "/",
|
|
402
|
+
rel: "home"
|
|
403
|
+
}, h("img", {
|
|
404
|
+
src: "/" + getLast(settings.fm_wordmark)
|
|
405
|
+
})))
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (settings.title?.[0]?.length) {
|
|
409
|
+
headerElements.push(h('a#title', { href: "/", rel: "home" }, getLast(settings.title)))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (settings.fm_tagline && getLast(settings.fm_tagline)) {
|
|
413
|
+
headerElements.push(h('p#tagline', getLast(settings.fm_tagline)))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const treeHeader = h('header', [
|
|
417
|
+
...headerElements,
|
|
418
|
+
treeNav
|
|
419
|
+
])
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
function testPaths(node, i, p) {
|
|
423
|
+
if (node.type !== 'element') return
|
|
424
|
+
if (node.tagName !== 'p') return
|
|
425
|
+
if (node.children.length !== 1) return
|
|
426
|
+
if (!node.children[0]) return
|
|
427
|
+
if (!node.children[0].value) return
|
|
428
|
+
return Boolean(node.children[0].value.match(/^\/\S*$/))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const slugger = unified()
|
|
432
|
+
.use(slug)
|
|
433
|
+
.use(toc, {
|
|
434
|
+
customizeTOC: (toc) => {
|
|
435
|
+
toc.properties = {
|
|
436
|
+
"aria-label": "Contents"
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const treeContentSlugged = slugger.runSync(abstract)
|
|
442
|
+
|
|
443
|
+
const treeTableOfContents = treeContentSlugged.children.shift()
|
|
444
|
+
|
|
445
|
+
const treeMainHead = makeHeader(metadata, null, api, config)
|
|
446
|
+
|
|
447
|
+
treeMainHead.push(treeTableOfContents)
|
|
448
|
+
|
|
449
|
+
visit(abstract, { tagName: "img" }, (node, index, parent) => {
|
|
450
|
+
const { src, alt } = node.properties
|
|
451
|
+
const image = createDynamicImage(src, api, alt)
|
|
452
|
+
if (!image) return
|
|
453
|
+
if (index === 0 && parent.children.length > 1) {
|
|
454
|
+
const [_, ...caption] = parent.children
|
|
455
|
+
parent.children = [
|
|
456
|
+
h("figure", [image, h("figcaption", caption)])
|
|
457
|
+
]
|
|
458
|
+
} else {
|
|
459
|
+
parent.children.splice(index, 1, image)
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
visit(abstract, testPaths, ({ children: [child] }, i, p) => {
|
|
465
|
+
|
|
466
|
+
// const recursive = child.value.endsWith("**")
|
|
467
|
+
// const many = child.value.endsWith("*")
|
|
468
|
+
|
|
469
|
+
const url = new URL(child.value, "thismessage://")
|
|
470
|
+
const { dir, base } = path.parse(url.pathname)
|
|
471
|
+
const recursive = base === "**"
|
|
472
|
+
const many = base === "*" || base === "**"
|
|
473
|
+
|
|
474
|
+
if (!many) {
|
|
475
|
+
const targetFilePathInfo = path.parse(child.value)
|
|
476
|
+
targetFilePathInfo.ext ||= ".html"
|
|
477
|
+
delete targetFilePathInfo.base
|
|
478
|
+
const targetFilePath = path.relative("/", path.format(targetFilePathInfo))
|
|
479
|
+
const target = api.target(targetFilePath)
|
|
480
|
+
|
|
481
|
+
if (target) {
|
|
482
|
+
const article = h('article', makeHeader(target.metadata, target.metadata.prettyURL, api, config))
|
|
483
|
+
|
|
484
|
+
p.children.splice(i, 1, article)
|
|
485
|
+
|
|
486
|
+
return SKIP
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (many) {
|
|
492
|
+
const folder = path.relative("/", dir)
|
|
493
|
+
// const url = new URL(child.value, "thismessage://")
|
|
494
|
+
const count = url.searchParams.get("count")
|
|
495
|
+
const tag = url.searchParams.get("tag")
|
|
496
|
+
const query = tag
|
|
497
|
+
? { tags: tag }
|
|
498
|
+
: {}
|
|
499
|
+
|
|
500
|
+
const targets = api.targets({
|
|
501
|
+
folder,
|
|
502
|
+
recursive,
|
|
503
|
+
query,
|
|
504
|
+
orderBy: { property: "date", direction: "desc" },
|
|
505
|
+
limit: count ? Number(count) : undefined
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const escapedDir = folder.replace("_", "--").replace(path.sep, "_")
|
|
509
|
+
const escapedDirs = escapedDir.split("_").filter(a => a).map((segment, index, array) => {
|
|
510
|
+
return "_" + array.slice(0, index + 1).join("_")
|
|
511
|
+
})
|
|
512
|
+
escapedDirs.unshift("_")
|
|
513
|
+
const dirClasses = escapedDirs.join(".")
|
|
514
|
+
|
|
515
|
+
const list = h(`ul.${dirClasses}`,
|
|
516
|
+
targets.map(target => {
|
|
517
|
+
return h('li',
|
|
518
|
+
h('article', makeHeader(target.metadata, target.metadata.prettyURL, api, config))
|
|
519
|
+
)
|
|
520
|
+
})
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
p.children.splice(i, 1, list)
|
|
524
|
+
|
|
525
|
+
return SKIP
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
})
|
|
529
|
+
} catch (e) {
|
|
530
|
+
// console.log(JSON.stringify(abstract, null, 2))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
visit(abstract, isExternalLinkParagraph, ({ children: [child] }, i, p) => {
|
|
535
|
+
const url = child.value
|
|
536
|
+
const preview = api.url.get(url)
|
|
537
|
+
if (!preview) return
|
|
538
|
+
|
|
539
|
+
const card = h('a.link-preview', { href: url, target: "_blank", rel: "noopener noreferrer" }, [
|
|
540
|
+
preview.image ? h('img.link-preview-image', { src: preview.image, alt: "" }) : null,
|
|
541
|
+
h('span.link-preview-body', [
|
|
542
|
+
h('span.link-preview-title', preview.title || url),
|
|
543
|
+
preview.description ? h('span.link-preview-description', preview.description) : null
|
|
544
|
+
].filter(Boolean))
|
|
545
|
+
].filter(Boolean))
|
|
546
|
+
|
|
547
|
+
p.children.splice(i, 1, card)
|
|
548
|
+
|
|
549
|
+
return SKIP
|
|
550
|
+
})
|
|
551
|
+
} catch (e) {
|
|
552
|
+
// console.log(JSON.stringify(abstract, null, 2))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
remove(abstract, (n, i, p) => p.type === "root" && n.tagName === "h1")
|
|
557
|
+
} catch (e) {
|
|
558
|
+
// console.log(JSON.stringify(abstract, null, 2))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// function copyTreeWithoutArticles(tree) {
|
|
562
|
+
// if (tree.tagName !== 'article') {
|
|
563
|
+
// return {
|
|
564
|
+
// type: tree.type,
|
|
565
|
+
// tagName: tree.tagName,
|
|
566
|
+
// properties: tree.properties,
|
|
567
|
+
// children: tree.children?.map(copyTreeWithoutArticles)
|
|
568
|
+
// }
|
|
569
|
+
// }
|
|
570
|
+
// }
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
const treeMain = h('main',
|
|
576
|
+
{
|
|
577
|
+
itemscope: true
|
|
578
|
+
},
|
|
579
|
+
[
|
|
580
|
+
h('nav', {
|
|
581
|
+
'aria-label': 'Breadcrumbs'
|
|
582
|
+
}, treeBreadcrumbs),
|
|
583
|
+
treeMainHead,
|
|
584
|
+
h('section#content', abstract)
|
|
585
|
+
])
|
|
586
|
+
|
|
587
|
+
visit(treeMain, (node, index, parent) => {
|
|
588
|
+
/* URLs */ if (node.type === "text" && parent.tagName === 'p' && parent.children.length === 1) {
|
|
589
|
+
const validURL = testURL(node.value)
|
|
590
|
+
if (validURL) {
|
|
591
|
+
const metadata = api.url.get(node.value)
|
|
592
|
+
if (metadata) {
|
|
593
|
+
parent.tagName = "article"
|
|
594
|
+
parent.children = [
|
|
595
|
+
h("a", { href: node.value },
|
|
596
|
+
h("h2", metadata.title)
|
|
597
|
+
)
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} /* GFM Alerts */ else if (node.tagName === "blockquote") {
|
|
602
|
+
if (node.children[1]
|
|
603
|
+
&& node.children[1].tagName === "p"
|
|
604
|
+
&& node.children[1].children.length === 1
|
|
605
|
+
&& node.children[1].children[0].type === "text"
|
|
606
|
+
) {
|
|
607
|
+
const matches = node.children[1].children[0].value.match(/^\[!(\w+)\]$/)
|
|
608
|
+
|
|
609
|
+
if (matches) {
|
|
610
|
+
const [_, alertLabel] = matches
|
|
611
|
+
|
|
612
|
+
node.tagName = "aside"
|
|
613
|
+
node.properties = {
|
|
614
|
+
class: `alert ${alertLabel.toLowerCase()}`
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
node.children.splice(0, 2, {
|
|
618
|
+
type: "element",
|
|
619
|
+
tagName: "h2",
|
|
620
|
+
children: [
|
|
621
|
+
{
|
|
622
|
+
value: toTitleCase(alertLabel),
|
|
623
|
+
type: "text"
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
const everything = api.targets({
|
|
634
|
+
folder: "",
|
|
635
|
+
recursive: true,
|
|
636
|
+
}).filter(target => target.path
|
|
637
|
+
&& target.path.endsWith(".html")
|
|
638
|
+
&& !target.metadata.date
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
const homeFile = everything.find(item => item.path === "index.html" && item.dir === "")
|
|
642
|
+
|
|
643
|
+
const globalNavItems = everything.filter(item => {
|
|
644
|
+
return item.dir === ""
|
|
645
|
+
&& item.path !== "index.html"
|
|
646
|
+
&& item.path !== "404.html"
|
|
647
|
+
&& item.path !== "tags.html"
|
|
648
|
+
&& item.metadata.global_menu_item !== 0 // FIXME: This data should come back as a boolean, not binary
|
|
649
|
+
&& item.metadata.html_file !== 0
|
|
650
|
+
})
|
|
651
|
+
.map(getChildren)
|
|
652
|
+
|
|
653
|
+
globalNavItems.unshift(homeFile)
|
|
654
|
+
|
|
655
|
+
function getChildren(item) {
|
|
656
|
+
const children = everything.filter(child => {
|
|
657
|
+
return "/" + child.dir === item.metadata.prettyURL
|
|
658
|
+
&& child.path !== "index.html"
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
const populatedChildren = children.length > 0 && children.map(child => {
|
|
662
|
+
return getChildren(child)
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
const node = {
|
|
666
|
+
path: "/" + item.path,
|
|
667
|
+
metadata: item.metadata
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (populatedChildren) node.children = populatedChildren
|
|
671
|
+
|
|
672
|
+
return node
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function treeNavItem(item) {
|
|
676
|
+
if (item.children) {
|
|
677
|
+
return h('li', [
|
|
678
|
+
h('a', { href: item.path }, item.metadata.breadcrumb),
|
|
679
|
+
treeNavList(item.children)
|
|
680
|
+
])
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return h('li',
|
|
684
|
+
h('a', { href: item.path }, item.metadata.breadcrumb)
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function treeNavList(items) {
|
|
689
|
+
return h('ul',
|
|
690
|
+
items.filter(a => a).map(treeNavItem)
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const treeGlobalNav = h('nav',
|
|
695
|
+
treeNavList(globalNavItems)
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
const treeAside = h('aside', treeGlobalNav)
|
|
699
|
+
|
|
700
|
+
const treeFooter = h('footer', [
|
|
701
|
+
h('section#copyright', `© ${new Date().getFullYear()}`),
|
|
702
|
+
h('section#shoutout', [
|
|
703
|
+
"Made with ",
|
|
704
|
+
h('a', {
|
|
705
|
+
href: "https://vowel.cc"
|
|
706
|
+
}, "Vowel"),
|
|
707
|
+
])
|
|
708
|
+
])
|
|
709
|
+
|
|
710
|
+
const pageClass = target.metadata.prettyURL
|
|
711
|
+
.split("/")
|
|
712
|
+
.filter(a => a)
|
|
713
|
+
.map(a => a.replace("_", ""))
|
|
714
|
+
.join("_")
|
|
715
|
+
|| "home"
|
|
716
|
+
|
|
717
|
+
const treeBody = h(`body.${pageClass}`, [
|
|
718
|
+
treeHeader,
|
|
719
|
+
treeMain,
|
|
720
|
+
treeAside,
|
|
721
|
+
treeFooter
|
|
722
|
+
])
|
|
723
|
+
|
|
724
|
+
const tree = h(
|
|
725
|
+
null,
|
|
726
|
+
[
|
|
727
|
+
{
|
|
728
|
+
type: "doctype",
|
|
729
|
+
name: 'html'
|
|
730
|
+
},
|
|
731
|
+
h('html',
|
|
732
|
+
{
|
|
733
|
+
lang: "en"
|
|
734
|
+
},
|
|
735
|
+
[
|
|
736
|
+
treeHead,
|
|
737
|
+
treeBody
|
|
738
|
+
]
|
|
739
|
+
)
|
|
740
|
+
]
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
const data = unified()
|
|
744
|
+
.use(rehypePresetMinify)
|
|
745
|
+
.use(rehypeStringify)
|
|
746
|
+
.stringify(tree)
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
data
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Injects voot's live-reload client into a served HTML page - dev-server
|
|
756
|
+
* only, never touches what's written to disk.
|
|
757
|
+
* @param {Buffer} body
|
|
758
|
+
*/
|
|
759
|
+
function handlePreviewRequest(body) {
|
|
760
|
+
const html = body.toString("utf-8")
|
|
761
|
+
const fileSplit = html.split("</body>")
|
|
762
|
+
fileSplit.splice(1, 0, `<script>${openSocket.toString()}\n\nopenSocket()</script>`)
|
|
763
|
+
return fileSplit.join("")
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Vowel always synthesizes a 404.html target (see readFolder in
|
|
767
|
+
// markdown/index.js) - when a requested .html page doesn't exist, serve
|
|
768
|
+
// that instead of an empty 404 body.
|
|
769
|
+
function handlePreviewError() {
|
|
770
|
+
return "404.html"
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const writeHTML = {
|
|
774
|
+
extensions: [".html"],
|
|
775
|
+
format: "text",
|
|
776
|
+
writeFile,
|
|
777
|
+
handlePreviewRequest,
|
|
778
|
+
handlePreviewError
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
/** @type {Votive.VotivePlugin} */
|
|
783
|
+
const vowelWriteHTMLPlugin = {
|
|
784
|
+
name: "vowel-write-html",
|
|
785
|
+
processors: [writeHTML],
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export default vowelWriteHTMLPlugin
|