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.
@@ -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