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