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
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import extractDate from "./../../extractDate.js"
|
|
2
1
|
import path from "node:path"
|
|
3
|
-
import rehypeDocument from 'rehype-document'
|
|
4
|
-
import rehypePresetMinify from "rehype-preset-minify"
|
|
5
|
-
import rehypeStringify from "rehype-stringify"
|
|
6
|
-
import yaml from 'yaml'
|
|
7
2
|
import { fromHtml } from 'hast-util-from-html'
|
|
8
3
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
4
|
+
import { highlightMarkFromMarkdown } from "mdast-util-highlight-mark"
|
|
5
|
+
import { highlightMark } from "micromark-extension-highlight-mark"
|
|
9
6
|
import { frontmatter } from "micromark-extension-frontmatter"
|
|
10
7
|
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
|
|
11
8
|
import { gfmFootnote } from "micromark-extension-gfm-footnote"
|
|
@@ -13,234 +10,50 @@ import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"
|
|
|
13
10
|
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
|
|
14
11
|
import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough'
|
|
15
12
|
import { gfmTable } from 'micromark-extension-gfm-table'
|
|
16
|
-
import rehypeGithubAlert from "rehype-github-alert"
|
|
17
13
|
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
|
|
18
|
-
import { h } from 'hastscript'
|
|
19
14
|
import { normalizeHeadings } from 'mdast-normalize-headings'
|
|
20
15
|
import { readFileSync } from "fs"
|
|
21
|
-
import {
|
|
22
|
-
import { testURL, testHashtags, createHashtagPage, toTitleCase, hashtagRegexSingle, createImagePaths, imageSizes, imageExts } from "./../../utils.js"
|
|
16
|
+
import { testURL, testHashtags, createHashtagPage, toTitleCase, hashtagRegexSingle } from "./../../utils.js"
|
|
23
17
|
import { toHast } from 'mdast-util-to-hast'
|
|
24
18
|
import { toString as hastToString } from 'hast-util-to-string'
|
|
25
|
-
import { toString as mdastToString } from 'mdast-util-to-string'
|
|
26
|
-
import { unified } from "unified"
|
|
27
19
|
import { visit } from "unist-util-visit"
|
|
28
|
-
import
|
|
29
|
-
import
|
|
30
|
-
import
|
|
20
|
+
import getMetadata from "./metadata.js"
|
|
21
|
+
import generateRobots from "./robots.js"
|
|
22
|
+
import { h } from "hastscript"
|
|
23
|
+
import { hash } from "node:crypto"
|
|
24
|
+
import { styleText } from "node:util"
|
|
31
25
|
|
|
32
26
|
const VOWEL_DIR = path.normalize(path.join(import.meta.dirname, "../../"))
|
|
33
27
|
|
|
34
28
|
/** @import * as Votive from "votive" */
|
|
35
29
|
/** @import * as Vowel from "./../../index.js" */
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
const robots = `User-agent: Google-Extended
|
|
39
|
-
Allow: /
|
|
40
|
-
|
|
41
|
-
User-agent: Googlebot-Image
|
|
42
|
-
Disallow: /
|
|
43
|
-
|
|
44
|
-
User-agent: GPTBot
|
|
45
|
-
Disallow: /
|
|
46
|
-
|
|
47
|
-
User-agent: ChatGPT-User
|
|
48
|
-
Disallow: /
|
|
49
|
-
|
|
50
|
-
User-agent: CCBot
|
|
51
|
-
Disallow: /`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @param {string} imagePath
|
|
57
|
-
* @param {Votive.Database} database
|
|
58
|
-
* @param {string} dependent
|
|
59
|
-
*/
|
|
60
|
-
function createDynamicImage(imagePath, database, dependent, alt, itemprop) {
|
|
61
|
-
// TODO: Hardcode image height and width
|
|
62
|
-
const parsed = path.parse(imagePath);
|
|
63
|
-
const isURL = testURL(imagePath)
|
|
64
|
-
const isImg = !parsed.ext.search(/^.(png|jpeg|jpg)$/);
|
|
65
|
-
|
|
66
|
-
if (isURL) {
|
|
67
|
-
const parsedImageURL = unpic.parseUrl(imagePath)
|
|
68
|
-
if (parsedImageURL) {
|
|
69
|
-
|
|
70
|
-
const sources = [...imageExts, "jpeg"].map((format, index) => {
|
|
71
|
-
|
|
72
|
-
const isImg = index === imageExts.length
|
|
73
|
-
// TODO: Ignore images that have options already specified
|
|
74
|
-
const urls = imageSizes.map(size => {
|
|
75
|
-
return unpic.transformUrl({
|
|
76
|
-
format: format,
|
|
77
|
-
url: parsedImageURL.src,
|
|
78
|
-
provider: parsedImageURL.cdn,
|
|
79
|
-
width: size
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const sizes = urls.map((url, index) => `${url} ${imageSizes[index]}w`).join(", ")
|
|
84
|
-
|
|
85
|
-
return h(isImg ? "img" : "source", {
|
|
86
|
-
type: "image/" + format,
|
|
87
|
-
srcset: sizes,
|
|
88
|
-
src: isImg && urls.at(-1),
|
|
89
|
-
loading: isImg && "lazy",
|
|
90
|
-
sizes: "100vw",
|
|
91
|
-
alt: isImg && alt
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
return h("picture", { itemprop: itemprop && "image" }, sources)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!isImg) return
|
|
100
|
-
|
|
101
|
-
const relativePath = imagePath.startsWith("/") ? path.relative("/", imagePath) : imagePath
|
|
102
|
-
const image = database.target.getWithTrackers(relativePath, dependent)
|
|
103
|
-
if (!image) return
|
|
104
|
-
const formats = createImagePaths(image.abstract.sourcePath, "./", image.abstract.uuid)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const sources = formats.map((format, index) => {
|
|
108
|
-
const isImg = index === formats.length - 1
|
|
109
|
-
|
|
110
|
-
const sizes = format.map((size, index) => `/${size} ${imageSizes[index]}w`).join(", ")
|
|
111
|
-
const type = "image/" + path.extname(format[0]).slice(1)
|
|
112
|
-
return h(isImg ? "img" : "source", {
|
|
113
|
-
type,
|
|
114
|
-
srcset: sizes,
|
|
115
|
-
loading: isImg && "lazy",
|
|
116
|
-
src: isImg && "/" + format.at(-1),
|
|
117
|
-
sizes: "100vw",
|
|
118
|
-
alt: isImg && alt
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return h("picture", { itemprop: itemprop && "image" }, sources)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* @param {object} metadata
|
|
128
|
-
* @param {string} url
|
|
129
|
-
* @param {Votive.Database} database
|
|
130
|
-
* @param {Votive.VotiveConfig} config
|
|
131
|
-
*/
|
|
132
|
-
function makeHead(metadata, url, database, config) {
|
|
133
|
-
|
|
134
|
-
const treeMainHead = []
|
|
135
|
-
|
|
136
|
-
if (metadata.title) treeMainHead.push(
|
|
137
|
-
h("h1", metadata.title)
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
if (metadata.date) {
|
|
141
|
-
const date = new Date(metadata.date)
|
|
142
|
-
treeMainHead.push(
|
|
143
|
-
h("time",
|
|
144
|
-
{
|
|
145
|
-
datetime: date.toISOString(),
|
|
146
|
-
itemprop: "date"
|
|
147
|
-
},
|
|
148
|
-
date.toLocaleDateString("en-US", {
|
|
149
|
-
year: "numeric",
|
|
150
|
-
month: "long",
|
|
151
|
-
day: "numeric"
|
|
152
|
-
})
|
|
153
|
-
)
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (metadata.image || (metadata.first_image && url)) {
|
|
161
|
-
const metaImage = metadata.image || metadata.first_image
|
|
162
|
-
|
|
163
|
-
treeMainHead.push(
|
|
164
|
-
createDynamicImage(metaImage, database, url, null, true)
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (metadata.fm_description) treeMainHead.push(
|
|
169
|
-
h("p",
|
|
170
|
-
{
|
|
171
|
-
itemprop: "description"
|
|
172
|
-
},
|
|
173
|
-
metadata.fm_description
|
|
174
|
-
)
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
if (url) return h('a', { href: url }, treeMainHead)
|
|
178
|
-
|
|
179
|
-
return treeMainHead
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
function readURL(data) {
|
|
184
|
-
const hast = fromHtml(data)
|
|
185
|
-
const metadata = {}
|
|
186
|
-
|
|
187
|
-
// TODO: Remove to a separate processor
|
|
188
|
-
visit(hast, (node) => {
|
|
189
|
-
if (node.tagName === "meta") {
|
|
190
|
-
if (node.properties && node.properties.property) {
|
|
191
|
-
metadata[node.properties.property] = node.properties.content
|
|
192
|
-
}
|
|
193
|
-
} else if (node.tagName === "title") {
|
|
194
|
-
metadata.title = hastToString(node)
|
|
195
|
-
} else if (node.tagName === "link") {
|
|
196
|
-
if (node.properties?.rel?.includes("me")) {
|
|
197
|
-
metadata.me = node.properties.href
|
|
198
|
-
} else if (node.properties?.rel?.includes("webmention")) {
|
|
199
|
-
metadata.webmention = node.properties.href
|
|
200
|
-
} else if (node.properties?.rel?.includes("icon")) {
|
|
201
|
-
metadata.icon = node.properties.href
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
return metadata
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
31
|
/** @type {Votive.ReadText} */
|
|
211
|
-
function readFile(string, filePath,
|
|
32
|
+
function readFile(string, filePath, targetPath, api, config) {
|
|
33
|
+
const urls = []
|
|
34
|
+
|
|
212
35
|
const mdast = fromMarkdown(string, {
|
|
213
36
|
// Micromark extensions
|
|
214
37
|
extensions: [
|
|
215
38
|
frontmatter(),
|
|
216
39
|
gfmFootnote(),
|
|
217
40
|
gfmStrikethrough(),
|
|
218
|
-
gfmTable()
|
|
41
|
+
gfmTable(),
|
|
42
|
+
highlightMark()
|
|
219
43
|
],
|
|
220
44
|
mdastExtensions: [
|
|
221
45
|
frontmatterFromMarkdown(),
|
|
222
46
|
gfmFootnoteFromMarkdown(),
|
|
223
47
|
gfmStrikethroughFromMarkdown(),
|
|
224
|
-
gfmTableFromMarkdown()
|
|
48
|
+
gfmTableFromMarkdown(),
|
|
49
|
+
highlightMarkFromMarkdown
|
|
225
50
|
]
|
|
226
51
|
})
|
|
227
52
|
|
|
228
53
|
normalizeHeadings(mdast)
|
|
229
54
|
const pathInfo = path.parse(filePath)
|
|
230
55
|
|
|
231
|
-
const metadata = getMetadata(mdast, filePath)
|
|
232
|
-
|
|
233
|
-
metadata.inferred_label = toTitleCase(pathInfo.name)
|
|
234
|
-
|
|
235
|
-
if (destinationPath) {
|
|
236
|
-
const destinationInfo = path.parse(destinationPath)
|
|
237
|
-
const name = destinationInfo.name === "index" ? "" : destinationInfo.name
|
|
238
|
-
// FIXME the prettyURL should include the preceding slash
|
|
239
|
-
metadata.prettyURL = (new URL(`${destinationInfo.dir}/${name}`, "thismessage:/")).pathname
|
|
240
|
-
}
|
|
241
|
-
const jobs = []
|
|
242
|
-
|
|
243
|
-
selectMetadata(metadata)
|
|
56
|
+
const metadata = getMetadata(mdast, filePath, targetPath)
|
|
244
57
|
|
|
245
58
|
if (!metadata.image) {
|
|
246
59
|
const firstImageParagraph = mdast.children.find(child => child.children && child.children[0].type === "image")
|
|
@@ -249,17 +62,20 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
249
62
|
}
|
|
250
63
|
}
|
|
251
64
|
|
|
65
|
+
|
|
252
66
|
if (metadata.fm_published === false) return
|
|
253
67
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
68
|
+
const secretFileName = metadata.secret_key && hash("MD5", filePath + metadata.secret_key)
|
|
69
|
+
const secretFileInfo = secretFileName && router({ name: secretFileName, dir: pathInfo.dir.split(path.sep), ext: ".html" })
|
|
70
|
+
const secretFilePath = secretFileInfo && path.format({ name: secretFileInfo.name, dir: secretFileInfo.dir.join(path.sep), ext: secretFileInfo.ext })
|
|
71
|
+
if(secretFilePath) {
|
|
72
|
+
const secretPrettyFilePath = "/" + secretFilePath.slice(0, -5)
|
|
73
|
+
metadata.prettyURL = secretPrettyFilePath
|
|
74
|
+
metadata.local_menu_item ??= false
|
|
75
|
+
metadata.global_menu_item ??= false
|
|
76
|
+
metadata.sitemap_item ??= false
|
|
77
|
+
metadata.rss_item ??= false
|
|
78
|
+
console.info(`${styleText("dim", "build: ")}: secret file path for ${filePath}: ${secretPrettyFilePath}`)
|
|
263
79
|
}
|
|
264
80
|
|
|
265
81
|
|
|
@@ -269,10 +85,10 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
269
85
|
|
|
270
86
|
if (validURL) {
|
|
271
87
|
|
|
272
|
-
|
|
88
|
+
urls.push({
|
|
273
89
|
data: node.value,
|
|
274
90
|
runner: "text",
|
|
275
|
-
|
|
91
|
+
target: targetPath
|
|
276
92
|
})
|
|
277
93
|
|
|
278
94
|
return
|
|
@@ -325,25 +141,22 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
325
141
|
|
|
326
142
|
const markdown = `/tags/**`
|
|
327
143
|
const mdast = fromMarkdown(markdown)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
database.dependency.track({}, "tags", null, destinationPath, "tags.html")
|
|
346
|
-
database.target.markStale("tags.html")
|
|
144
|
+
const hast = toHast(mdast)
|
|
145
|
+
|
|
146
|
+
// target.create() is upsert-safe and this is a deterministic
|
|
147
|
+
// function of a fixed string - re-creating it once per
|
|
148
|
+
// hashtag-bearing file converges to a no-op after the first,
|
|
149
|
+
// without needing to check whether it already exists first.
|
|
150
|
+
api.createTarget({
|
|
151
|
+
path: `tags.html`,
|
|
152
|
+
abstract: hast,
|
|
153
|
+
metadata: {
|
|
154
|
+
breadcrumb: "Tags",
|
|
155
|
+
title: "Tags",
|
|
156
|
+
prettyURL: "/tags",
|
|
157
|
+
hastAbstract: hast,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
347
160
|
|
|
348
161
|
if (hashtags) {
|
|
349
162
|
if (!metadata.tags) {
|
|
@@ -365,10 +178,11 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
365
178
|
title: title,
|
|
366
179
|
prettyURL: `/tags/${hashtag}`,
|
|
367
180
|
type: "tag",
|
|
368
|
-
tag: hashtag
|
|
181
|
+
tag: hashtag,
|
|
182
|
+
hastAbstract: abstract,
|
|
369
183
|
}
|
|
370
184
|
|
|
371
|
-
const created =
|
|
185
|
+
const created = api.createTarget({
|
|
372
186
|
path: `tags/${hashtag}.html`,
|
|
373
187
|
abstract,
|
|
374
188
|
metadata: tagMetadata
|
|
@@ -383,7 +197,7 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
383
197
|
|
|
384
198
|
if (filePath === "settings.md") {
|
|
385
199
|
if (metadata.fm_domain) {
|
|
386
|
-
|
|
200
|
+
api.createTarget({
|
|
387
201
|
path: "sitemap.xml",
|
|
388
202
|
abstract: {},
|
|
389
203
|
metadata: {
|
|
@@ -392,7 +206,7 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
392
206
|
}
|
|
393
207
|
})
|
|
394
208
|
|
|
395
|
-
|
|
209
|
+
api.createTarget({
|
|
396
210
|
path: "feed.xml",
|
|
397
211
|
abstract: {},
|
|
398
212
|
metadata: {
|
|
@@ -404,154 +218,58 @@ function readFile(string, filePath, destinationPath, database, config) {
|
|
|
404
218
|
}
|
|
405
219
|
|
|
406
220
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
/** @param {ReturnType<getMetadata>} metadata */
|
|
411
|
-
function selectMetadata(metadata) {
|
|
412
|
-
const date =
|
|
413
|
-
metadata.fm_date
|
|
414
|
-
|| metadata.inferred_date
|
|
415
|
-
|
|
416
|
-
if (date) {
|
|
417
|
-
metadata.date = date
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const title =
|
|
421
|
-
metadata.fm_title
|
|
422
|
-
|| metadata.inferred_title
|
|
423
|
-
|| metadata.inferred_label
|
|
424
|
-
|
|
425
|
-
if (title) {
|
|
426
|
-
metadata.title = title
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const breadcrumb =
|
|
430
|
-
metadata.fm_breadcrumb
|
|
431
|
-
|| metadata.title
|
|
432
|
-
|| metadata.inferred_label
|
|
433
|
-
|
|
434
|
-
if (breadcrumb) {
|
|
435
|
-
metadata.breadcrumb = breadcrumb
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const description =
|
|
439
|
-
metadata.fm_description
|
|
440
|
-
|| metadata.tagline
|
|
441
|
-
|| metadata.inferred_description
|
|
442
|
-
|
|
443
|
-
if (description) {
|
|
444
|
-
metadata.description = description
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const image =
|
|
448
|
-
metadata.fm_image
|
|
449
|
-
|| metadata.inferred_image
|
|
221
|
+
const hast = toHast(mdast, {
|
|
222
|
+
unknownHandler: (_, n, p) => n.type === "highlight" && h("mark", n.children)
|
|
223
|
+
})
|
|
450
224
|
|
|
451
|
-
|
|
452
|
-
|
|
225
|
+
return {
|
|
226
|
+
abstract: hast,
|
|
227
|
+
filePath: secretFilePath,
|
|
228
|
+
write: metadata.html_file ?? true,
|
|
229
|
+
metadata: { ...metadata, hastAbstract: hast },
|
|
230
|
+
data: "hello",
|
|
231
|
+
settings: pathInfo.base === "settings.md" ? metadata : undefined
|
|
453
232
|
}
|
|
454
233
|
}
|
|
455
234
|
|
|
456
|
-
/**
|
|
457
|
-
* @param {string} text
|
|
458
|
-
*/
|
|
459
|
-
function truncateText(text) {
|
|
460
|
-
// const match = text.match(/^(?<desc>((\b.+?){28})\S)\s(?<etc>.+)$/)
|
|
461
|
-
// if (!match) {
|
|
462
|
-
return text
|
|
463
|
-
// } else if (match.groups.etc) {
|
|
464
|
-
// console.log("truncate 4")
|
|
465
|
-
// return match.groups.desc + "..."
|
|
466
|
-
// } else {
|
|
467
|
-
// console.log("truncate 5")
|
|
468
|
-
// return match.groups.desc
|
|
469
|
-
// }
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/** @param {object} tree */
|
|
473
|
-
function getMetadata(tree, filePath) {
|
|
474
|
-
const metadata = {}
|
|
475
|
-
|
|
476
|
-
for (let i = 0; i < tree.children.length; i++) {
|
|
477
|
-
const child = tree.children[i]
|
|
478
|
-
const text = mdastToString(child)
|
|
479
|
-
switch (child.type) {
|
|
480
|
-
case "paragraph":
|
|
481
|
-
if (child.children.length !== 1) {
|
|
482
|
-
if (!metadata.fm_description && !metadata.inferred_description) {
|
|
483
|
-
metadata.inferred_description = text
|
|
484
|
-
}
|
|
485
|
-
i = Infinity
|
|
486
|
-
break
|
|
487
|
-
} else if (child.children[0].type === "image") {
|
|
488
|
-
metadata.inferred_image = child.children[0].url
|
|
489
|
-
metadata.inferred_alt_text = child.children[0].alt
|
|
490
|
-
break
|
|
491
|
-
} else if (testURL(text)) {
|
|
492
|
-
const url = new URL(text)
|
|
493
|
-
if (text.match(/\.(jpeg|jpg|png)$/)) {
|
|
494
|
-
metadata.inferred_image = url
|
|
495
|
-
break
|
|
496
|
-
} else {
|
|
497
|
-
metadata.inferred_link = url
|
|
498
|
-
break
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
const inferred_date = extractDate(mdastToString(child))
|
|
502
|
-
if (inferred_date) {
|
|
503
|
-
metadata.inferred_date = inferred_date
|
|
504
|
-
} else {
|
|
505
|
-
if (!metadata.fm_description && !metadata.inferred_description) {
|
|
506
|
-
metadata.inferred_description = text
|
|
507
|
-
}
|
|
508
|
-
// tree.children.splice(0, i + 1)
|
|
509
|
-
i = Infinity
|
|
510
|
-
}
|
|
511
|
-
break
|
|
512
|
-
}
|
|
513
|
-
case "heading":
|
|
514
|
-
if (child.depth === 1) {
|
|
515
|
-
metadata.inferred_title = mdastToString(child) || toTitleCase(path.parse(filePath).name)
|
|
516
|
-
tree.children.splice(0, i + 1)
|
|
517
|
-
} else {
|
|
518
|
-
i = Infinity;
|
|
519
|
-
}
|
|
520
|
-
break
|
|
521
|
-
case "yaml":
|
|
522
|
-
const frontmatter = yaml.parse(child.value)
|
|
523
|
-
for (const key in frontmatter) {
|
|
524
|
-
metadata["fm_" + key] = frontmatter[key]
|
|
525
|
-
}
|
|
526
|
-
break
|
|
527
|
-
default:
|
|
528
|
-
i = Infinity
|
|
529
|
-
break
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return metadata
|
|
534
235
|
|
|
535
|
-
}
|
|
536
236
|
|
|
537
237
|
/** @type {Votive.ReadAbstract} */
|
|
538
|
-
function transformFile(abstract,
|
|
539
|
-
const
|
|
540
|
-
return { abstract,
|
|
238
|
+
function transformFile(abstract, settings, api, config) {
|
|
239
|
+
const urls = []
|
|
240
|
+
return { abstract, urls }
|
|
541
241
|
}
|
|
542
242
|
|
|
543
243
|
/** @type {Votive.ReadFolder} */
|
|
544
|
-
function readFolder(folder,
|
|
244
|
+
function readFolder(folder, settings, api, config, isRoot) {
|
|
545
245
|
if (folder === "") {
|
|
546
|
-
|
|
246
|
+
api.createTarget({
|
|
547
247
|
path: "robots.txt",
|
|
548
248
|
abstract: {
|
|
549
|
-
content:
|
|
249
|
+
content: generateRobots()
|
|
550
250
|
},
|
|
551
251
|
metadata: {}
|
|
552
252
|
})
|
|
553
253
|
}
|
|
554
|
-
|
|
254
|
+
|
|
255
|
+
const pageNotFound = api.target("404.html")
|
|
256
|
+
|
|
257
|
+
if(!pageNotFound) {
|
|
258
|
+
const abstract = toHast(fromMarkdown(`# 404\n\nPage not found.`))
|
|
259
|
+
api.createTarget({
|
|
260
|
+
abstract,
|
|
261
|
+
metadata: {
|
|
262
|
+
title: "Page not found",
|
|
263
|
+
breadcrumb: "404",
|
|
264
|
+
prettyURL: "404.html",
|
|
265
|
+
hastAbstract: abstract,
|
|
266
|
+
},
|
|
267
|
+
path: "404.html",
|
|
268
|
+
extension: ".html"
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const newSettings = {}
|
|
555
273
|
|
|
556
274
|
const folderInfo = path.parse(folder)
|
|
557
275
|
|
|
@@ -568,8 +286,8 @@ function readFolder(folder, database, config, isRoot) {
|
|
|
568
286
|
})
|
|
569
287
|
|
|
570
288
|
|
|
571
|
-
const aliasFile =
|
|
572
|
-
const indexFile =
|
|
289
|
+
const aliasFile = api.target(aliasPath)
|
|
290
|
+
const indexFile = api.target(indexPath)
|
|
573
291
|
|
|
574
292
|
if (!isRoot) {
|
|
575
293
|
if (!aliasFile) {
|
|
@@ -583,15 +301,16 @@ function readFolder(folder, database, config, isRoot) {
|
|
|
583
301
|
|
|
584
302
|
const indexPath = prettyURL + "/*"
|
|
585
303
|
|
|
586
|
-
const abstract = fromMarkdown(`# ${title}\n\n${indexPath}`)
|
|
587
|
-
|
|
304
|
+
const abstract = toHast(fromMarkdown(`# ${title}\n\n${indexPath}`))
|
|
305
|
+
api.createTarget({
|
|
588
306
|
abstract,
|
|
589
307
|
path: aliasPath,
|
|
590
|
-
|
|
308
|
+
extension: ".html",
|
|
591
309
|
metadata: {
|
|
592
310
|
title: toTitleCase(folderInfo.name),
|
|
593
311
|
breadcrumb: toTitleCase(folderInfo.name),
|
|
594
|
-
prettyURL
|
|
312
|
+
prettyURL,
|
|
313
|
+
hastAbstract: abstract,
|
|
595
314
|
}
|
|
596
315
|
})
|
|
597
316
|
}
|
|
@@ -602,135 +321,91 @@ function readFolder(folder, database, config, isRoot) {
|
|
|
602
321
|
const indexPath = prettyURL + "/*"
|
|
603
322
|
|
|
604
323
|
const abstract = fromMarkdown(`# ${title}\n\n${indexPath}`)
|
|
605
|
-
|
|
324
|
+
api.createTarget({
|
|
606
325
|
abstract,
|
|
607
326
|
path: "index.html",
|
|
608
|
-
|
|
327
|
+
extension: "html",
|
|
609
328
|
metadata: {
|
|
610
329
|
title,
|
|
611
330
|
breadcrumb: title,
|
|
612
|
-
prettyURL: "/"
|
|
331
|
+
prettyURL: "/",
|
|
332
|
+
hastAbstract: abstract,
|
|
613
333
|
}
|
|
614
334
|
})
|
|
615
335
|
}
|
|
616
336
|
}
|
|
617
337
|
|
|
618
|
-
// if (!indexFile && (!isRoot && !aliasFile)) {
|
|
619
|
-
// database.target.create({
|
|
620
|
-
// metadata: {
|
|
621
|
-
// title: toTitleCase(folderInfo.name),
|
|
622
|
-
// breadcrumb: toTitleCase(folderInfo.name),
|
|
623
|
-
// prettyURL: "/" + path.normalize(path.format({
|
|
624
|
-
// dir: path.join(folderInfo.dir),
|
|
625
|
-
// name: folderInfo.name
|
|
626
|
-
// }))
|
|
627
|
-
// },
|
|
628
|
-
// path: aliasPath,
|
|
629
|
-
// abstract: {},
|
|
630
|
-
// syntax: "html"
|
|
631
|
-
// })
|
|
632
|
-
// }
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
338
|
if (isRoot) {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
setTheme(settings)
|
|
641
|
-
|
|
642
|
-
function setTheme(settings) {
|
|
643
|
-
const themes = [
|
|
644
|
-
"reset",
|
|
645
|
-
"typography",
|
|
646
|
-
"default"
|
|
647
|
-
]
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (themes.includes(settings.fm_theme?.[""][0]) || !settings.fm_theme) {
|
|
651
|
-
if (!settings.fm_theme) database.setting.create("", "theme", "default")
|
|
339
|
+
const themes = ["reset", "typography", "default"]
|
|
340
|
+
const existingTheme = settings.fm_theme?.[0]?.at(-1)
|
|
652
341
|
|
|
653
|
-
|
|
342
|
+
if (!existingTheme || themes.includes(existingTheme)) {
|
|
343
|
+
if (!existingTheme) newSettings.theme = "default"
|
|
654
344
|
|
|
655
|
-
|
|
345
|
+
const theme = existingTheme || "default"
|
|
656
346
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
"stylesheets",
|
|
660
|
-
"reset.css"
|
|
661
|
-
)
|
|
347
|
+
if (themes.includes(theme)) {
|
|
348
|
+
newSettings.stylesheets = ["reset.css"]
|
|
662
349
|
|
|
663
|
-
|
|
664
|
-
|
|
350
|
+
const resetStylesPath = path.join(VOWEL_DIR, "stylesheets", "ResetStyles.css")
|
|
351
|
+
const resetStyles = readFileSync(resetStylesPath, "utf-8")
|
|
665
352
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (theme === "reset") return
|
|
353
|
+
api.createTarget({
|
|
354
|
+
path: "reset.css",
|
|
355
|
+
abstract: { css: resetStyles },
|
|
356
|
+
metadata: {},
|
|
357
|
+
extension: "css"
|
|
358
|
+
})
|
|
674
359
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
"stylesheets",
|
|
678
|
-
"typography.css"
|
|
679
|
-
)
|
|
360
|
+
if (theme !== "reset") {
|
|
361
|
+
newSettings.stylesheets.push("typography.css")
|
|
680
362
|
|
|
681
363
|
const typeStylesPath = path.join(VOWEL_DIR, "stylesheets", "TypographyStyles.css")
|
|
682
364
|
const typeStyles = readFileSync(typeStylesPath, "utf-8")
|
|
683
365
|
|
|
684
|
-
|
|
366
|
+
api.createTarget({
|
|
685
367
|
path: "typography.css",
|
|
686
368
|
abstract: { css: typeStyles },
|
|
687
369
|
metadata: {},
|
|
688
|
-
|
|
370
|
+
extension: "css"
|
|
689
371
|
})
|
|
690
372
|
|
|
691
|
-
if (theme
|
|
373
|
+
if (theme !== "typography") {
|
|
374
|
+
newSettings.stylesheets.push("default.css")
|
|
692
375
|
|
|
693
|
-
|
|
694
|
-
""
|
|
695
|
-
"stylesheets",
|
|
696
|
-
"default.css"
|
|
697
|
-
)
|
|
376
|
+
const defaultStylesPath = path.join(VOWEL_DIR, "stylesheets", "DefaultStyles.css")
|
|
377
|
+
const defaultStyles = readFileSync(defaultStylesPath, "utf-8")
|
|
698
378
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
syntax: "css"
|
|
707
|
-
})
|
|
379
|
+
api.createTarget({
|
|
380
|
+
path: "default.css",
|
|
381
|
+
abstract: { css: defaultStyles },
|
|
382
|
+
metadata: {},
|
|
383
|
+
extension: "css"
|
|
384
|
+
})
|
|
385
|
+
}
|
|
708
386
|
}
|
|
709
387
|
}
|
|
710
388
|
}
|
|
711
389
|
|
|
712
|
-
const site_title = settings.fm_title
|
|
713
|
-
|| settings.inferred_title
|
|
390
|
+
const site_title = settings.fm_title?.[0]?.at(-1)
|
|
391
|
+
|| settings.inferred_title?.[0]?.at(-1)
|
|
714
392
|
|| (indexFile && indexFile.metadata.title)
|
|
715
393
|
|
|
716
|
-
if (site_title && !settings.title) {
|
|
717
|
-
|
|
394
|
+
if (site_title && !settings.title?.[0]?.length) {
|
|
395
|
+
newSettings.title = site_title
|
|
718
396
|
}
|
|
719
397
|
|
|
720
|
-
const tagline = settings.fm_tagline
|
|
721
|
-
|
|
722
|
-
|| settings.inferred_description
|
|
723
|
-
&& settings.inferred_description[0]
|
|
398
|
+
const tagline = settings.fm_tagline?.[0]?.at(-1)
|
|
399
|
+
|| settings.inferred_description?.[0]?.at(-1)
|
|
724
400
|
|
|
725
401
|
if (tagline) {
|
|
726
|
-
|
|
402
|
+
newSettings.tagline = tagline
|
|
727
403
|
}
|
|
728
404
|
|
|
729
|
-
const icon = settings.fm_icon
|
|
730
|
-
&& settings.fm_icon[0]
|
|
405
|
+
const icon = settings.fm_icon?.[0]?.at(-1)
|
|
731
406
|
|
|
732
407
|
if (icon) {
|
|
733
|
-
|
|
408
|
+
newSettings.icon = icon
|
|
734
409
|
}
|
|
735
410
|
}
|
|
736
411
|
|
|
@@ -740,610 +415,12 @@ function readFolder(folder, database, config, isRoot) {
|
|
|
740
415
|
|| toTitleCase(folder.split(path.sep).at(-1))
|
|
741
416
|
|| "Home"
|
|
742
417
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
database.setting.create(folder, "breadcrumbs", breadcrumb)
|
|
746
|
-
|
|
747
|
-
return {
|
|
748
|
-
jobs: [],
|
|
749
|
-
destinations: []
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/** @type {Votive.ProcessorWrite} */
|
|
754
|
-
function writeFile(destination, database, config) {
|
|
755
|
-
const isRoot = destination.path === "index.html"
|
|
756
|
-
|
|
757
|
-
// console.log(destination.metadata)
|
|
758
|
-
|
|
759
|
-
if (destination.metadata.type === "tag") {
|
|
760
|
-
if (!destination.metadata.tag) return false
|
|
761
|
-
|
|
762
|
-
const pages = database.target.getManyWithTrackers({
|
|
763
|
-
recursive: true,
|
|
764
|
-
dependent: destination,
|
|
765
|
-
query: {
|
|
766
|
-
tags: destination.metadata.tag
|
|
767
|
-
}
|
|
768
|
-
})
|
|
769
|
-
|
|
770
|
-
if (!pages.length) return false
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const settings = database.setting.getByFolder(destination.dir + path.sep)
|
|
774
|
-
|
|
775
|
-
const { abstract, metadata, ...rest } = destination
|
|
776
|
-
|
|
777
|
-
if (metadata.tags) metadata.tags = JSON.parse(metadata.tags)
|
|
778
|
-
|
|
779
|
-
/** @param {string} filePath */
|
|
780
|
-
function listFolders(filePath) {
|
|
781
|
-
if (!filePath) return []
|
|
782
|
-
|
|
783
|
-
const pathInfo = path.parse(filePath)
|
|
784
|
-
const dir = pathInfo.dir && pathInfo.dir
|
|
785
|
-
return [...listFolders(
|
|
786
|
-
dir
|
|
787
|
-
), filePath]
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const parsedPath = path.parse("" + destination.path)
|
|
791
|
-
|
|
792
|
-
const destinationAsDir = path.relative("", path.format({
|
|
793
|
-
dir: parsedPath.dir,
|
|
794
|
-
name: parsedPath.name
|
|
795
|
-
}))
|
|
796
|
-
|
|
797
|
-
const ancestorFolders = listFolders(rest.dir)
|
|
798
|
-
ancestorFolders.unshift("")
|
|
799
|
-
|
|
800
|
-
const family = [...ancestorFolders, destinationAsDir].flatMap(folder => {
|
|
801
|
-
// FIXME typing
|
|
802
|
-
return database.target.getManyWithTrackers({
|
|
803
|
-
folder: Array.isArray(folder) ? path.join(...folder) : folder,
|
|
804
|
-
recursive: false,
|
|
805
|
-
dependent: destination.path,
|
|
806
|
-
query: {}
|
|
807
|
-
})
|
|
808
|
-
}).filter(({ path, dir }) => {
|
|
809
|
-
return path && path !== "tags.html" && dir !== "tags"
|
|
810
|
-
})
|
|
811
|
-
|
|
812
|
-
const treeStyleSheets = []
|
|
813
|
-
|
|
814
|
-
Object.values(settings.stylesheets).forEach(file => {
|
|
815
|
-
file.forEach(sheet => {
|
|
816
|
-
const cacheBuster = Math.random().toString(36).slice(2, 10);
|
|
817
|
-
treeStyleSheets.push(
|
|
818
|
-
h('link', {
|
|
819
|
-
rel: "stylesheet",
|
|
820
|
-
href: `/${sheet}?${cacheBuster}`
|
|
821
|
-
})
|
|
822
|
-
)
|
|
823
|
-
})
|
|
824
|
-
})
|
|
825
|
-
|
|
826
|
-
function createTitle() {
|
|
827
|
-
if (isRoot) {
|
|
828
|
-
const title = [settings?.title?.[0] || metadata?.title, settings?.fm_tagline?.[""]?.[0]]
|
|
829
|
-
.filter(a => a)
|
|
830
|
-
.join(" - ")
|
|
831
|
-
|
|
832
|
-
return title
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// FIXME Check that this works properly
|
|
836
|
-
if (metadata.title && settings.title) {
|
|
837
|
-
const titles = [metadata.title, ...Object.values(settings.title).flatMap(a => a).reverse()]
|
|
838
|
-
return titles.join(" - ")
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
if (metadata.title || settings.title) {
|
|
842
|
-
return metadata.title || settings.title.reverse()
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
return "Website"
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const title = createTitle()
|
|
850
|
-
|
|
851
|
-
const treeHead = h('head', [
|
|
852
|
-
h('meta', {
|
|
853
|
-
charset: "UTF-8",
|
|
854
|
-
}),
|
|
855
|
-
h("meta", {
|
|
856
|
-
name: "viewport",
|
|
857
|
-
content: "width=device-width, initial-scale=1.0"
|
|
858
|
-
}),
|
|
859
|
-
h("meta", {
|
|
860
|
-
"http-equiv": "X-UA-Compatible",
|
|
861
|
-
content: "ie-edge"
|
|
862
|
-
}),
|
|
863
|
-
h('title', title),
|
|
864
|
-
h('meta', {
|
|
865
|
-
property: "og:title",
|
|
866
|
-
content: title
|
|
867
|
-
}),
|
|
868
|
-
h('meta', {
|
|
869
|
-
property: "og:description",
|
|
870
|
-
content: metadata.description,
|
|
871
|
-
}),
|
|
872
|
-
...treeStyleSheets,
|
|
873
|
-
])
|
|
874
|
-
|
|
875
|
-
if (settings.fm_domain) {
|
|
876
|
-
const settingsDomain = settings.fm_domain[""][0]
|
|
877
|
-
const domain = settingsDomain.startsWith("http")
|
|
878
|
-
? settingsDomain
|
|
879
|
-
: "https://" + settingsDomain
|
|
880
|
-
|
|
881
|
-
const { href } = new URL(metadata.prettyURL, domain)
|
|
882
|
-
|
|
883
|
-
treeHead.children.push(
|
|
884
|
-
h("meta", {
|
|
885
|
-
property: "og:url",
|
|
886
|
-
content: href
|
|
887
|
-
}),
|
|
888
|
-
h("link", {
|
|
889
|
-
rel: "canonical",
|
|
890
|
-
href
|
|
891
|
-
})
|
|
892
|
-
)
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (metadata.image) {
|
|
897
|
-
treeHead.children.push(h("meta", {
|
|
898
|
-
property: "og:image",
|
|
899
|
-
content: metadata.image
|
|
900
|
-
}))
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/* FIXME this could be a section title */
|
|
904
|
-
if (settings.title) {
|
|
905
|
-
treeHead.children.push(h("meta", {
|
|
906
|
-
property: "og:site_name",
|
|
907
|
-
content: settings.fm_title?.[""][0]
|
|
908
|
-
}))
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/* FIXME Properly handle this image */
|
|
912
|
-
if (settings.icon) {
|
|
913
|
-
treeHead.children.push(h("link", {
|
|
914
|
-
href: "/" + settings.icon[0],
|
|
915
|
-
rel: "icon",
|
|
916
|
-
type: "image/png"
|
|
917
|
-
}))
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function treeNavItems(navItem) {
|
|
921
|
-
return h('li', h('a', {
|
|
922
|
-
href: navItem.metadata.prettyURL,
|
|
923
|
-
"aria-current": metadata.prettyURL === navItem.metadata.prettyURL ? 'page' : null
|
|
924
|
-
}, navItem.metadata.breadcrumb))
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function navItemFilter(nav_item) {
|
|
928
|
-
return !nav_item.metadata.date
|
|
929
|
-
&& nav_item.path !== "index.html"
|
|
930
|
-
&& nav_item.path !== "404.html"
|
|
931
|
-
&& nav_item.syntax === ".html"
|
|
932
|
-
&& nav_item.path
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function sort_items(a, b) {
|
|
936
|
-
if (typeof a === "number" && typeof b === "number") return a - b
|
|
937
|
-
if (typeof b === "number") return -1
|
|
938
|
-
if (typeof a === "number") return 1
|
|
939
|
-
if (a.metadata.breadcrumb && b.metadata.breadcrumb) return String(a.metadata.breadcrumb).localeCompare(String(b.metadata.breadcrumb))
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
function treeNavFolder(navFolder) {
|
|
945
|
-
const sorted = navFolder
|
|
946
|
-
.filter(navItemFilter)
|
|
947
|
-
.toSorted(sort_items)
|
|
948
|
-
|
|
949
|
-
return h('ul', sorted.map(treeNavItems))
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const groupedNavs = Object.groupBy(family, ({ dir }) => dir)
|
|
954
|
-
|
|
955
|
-
const treeNav = h('nav', Object.entries(groupedNavs)
|
|
956
|
-
.sort(([a], [b]) => a.length - b.length)
|
|
957
|
-
.map(([k, v]) => treeNavFolder(v))
|
|
958
|
-
.filter(folder => folder.children.length)
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
let treeBreadcrumbs = []
|
|
962
|
-
|
|
963
|
-
const breadcrumbs = Object.entries(settings.breadcrumbs)
|
|
964
|
-
.sort(([a], [b]) => a.length - b.length)
|
|
965
|
-
.map(([path, [label]]) => ["/" + path, label])
|
|
966
|
-
|
|
967
|
-
treeBreadcrumbs.push(
|
|
968
|
-
...breadcrumbs.map(([path, label]) => {
|
|
969
|
-
return h('a', {
|
|
970
|
-
href: path
|
|
971
|
-
}, label)
|
|
972
|
-
})
|
|
973
|
-
)
|
|
974
|
-
|
|
975
|
-
if (!isRoot) {
|
|
976
|
-
treeBreadcrumbs.push(
|
|
977
|
-
h('a', {
|
|
978
|
-
href: destination.metadata.prettyURL,
|
|
979
|
-
'aria-current': 'page'
|
|
980
|
-
}, metadata.breadcrumb)
|
|
981
|
-
)
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
const headerElements = []
|
|
985
|
-
|
|
986
|
-
const homeLink = []
|
|
987
|
-
|
|
988
|
-
// TODO better color handling https://antfu.me/posts/icons-in-pure-css
|
|
989
|
-
if (settings.fm_logo && settings.fm_logo[""]) {
|
|
990
|
-
headerElements.push(
|
|
991
|
-
h('a#logo', {
|
|
992
|
-
href: "/",
|
|
993
|
-
"aria-label": "logo",
|
|
994
|
-
rel: "home",
|
|
995
|
-
"style": `--logo-url: url("/${settings.fm_logo[""]}")`
|
|
996
|
-
}, h("img", {
|
|
997
|
-
src: "/" + settings.fm_logo[""],
|
|
998
|
-
alt: ""
|
|
999
|
-
}))
|
|
1000
|
-
)
|
|
1001
|
-
/*
|
|
1002
|
-
if (settings.fm_logo[""][0].endsWith(".svg")) {
|
|
1003
|
-
const [svg] = settings.fm_logo[""]
|
|
1004
|
-
const svgTarget = database.target.getWithTrackers(svg, destination.path)
|
|
1005
|
-
if (svgTarget.metadata.monochrome) {
|
|
1006
|
-
// FIXME aspect ratio
|
|
1007
|
-
headerElements.push(
|
|
1008
|
-
h('a.logo', {
|
|
1009
|
-
style: `background-color: currentColor; --logo-url: url('${(new URL(settings.fm_logo[""], "thismessage://")).pathname}'); mask-size: 100% 100%;`,
|
|
1010
|
-
href: "/",
|
|
1011
|
-
rel: "home",
|
|
1012
|
-
alt: ""
|
|
1013
|
-
})
|
|
1014
|
-
)
|
|
1015
|
-
} else {
|
|
1016
|
-
headerElements.push(
|
|
1017
|
-
h('a.logo', {
|
|
1018
|
-
style: `background: url(${(new URL(settings.fm_logo[""], "thismessage://")).pathname}) norepeat center; background-color: transparent; background-size: 100% 100%;`,
|
|
1019
|
-
href: "/",
|
|
1020
|
-
rel: "home",
|
|
1021
|
-
alt: ""
|
|
1022
|
-
})
|
|
1023
|
-
)
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
*/
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (settings.fm_wordmark && settings.fm_wordmark[""]) {
|
|
1030
|
-
headerElements.push(h("a#wordmark", {
|
|
1031
|
-
href: "/",
|
|
1032
|
-
rel: "home"
|
|
1033
|
-
}, h("img", {
|
|
1034
|
-
src: "/" + settings.fm_wordmark[""]
|
|
1035
|
-
})))
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
if (settings.title && settings.title[""]) {
|
|
1039
|
-
headerElements.push(h('a#title', { href: "/", rel: "home" }, settings.title[""]))
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
if (settings.fm_tagline && settings.fm_tagline[""]) {
|
|
1043
|
-
headerElements.push(h('p#tagline', settings.fm_tagline[""][0]))
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const treeHeader = h('header', [
|
|
1047
|
-
...headerElements,
|
|
1048
|
-
treeNav
|
|
1049
|
-
])
|
|
1050
|
-
|
|
1051
|
-
const treeContent = toHast(abstract)
|
|
1052
|
-
|
|
1053
|
-
function testPaths(node, i, p) {
|
|
1054
|
-
if (node.type !== 'element') return
|
|
1055
|
-
if (node.tagName !== 'p') return
|
|
1056
|
-
if (node.children.length !== 1) return
|
|
1057
|
-
if (!node.children[0].value) return
|
|
1058
|
-
return Boolean(node.children[0].value.match(/^\/\S*$/))
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
const slugger = unified()
|
|
1062
|
-
.use(slug)
|
|
1063
|
-
.use(toc, {
|
|
1064
|
-
customizeTOC: (toc) => {
|
|
1065
|
-
toc.properties = {
|
|
1066
|
-
"aria-label": "Contents"
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
})
|
|
1070
|
-
|
|
1071
|
-
const treeContentSlugged = slugger.runSync(treeContent)
|
|
1072
|
-
|
|
1073
|
-
const treeTableOfContents = treeContentSlugged.children.shift()
|
|
1074
|
-
|
|
1075
|
-
const treeMainHead = makeHead(metadata, null, database, config)
|
|
1076
|
-
|
|
1077
|
-
treeMainHead.push(treeTableOfContents)
|
|
1078
|
-
|
|
1079
|
-
visit(treeContent, { tagName: "img" }, (node, index, parent) => {
|
|
1080
|
-
const { src, alt } = node.properties
|
|
1081
|
-
const image = createDynamicImage(src, database, destination.path.path, alt)
|
|
1082
|
-
if (index === 0 && parent.children.length > 1) {
|
|
1083
|
-
console.log(parent.children)
|
|
1084
|
-
const [_, ...caption] = parent.children
|
|
1085
|
-
parent.children = [
|
|
1086
|
-
h("figure", [image, h("figcaption", caption)])
|
|
1087
|
-
]
|
|
1088
|
-
} else {
|
|
1089
|
-
parent.children.splice(index, 1, image)
|
|
1090
|
-
}
|
|
1091
|
-
})
|
|
1092
|
-
|
|
1093
|
-
visit(treeContent, testPaths, ({ children: [child] }, i, p) => {
|
|
1094
|
-
|
|
1095
|
-
// const recursive = child.value.endsWith("**")
|
|
1096
|
-
// const many = child.value.endsWith("*")
|
|
1097
|
-
|
|
1098
|
-
const url = new URL(child.value, "thismessage://")
|
|
1099
|
-
const { dir, base } = path.parse(url.pathname)
|
|
1100
|
-
const recursive = base === "**"
|
|
1101
|
-
const many = base === "*" || base === "**"
|
|
1102
|
-
|
|
1103
|
-
if (!many) {
|
|
1104
|
-
const targetFilePathInfo = path.parse(child.value)
|
|
1105
|
-
targetFilePathInfo.ext ||= ".html"
|
|
1106
|
-
delete targetFilePathInfo.base
|
|
1107
|
-
const targetFilePath = path.relative("/", path.format(targetFilePathInfo))
|
|
1108
|
-
const target = database.target.getWithTrackers(targetFilePath, destination.path)
|
|
1109
|
-
|
|
1110
|
-
if (target) {
|
|
1111
|
-
const article = h('article', makeHead(target.metadata, target.metadata.prettyURL, database, config))
|
|
1112
|
-
|
|
1113
|
-
p.children.splice(i, 1, article)
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
if (many) {
|
|
1119
|
-
const folder = path.relative("/", dir)
|
|
1120
|
-
// const url = new URL(child.value, "thismessage://")
|
|
1121
|
-
const count = url.searchParams.get("count")
|
|
1122
|
-
const tag = url.searchParams.get("tag")
|
|
1123
|
-
const query = tag
|
|
1124
|
-
? { tags: tag }
|
|
1125
|
-
: {}
|
|
1126
|
-
|
|
1127
|
-
const targets = database.target.getManyWithTrackers({
|
|
1128
|
-
folder,
|
|
1129
|
-
recursive,
|
|
1130
|
-
query,
|
|
1131
|
-
dependent: destination.path
|
|
1132
|
-
})
|
|
1133
|
-
|
|
1134
|
-
if (count) targets.splice(Number(count))
|
|
1135
|
-
|
|
1136
|
-
const escapedDir = folder.replace("_", "--").replace(path.sep, "_")
|
|
1137
|
-
const escapedDirs = escapedDir.split("_").filter(a => a).map((segment, index, array) => {
|
|
1138
|
-
return "_" + array.slice(0, index + 1).join("_")
|
|
1139
|
-
})
|
|
1140
|
-
escapedDirs.unshift("_")
|
|
1141
|
-
const dirClasses = escapedDirs.join(".")
|
|
1142
|
-
|
|
1143
|
-
const list = h(`ul.${dirClasses}`,
|
|
1144
|
-
targets.map(target => {
|
|
1145
|
-
return h('li',
|
|
1146
|
-
h('article', makeHead(target.metadata, target.metadata.prettyURL, database, config))
|
|
1147
|
-
)
|
|
1148
|
-
})
|
|
1149
|
-
)
|
|
1150
|
-
|
|
1151
|
-
p.children.splice(i, 1, list)
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
})
|
|
1155
|
-
|
|
1156
|
-
remove(treeContent, (n, i, p) => p.type === "root" && n.tagName === "h1")
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
// function copyTreeWithoutArticles(tree) {
|
|
1160
|
-
// if (tree.tagName !== 'article') {
|
|
1161
|
-
// return {
|
|
1162
|
-
// type: tree.type,
|
|
1163
|
-
// tagName: tree.tagName,
|
|
1164
|
-
// properties: tree.properties,
|
|
1165
|
-
// children: tree.children?.map(copyTreeWithoutArticles)
|
|
1166
|
-
// }
|
|
1167
|
-
// }
|
|
1168
|
-
// }
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const treeMain = h('main',
|
|
1174
|
-
{
|
|
1175
|
-
itemscope: true
|
|
1176
|
-
},
|
|
1177
|
-
[
|
|
1178
|
-
h('nav', {
|
|
1179
|
-
'aria-label': 'Breadcrumbs'
|
|
1180
|
-
}, treeBreadcrumbs),
|
|
1181
|
-
treeMainHead,
|
|
1182
|
-
h('section#content', treeContent)
|
|
1183
|
-
])
|
|
1184
|
-
|
|
1185
|
-
visit(treeMain, (node, index, parent) => {
|
|
1186
|
-
/* URLs */ if (node.type === "text" && parent.tagName === 'p' && parent.children.length === 1) {
|
|
1187
|
-
const validURL = testURL(node.value)
|
|
1188
|
-
if (validURL) {
|
|
1189
|
-
const metadata = database.url.get(node.value)
|
|
1190
|
-
if (metadata) {
|
|
1191
|
-
parent.tagName = "article"
|
|
1192
|
-
parent.children = [
|
|
1193
|
-
h("a", { href: node.value },
|
|
1194
|
-
h("h2", metadata.title)
|
|
1195
|
-
)
|
|
1196
|
-
]
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
} /* GFM Alerts */ else if (node.tagName === "blockquote") {
|
|
1200
|
-
if (node.children[1]
|
|
1201
|
-
&& node.children[1].tagName === "p"
|
|
1202
|
-
&& node.children[1].children.length === 1
|
|
1203
|
-
&& node.children[1].children[0].type === "text"
|
|
1204
|
-
) {
|
|
1205
|
-
const matches = node.children[1].children[0].value.match(/^\[!(\w+)\]$/)
|
|
1206
|
-
|
|
1207
|
-
if (matches) {
|
|
1208
|
-
const [_, alertLabel] = matches
|
|
1209
|
-
|
|
1210
|
-
node.tagName = "aside"
|
|
1211
|
-
node.properties = {
|
|
1212
|
-
class: `alert ${alertLabel.toLowerCase()}`
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
node.children.splice(0, 2, {
|
|
1216
|
-
type: "element",
|
|
1217
|
-
tagName: "h2",
|
|
1218
|
-
children: [
|
|
1219
|
-
{
|
|
1220
|
-
value: toTitleCase(alertLabel),
|
|
1221
|
-
type: "text"
|
|
1222
|
-
}
|
|
1223
|
-
]
|
|
1224
|
-
})
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
})
|
|
1230
|
-
|
|
1231
|
-
const everything = database.target.getManyWithTrackers({
|
|
1232
|
-
folder: "",
|
|
1233
|
-
recursive: true,
|
|
1234
|
-
dependent: destination.path,
|
|
1235
|
-
}).filter(target => target.path
|
|
1236
|
-
&& target.path.endsWith(".html")
|
|
1237
|
-
&& !target.metadata.date
|
|
1238
|
-
)
|
|
1239
|
-
|
|
1240
|
-
const homeFile = everything.find(item => item.path === "index.html" && item.dir === "")
|
|
1241
|
-
|
|
1242
|
-
const globalNavItems = everything.filter(item => {
|
|
1243
|
-
return item.dir === ""
|
|
1244
|
-
&& item.path !== "index.html"
|
|
1245
|
-
&& item.path !== "404.html"
|
|
1246
|
-
&& item.path !== "tags.html"
|
|
1247
|
-
})
|
|
1248
|
-
.map(getChildren)
|
|
1249
|
-
|
|
1250
|
-
globalNavItems.unshift(homeFile)
|
|
1251
|
-
|
|
1252
|
-
function getChildren(item) {
|
|
1253
|
-
const children = everything.filter(child => {
|
|
1254
|
-
return "/" + child.dir === item.metadata.prettyURL
|
|
1255
|
-
&& child.path !== "index.html"
|
|
1256
|
-
})
|
|
1257
|
-
|
|
1258
|
-
const populatedChildren = children.length > 0 && children.map(child => {
|
|
1259
|
-
return getChildren(child)
|
|
1260
|
-
})
|
|
1261
|
-
|
|
1262
|
-
const node = {
|
|
1263
|
-
path: "/" + item.path,
|
|
1264
|
-
metadata: item.metadata
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (populatedChildren) node.children = populatedChildren
|
|
1268
|
-
|
|
1269
|
-
return node
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
function treeNavItem(item) {
|
|
1273
|
-
if (item.children) {
|
|
1274
|
-
return h('li', [
|
|
1275
|
-
h('a', { href: item.path }, item.metadata.breadcrumb),
|
|
1276
|
-
treeNavList(item.children)
|
|
1277
|
-
])
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
return h('li',
|
|
1281
|
-
h('a', { href: item.path }, item.metadata.breadcrumb)
|
|
1282
|
-
)
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
function treeNavList(items) {
|
|
1286
|
-
return h('ul',
|
|
1287
|
-
items.filter(a => a).map(treeNavItem)
|
|
1288
|
-
)
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
const treeGlobalNav = h('nav',
|
|
1292
|
-
treeNavList(globalNavItems)
|
|
1293
|
-
)
|
|
1294
|
-
|
|
1295
|
-
const treeAside = h('aside', treeGlobalNav)
|
|
1296
|
-
|
|
1297
|
-
const treeFooter = h('footer', [
|
|
1298
|
-
h('section#copyright', `© ${new Date().getFullYear()}`),
|
|
1299
|
-
h('section#shoutout', [
|
|
1300
|
-
"Made with ",
|
|
1301
|
-
h('a', {
|
|
1302
|
-
href: "https://vowel.cc"
|
|
1303
|
-
}, "Vowel"),
|
|
1304
|
-
])
|
|
1305
|
-
])
|
|
1306
|
-
|
|
1307
|
-
const pageClass = destination.metadata.prettyURL
|
|
1308
|
-
.split("/")
|
|
1309
|
-
.filter(a => a)
|
|
1310
|
-
.map(a => a.replace("_", ""))
|
|
1311
|
-
.join("_")
|
|
1312
|
-
|| "home"
|
|
1313
|
-
|
|
1314
|
-
const treeBody = h(`body.${pageClass}`, [
|
|
1315
|
-
treeHeader,
|
|
1316
|
-
treeMain,
|
|
1317
|
-
treeAside,
|
|
1318
|
-
treeFooter
|
|
1319
|
-
])
|
|
1320
|
-
|
|
1321
|
-
const tree = h(
|
|
1322
|
-
null,
|
|
1323
|
-
[
|
|
1324
|
-
{
|
|
1325
|
-
type: "doctype",
|
|
1326
|
-
name: 'html'
|
|
1327
|
-
},
|
|
1328
|
-
h('html',
|
|
1329
|
-
{
|
|
1330
|
-
lang: "en"
|
|
1331
|
-
},
|
|
1332
|
-
[
|
|
1333
|
-
treeHead,
|
|
1334
|
-
treeBody
|
|
1335
|
-
]
|
|
1336
|
-
)
|
|
1337
|
-
]
|
|
1338
|
-
)
|
|
1339
|
-
|
|
1340
|
-
const data = unified()
|
|
1341
|
-
.use(rehypePresetMinify)
|
|
1342
|
-
.use(rehypeStringify)
|
|
1343
|
-
.stringify(tree)
|
|
418
|
+
newSettings.breadcrumbs = breadcrumb
|
|
1344
419
|
|
|
1345
420
|
return {
|
|
1346
|
-
|
|
421
|
+
urls: [],
|
|
422
|
+
settings: newSettings,
|
|
423
|
+
targets: []
|
|
1347
424
|
}
|
|
1348
425
|
}
|
|
1349
426
|
|
|
@@ -1384,22 +461,16 @@ const readMarkdown = {
|
|
|
1384
461
|
extensions: [".md"],
|
|
1385
462
|
format: "text",
|
|
1386
463
|
readFile,
|
|
1387
|
-
readResource: readURL,
|
|
1388
464
|
transformFile,
|
|
1389
465
|
readFolder,
|
|
1390
466
|
}
|
|
1391
467
|
|
|
1392
|
-
const writeHTML = {
|
|
1393
|
-
extensions: [".html"],
|
|
1394
|
-
format: "text",
|
|
1395
|
-
writeFile
|
|
1396
|
-
}
|
|
1397
468
|
|
|
1398
469
|
|
|
1399
470
|
/** @type {Votive.VotivePlugin} */
|
|
1400
471
|
const vowelMarkdownPlugin = {
|
|
1401
472
|
name: "vowel",
|
|
1402
|
-
processors: [readMarkdown
|
|
473
|
+
processors: [readMarkdown],
|
|
1403
474
|
router
|
|
1404
475
|
}
|
|
1405
476
|
|