vowel 0.1.46 → 0.2.2
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 +82 -20
- package/bin.js +2 -78
- package/docs-source/$features/cards.md +7 -0
- package/docs-source/$features/editing.md +7 -0
- package/docs-source/$features/emoji.md +7 -0
- package/docs-source/$features/frontmatter.md +7 -0
- package/docs-source/$features/lists.md +7 -0
- package/docs-source/$features/navigation.md +7 -0
- package/docs-source/$features/rich-previews.md +7 -0
- package/docs-source/$features/robots.md +7 -0
- package/docs-source/$features/rss.md +7 -0
- package/docs-source/$features/sitemap.md +7 -0
- package/docs-source/$features/speed.md +7 -0
- package/docs-source/$features/static.md +7 -0
- package/docs-source/$features/taxonomies.md +7 -0
- package/docs-source/.cache.json +9 -0
- package/docs-source/.obsidian/app.json +3 -0
- package/docs-source/.obsidian/appearance.json +3 -0
- package/docs-source/.obsidian/core-plugins-migration.json +30 -0
- package/docs-source/.obsidian/core-plugins.json +20 -0
- package/docs-source/.obsidian/workspace.json +168 -0
- package/docs-source/.stackblitzrc +3 -0
- package/docs-source/.vercel/README.txt +11 -0
- package/docs-source/.vercel/project.json +1 -0
- package/docs-source/about.md +3 -0
- package/docs-source/assets/styles.css +51 -0
- package/docs-source/blog/home.md +5 -0
- package/docs-source/blog/url-ui.md +21 -0
- package/docs-source/docs/.votive.db +0 -0
- package/docs-source/docs/deploy.md +67 -0
- package/docs-source/docs/file-structure.md +31 -0
- package/docs-source/docs/folder-settings.md +23 -0
- package/docs-source/docs/home.md +55 -0
- package/docs-source/docs/images.md +10 -0
- package/docs-source/docs/items.md +13 -0
- package/docs-source/docs/pages.md +141 -0
- package/docs-source/docs/settings.md +4 -0
- package/docs-source/docs/styling.md +34 -0
- package/docs-source/docs/taxonomies.md +37 -0
- package/docs-source/home.md +42 -0
- package/docs-source/roadmap.md +98 -0
- package/docs-source/settings.md +12 -0
- package/extractDate.js +83 -0
- package/getMetadata.js +41 -0
- package/index.js +669 -0
- package/jsconfig.json +9 -17
- package/package.json +61 -63
- package/regex.js +36 -0
- package/{src/lib/components → stylesheets}/DefaultStyles.css +5 -5
- package/utils.js +10 -0
- package/.cache.json +0 -1
- package/.prettierrc +0 -8
- package/.vscode/settings.json +0 -3
- package/CHANGELOG.md +0 -79
- package/server.js +0 -87
- package/src/app.d.ts +0 -12
- package/src/app.html +0 -13
- package/src/lib/components/Breadcrumbs.svelte +0 -19
- package/src/lib/components/ConditionalWrapper.svelte +0 -10
- package/src/lib/components/DefaultStyles.svelte +0 -11
- package/src/lib/components/FrontMatterTaxonomy.svelte +0 -48
- package/src/lib/components/Frontmatter.svelte +0 -56
- package/src/lib/components/FrontmatterProperty.svelte +0 -78
- package/src/lib/components/Markdown/Image.svelte +0 -50
- package/src/lib/components/Markdown/Link.svelte +0 -19
- package/src/lib/components/Markdown/LinkPreview.svelte +0 -45
- package/src/lib/components/Markdown/Text.svelte +0 -6
- package/src/lib/components/Markdown/index.svelte +0 -147
- package/src/lib/components/Markdown/validators.js +0 -29
- package/src/lib/components/Nav.svelte +0 -40
- package/src/lib/components/NoStyles.svelte +0 -5
- package/src/lib/components/Page.svelte +0 -90
- package/src/lib/components/ResetStyles.svelte +0 -7
- package/src/lib/components/Sitemap.svelte +0 -38
- package/src/lib/components/TypographyStyles.svelte +0 -10
- package/src/lib/components/index.js +0 -12
- package/src/lib/index.js +0 -1
- package/src/lib/utilities/buildURL.js +0 -18
- package/src/lib/utilities/checkFileExists.js +0 -16
- package/src/lib/utilities/createFolderClass.js +0 -4
- package/src/lib/utilities/createPageClass.js +0 -6
- package/src/lib/utilities/getFileLabel.js +0 -35
- package/src/lib/utilities/getFolder.js +0 -16
- package/src/lib/utilities/getFolderLabel.js +0 -12
- package/src/lib/utilities/getMetadata.js +0 -46
- package/src/lib/utilities/getPage.js +0 -25
- package/src/lib/utilities/getPagesByFolder.js +0 -95
- package/src/lib/utilities/index.js +0 -22
- package/src/lib/utilities/isActiveLink.js +0 -12
- package/src/lib/utilities/isObject.js +0 -8
- package/src/lib/utilities/loadCache.js +0 -28
- package/src/lib/utilities/mutateMarkdownAST.js +0 -68
- package/src/lib/utilities/mutateMarkdownFrontmatter.js +0 -113
- package/src/lib/utilities/parseDate.js +0 -43
- package/src/lib/utilities/processMarkdownFiles.js +0 -243
- package/src/lib/utilities/readMarkdownFile.js +0 -188
- package/src/lib/utilities/regexPatterns.js +0 -12
- package/src/lib/utilities/resolveHomeDirPath.js +0 -5
- package/src/lib/utilities/sendWebmention.js +0 -34
- package/src/lib/utilities/writeCache.js +0 -14
- package/src/routes/$vowel/published.json/+server.js +0 -54
- package/src/routes/+error.svelte +0 -110
- package/src/routes/[...path]/+layout.server.js +0 -78
- package/src/routes/[...path]/+page.server.js +0 -42
- package/src/routes/[...path]/+page.svelte +0 -186
- package/src/routes/feed.xml/+server.js +0 -120
- package/src/routes/robots.txt/+server.js +0 -54
- package/src/routes/sitemap.xml/+server.js +0 -68
- package/static/favicon.png +0 -0
- package/static/styles.css +0 -0
- package/svelte.config.js +0 -30
- package/vercel.json +0 -5
- package/vite.config.js +0 -84
- /package/{src/lib/components → stylesheets}/ResetStyles.css +0 -0
- /package/{src/lib/components → stylesheets}/TypographyStyles.css +0 -0
package/getMetadata.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser"
|
|
2
|
+
import urlMetadata from 'url-metadata';
|
|
3
|
+
|
|
4
|
+
export default async function getMetadata(url) {
|
|
5
|
+
try {
|
|
6
|
+
const urlObject = new URL(url);
|
|
7
|
+
const allMetadata = await urlMetadata(urlObject.href, {
|
|
8
|
+
includeResponseBody: true,
|
|
9
|
+
ensureSecureImageRequest: true
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const parser = new XMLParser({
|
|
13
|
+
unpairedTags: ["!doctype", "meta", "link", "hr", "br", "img"],
|
|
14
|
+
ignoreAttributes: false,
|
|
15
|
+
stopNodes: ["*.pre", "*.script"],
|
|
16
|
+
processEntities: true,
|
|
17
|
+
htmlEntities: true
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
let parsedData = parser.parse(allMetadata.responseBody)
|
|
21
|
+
const webmentionEndpoint = parsedData?.html?.head?.link?.find(link => {
|
|
22
|
+
return link["@_rel"] === "webmention"
|
|
23
|
+
})?.["@_href"]
|
|
24
|
+
|
|
25
|
+
const metadata = {
|
|
26
|
+
image: allMetadata['og:image'],
|
|
27
|
+
ogURL: allMetadata['og:url'],
|
|
28
|
+
canonicalURL: allMetadata.canonical,
|
|
29
|
+
title: allMetadata.title,
|
|
30
|
+
ogTitle: allMetadata['og:title'],
|
|
31
|
+
author: allMetadata.author,
|
|
32
|
+
description: allMetadata.description,
|
|
33
|
+
webmentionEndpoint
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return { metadata }
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log({ fetchingMetadataError: error })
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import voot from "voot"
|
|
2
|
+
import { h } from 'hastscript'
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import { readFileSync } from "fs"
|
|
5
|
+
import { rehype } from "rehype"
|
|
6
|
+
import { find } from 'unist-util-find'
|
|
7
|
+
import { toHast } from 'mdast-util-to-hast'
|
|
8
|
+
import { fromHtml } from 'hast-util-from-html'
|
|
9
|
+
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
10
|
+
import { frontmatter } from "micromark-extension-frontmatter"
|
|
11
|
+
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
|
|
12
|
+
import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"
|
|
13
|
+
import { visit } from "unist-util-visit"
|
|
14
|
+
import { gfmFootnote } from "micromark-extension-gfm-footnote"
|
|
15
|
+
import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough'
|
|
16
|
+
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
|
|
17
|
+
import { gfmTable } from 'micromark-extension-gfm-table'
|
|
18
|
+
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
|
|
19
|
+
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item' // TODO: Add
|
|
20
|
+
import { gfmTaskListItemFromMarkdown } from 'mdast-util-gfm-task-list-item' // TODO: Add
|
|
21
|
+
import { normalizeHeadings } from 'mdast-normalize-headings'
|
|
22
|
+
import { toString as mdastToString } from 'mdast-util-to-string'
|
|
23
|
+
import { toString as hastToString } from 'hast-util-to-string'
|
|
24
|
+
import yaml from 'yaml'
|
|
25
|
+
import extractDate from "./extractDate.js"
|
|
26
|
+
import { testURL } from "./utils.js"
|
|
27
|
+
import path from "node:path"
|
|
28
|
+
import { styleText } from "node:util"
|
|
29
|
+
|
|
30
|
+
const VOWEL_DIR = import.meta.dirname
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/** @import {Runner, ReadPath, VotiveConfig, VotivePlugin, VotiveProcessor, ReadText, ReadAbstract, ReadFolder, ProcessorWrite, Router} from "votive" */
|
|
34
|
+
|
|
35
|
+
/** @type {VotiveProcessor} */
|
|
36
|
+
const cssWriter = {
|
|
37
|
+
syntax: "css",
|
|
38
|
+
write: writeCSS
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @type {VotiveProcessor} */
|
|
42
|
+
const jpegLoader = {
|
|
43
|
+
syntax: "jpeg",
|
|
44
|
+
filter: {
|
|
45
|
+
extensions: [".jpeg", ".jpg"]
|
|
46
|
+
},
|
|
47
|
+
read: {
|
|
48
|
+
path: readImagePath,
|
|
49
|
+
},
|
|
50
|
+
write: writeImage
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/** @type {ProcessorWrite} */
|
|
55
|
+
function writeCSS(destination, database, config) {
|
|
56
|
+
return {
|
|
57
|
+
data: destination.abstract.css,
|
|
58
|
+
encoding: "utf-8"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readURL(data) {
|
|
63
|
+
const hast = fromHtml(data)
|
|
64
|
+
const metadata = {}
|
|
65
|
+
|
|
66
|
+
visit(hast, (node) => {
|
|
67
|
+
if (node.tagName === "meta") {
|
|
68
|
+
if (node.properties && node.properties.property) {
|
|
69
|
+
metadata[node.properties.property] = node.properties.content
|
|
70
|
+
}
|
|
71
|
+
} else if (node.tagName === "title") {
|
|
72
|
+
metadata.title = hastToString(node)
|
|
73
|
+
} else if (node.tagName === "link") {
|
|
74
|
+
if (node.properties?.rel?.includes("me")) {
|
|
75
|
+
metadata.me = node.properties.href
|
|
76
|
+
} else if (node.properties?.rel?.includes("webmention")) {
|
|
77
|
+
metadata.webmention = node.properties.href
|
|
78
|
+
} else if (node.properties?.rel?.includes("icon")) {
|
|
79
|
+
metadata.icon = node.properties.href
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return metadata
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @type {VotiveProcessor} */
|
|
88
|
+
const markdownReader = {
|
|
89
|
+
syntax: "mdast",
|
|
90
|
+
filter: { extensions: [".md"] },
|
|
91
|
+
read: {
|
|
92
|
+
text: readMarkdown,
|
|
93
|
+
url: readURL,
|
|
94
|
+
abstract: readAbstract,
|
|
95
|
+
folder: readFolder
|
|
96
|
+
},
|
|
97
|
+
write: writeHTML
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async function removeCache() {
|
|
102
|
+
try {
|
|
103
|
+
await fs.rm("./.votive.db")
|
|
104
|
+
console.info(styleText("yellow", "Cache cleared"))
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.info(styleText("yellow", "No database cache found"))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function removeDB() {
|
|
111
|
+
try {
|
|
112
|
+
await fs.rmdir("./output")
|
|
113
|
+
console.info(styleText("yellow", "Output cleared"))
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.info(styleText("yellow", "No output cache found"))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await removeCache()
|
|
120
|
+
await removeDB()
|
|
121
|
+
|
|
122
|
+
/** @type {ReadText} */
|
|
123
|
+
function readMarkdown(string, filePath, destinationPath, database, config) {
|
|
124
|
+
const mdast = fromMarkdown(string, {
|
|
125
|
+
// Micromark extensions
|
|
126
|
+
extensions: [
|
|
127
|
+
frontmatter(),
|
|
128
|
+
gfmFootnote(),
|
|
129
|
+
gfmStrikethrough(),
|
|
130
|
+
gfmTable()
|
|
131
|
+
],
|
|
132
|
+
mdastExtensions: [
|
|
133
|
+
frontmatterFromMarkdown(),
|
|
134
|
+
gfmFootnoteFromMarkdown(),
|
|
135
|
+
gfmStrikethroughFromMarkdown(),
|
|
136
|
+
gfmTableFromMarkdown()
|
|
137
|
+
]
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
normalizeHeadings(mdast)
|
|
141
|
+
const pathInfo = path.parse(filePath)
|
|
142
|
+
const metadata = getMetadata(mdast)
|
|
143
|
+
metadata.inferred_label = pathInfo.name
|
|
144
|
+
const destinationInfo = path.parse(destinationPath)
|
|
145
|
+
metadata.prettyURL = (new URL(`${destinationInfo.dir}/${destinationInfo.name}`, "thismessage:/")).pathname
|
|
146
|
+
const jobs = []
|
|
147
|
+
|
|
148
|
+
selectMetadata(metadata)
|
|
149
|
+
|
|
150
|
+
if (pathInfo.base === "settings.md") {
|
|
151
|
+
for (const key in metadata) {
|
|
152
|
+
database.setSetting(
|
|
153
|
+
pathInfo.dir,
|
|
154
|
+
key,
|
|
155
|
+
metadata[key],
|
|
156
|
+
filePath
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
visit(mdast, (node, index, parent) => {
|
|
162
|
+
if (node.type === "text" && parent.children.length === 1 && parent.type === "paragraph") {
|
|
163
|
+
const validURL = testURL(node.value)
|
|
164
|
+
if (validURL) {
|
|
165
|
+
|
|
166
|
+
jobs.push({
|
|
167
|
+
data: node.value,
|
|
168
|
+
runner: "text",
|
|
169
|
+
destination: destinationPath
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return { abstract: mdast, metadata, jobs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @param {ReturnType<getMetadata>} metadata */
|
|
179
|
+
function selectMetadata(metadata) {
|
|
180
|
+
const date =
|
|
181
|
+
metadata.fm_date
|
|
182
|
+
|| metadata.inferred_date
|
|
183
|
+
|
|
184
|
+
if (date) {
|
|
185
|
+
metadata.date = date
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const title =
|
|
189
|
+
metadata.fm_title
|
|
190
|
+
|| metadata.inferred_title
|
|
191
|
+
|| metadata.inferred_label
|
|
192
|
+
|
|
193
|
+
if (title) {
|
|
194
|
+
metadata.title = title
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const breadcrumb =
|
|
198
|
+
metadata.fm_breadcrumb
|
|
199
|
+
|| metadata.title
|
|
200
|
+
|| metadata.inferred_label
|
|
201
|
+
|
|
202
|
+
if (breadcrumb) {
|
|
203
|
+
metadata.breadcrumb = breadcrumb
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const description =
|
|
207
|
+
metadata.fm_description
|
|
208
|
+
|| metadata.inferred_description
|
|
209
|
+
|
|
210
|
+
if (description) {
|
|
211
|
+
metadata.description = description
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const image =
|
|
215
|
+
metadata.fm_image
|
|
216
|
+
|| metadata.inferred_image
|
|
217
|
+
|
|
218
|
+
if (image) {
|
|
219
|
+
metadata.image = image
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} text
|
|
226
|
+
*/
|
|
227
|
+
function truncateText(text) {
|
|
228
|
+
const match = text.match(/^(?<desc>((\b.+?){28})\S)\s(?<etc>.+)$/)
|
|
229
|
+
if (!match) {
|
|
230
|
+
return text
|
|
231
|
+
} else if (match.groups.etc) {
|
|
232
|
+
return match.groups.desc + "..."
|
|
233
|
+
} else {
|
|
234
|
+
return match.groups.desc
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @param {object} tree */
|
|
239
|
+
function getMetadata(tree) {
|
|
240
|
+
const metadata = {}
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
243
|
+
const child = tree.children[i]
|
|
244
|
+
const text = mdastToString(child)
|
|
245
|
+
switch (child.type) {
|
|
246
|
+
case "paragraph":
|
|
247
|
+
if (child.children.length !== 1) {
|
|
248
|
+
if (!metadata.fm_description && !metadata.inferred_description) {
|
|
249
|
+
const description = truncateText(text)
|
|
250
|
+
metadata.inferred_description = description
|
|
251
|
+
}
|
|
252
|
+
i = Infinity
|
|
253
|
+
break
|
|
254
|
+
} else if (child.children[0].type === "image") {
|
|
255
|
+
metadata.inferred_image = child.children[0].url
|
|
256
|
+
metadata.inferred_alt_text = child.children[0].alt
|
|
257
|
+
break
|
|
258
|
+
} else if (testURL(text)) {
|
|
259
|
+
const url = new URL(text)
|
|
260
|
+
if (text.match(/\.(jpeg|jpg|png)$/)) {
|
|
261
|
+
metadata.inferred_image = url
|
|
262
|
+
break
|
|
263
|
+
} else {
|
|
264
|
+
metadata.inferred_link = url
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const inferred_date = extractDate(mdastToString(child))
|
|
269
|
+
if (inferred_date) metadata.inferred_date = inferred_date
|
|
270
|
+
else i = Infinity
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
case "heading":
|
|
274
|
+
if (child.depth === 1) {
|
|
275
|
+
metadata.inferred_title = mdastToString(child)
|
|
276
|
+
}
|
|
277
|
+
break
|
|
278
|
+
case "yaml":
|
|
279
|
+
const frontmatter = yaml.parse(child.value)
|
|
280
|
+
for (const key in frontmatter) {
|
|
281
|
+
metadata["fm_" + key] = frontmatter[key]
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
default:
|
|
285
|
+
i = Infinity
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// TODO: Extract links!
|
|
291
|
+
// TODO: Extract backlinks
|
|
292
|
+
return metadata
|
|
293
|
+
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @type {ReadAbstract} */
|
|
297
|
+
function readAbstract(abstract, database, config) {
|
|
298
|
+
const jobs = []
|
|
299
|
+
return { abstract, jobs }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @type {ReadFolder} */
|
|
303
|
+
function readFolder(folder, database, config, isRoot) {
|
|
304
|
+
const folderInfo = path.parse(folder)
|
|
305
|
+
|
|
306
|
+
const indexPath = path.format({
|
|
307
|
+
dir: path.join(folderInfo.dir, folderInfo.name),
|
|
308
|
+
name: "index",
|
|
309
|
+
ext: ".html"
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const aliasPath = path.format({
|
|
313
|
+
dir: path.join(folderInfo.dir),
|
|
314
|
+
name: folderInfo.name,
|
|
315
|
+
ext: ".html"
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const indexFile = database.getDestinationIndependently(indexPath)
|
|
319
|
+
const aliasFile = database.getDestinationIndependently(aliasPath)
|
|
320
|
+
|
|
321
|
+
if (!indexFile && !aliasFile) {
|
|
322
|
+
database.createOrUpdateDestination({
|
|
323
|
+
metadata: {
|
|
324
|
+
title: folderInfo.name
|
|
325
|
+
},
|
|
326
|
+
path: aliasPath,
|
|
327
|
+
abstract: {},
|
|
328
|
+
syntax: "mdast"
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if (isRoot) {
|
|
334
|
+
// TODO: Create theme job
|
|
335
|
+
const settings = database.getSettings(config.sourceFolder)
|
|
336
|
+
if (!settings.theme || settings.theme[0] === "default") {
|
|
337
|
+
database.setSetting(folder, "theme", "default")
|
|
338
|
+
// TODO: Save theme file (reset, typography, default)
|
|
339
|
+
|
|
340
|
+
const resetStylesPath = path.join(VOWEL_DIR, "stylesheets", "ResetStyles.css")
|
|
341
|
+
const typeStylesPath = path.join(VOWEL_DIR, "stylesheets", "TypographyStyles.css")
|
|
342
|
+
const defaultStylesPath = path.join(VOWEL_DIR, "stylesheets", "DefaultStyles.css")
|
|
343
|
+
|
|
344
|
+
const resetStyles = readFileSync(resetStylesPath, "utf-8")
|
|
345
|
+
const typeStyles = readFileSync(typeStylesPath, "utf-8")
|
|
346
|
+
const defaultStyles = readFileSync(defaultStylesPath, "utf-8")
|
|
347
|
+
|
|
348
|
+
database.createOrUpdateDestination({
|
|
349
|
+
path: "reset.css",
|
|
350
|
+
abstract: { css: resetStyles },
|
|
351
|
+
metadata: {},
|
|
352
|
+
syntax: "css"
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
database.createOrUpdateDestination({
|
|
356
|
+
path: "typography.css",
|
|
357
|
+
abstract: { css: typeStyles },
|
|
358
|
+
metadata: {},
|
|
359
|
+
syntax: "css"
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
database.createOrUpdateDestination({
|
|
363
|
+
path: "default.css",
|
|
364
|
+
abstract: { css: defaultStyles },
|
|
365
|
+
metadata: {},
|
|
366
|
+
syntax: "css"
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
// TODO: Create robots
|
|
370
|
+
// TODO: Create RSS
|
|
371
|
+
// TODO: Create sitemap
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
jobs: [],
|
|
378
|
+
destinations: []
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @type {ProcessorWrite} */
|
|
383
|
+
function writeHTML(destination, database, config) {
|
|
384
|
+
const settings = database.getSettings(destination.dir)
|
|
385
|
+
const { abstract, metadata, ...rest } = destination
|
|
386
|
+
|
|
387
|
+
/** @param {string} filePath */
|
|
388
|
+
function listFolders(filePath) {
|
|
389
|
+
if (!filePath) return []
|
|
390
|
+
|
|
391
|
+
const pathInfo = path.parse(filePath)
|
|
392
|
+
const dir = pathInfo.dir && pathInfo.dir + path.sep
|
|
393
|
+
|
|
394
|
+
return [filePath, ...listFolders(
|
|
395
|
+
dir
|
|
396
|
+
)]
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const ancestorFolders = listFolders(rest.dir)
|
|
400
|
+
|
|
401
|
+
const family = database.getDestinations({
|
|
402
|
+
filter: [
|
|
403
|
+
{
|
|
404
|
+
property: "dir",
|
|
405
|
+
operator: "in",
|
|
406
|
+
value: ancestorFolders
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
}, destination.path)
|
|
410
|
+
|
|
411
|
+
const treeStyleSheets = []
|
|
412
|
+
|
|
413
|
+
!settings.theme || settings.theme.includes("default") && treeStyleSheets.push(
|
|
414
|
+
h('link', {
|
|
415
|
+
rel: "stylesheet",
|
|
416
|
+
href: "/default.css"
|
|
417
|
+
}),
|
|
418
|
+
h('link', {
|
|
419
|
+
rel: "stylesheet",
|
|
420
|
+
href: "/typography.css"
|
|
421
|
+
}),
|
|
422
|
+
h('link', {
|
|
423
|
+
rel: "stylesheet",
|
|
424
|
+
href: "/reset.css"
|
|
425
|
+
})
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
function createTitle() {
|
|
430
|
+
if (metadata.title && settings.title) {
|
|
431
|
+
const titles = [metadata.title, ...settings.title.reverse()]
|
|
432
|
+
return titles.join(" - ")
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (metadata.title || settings.title) {
|
|
436
|
+
return metadata.title || settings.title.reverse()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return "Website"
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const treeHead = h('head', [
|
|
443
|
+
h('title', createTitle()),
|
|
444
|
+
h('meta', {
|
|
445
|
+
property: "og:description",
|
|
446
|
+
content: metadata.description,
|
|
447
|
+
}),
|
|
448
|
+
...treeStyleSheets,
|
|
449
|
+
// TODO: Favicon
|
|
450
|
+
// TODO: Site name
|
|
451
|
+
// TODO: og:url
|
|
452
|
+
// TODO: Canonical URL
|
|
453
|
+
// TODO: REL=ME (github)
|
|
454
|
+
// TODO: Webmention URL
|
|
455
|
+
// TODO: Image
|
|
456
|
+
])
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
function treeNavItems(navItem) {
|
|
460
|
+
return h('a', {
|
|
461
|
+
href: navItem.metadata.prettyURL,
|
|
462
|
+
"aria-current": metadata.prettyURL === navItem.metadata.prettyURL ? 'page' : null
|
|
463
|
+
}, navItem.metadata.breadcrumb)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function navItemFilter(nav_item) {
|
|
467
|
+
return !nav_item.date
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
function treeNavFolder(navFolder) {
|
|
472
|
+
return navFolder.length > 1
|
|
473
|
+
? h('nav.primary', navFolder.filter(navItemFilter).map(treeNavItems))
|
|
474
|
+
: null
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// TODO: Filter singleton navs
|
|
478
|
+
// TODO: Filter ephemeral content
|
|
479
|
+
|
|
480
|
+
const groupedNavs = Object.groupBy(family, ({ dir }) => dir)
|
|
481
|
+
|
|
482
|
+
const treeNav = Object.values(groupedNavs).map(treeNavFolder)
|
|
483
|
+
|
|
484
|
+
let treeBreadcrumbs = []
|
|
485
|
+
|
|
486
|
+
if (settings.breadcrumbs) {
|
|
487
|
+
treeBreadcrumbs.push(
|
|
488
|
+
...settings.breadcrumb.map((b, i) => h('a', {
|
|
489
|
+
href: settings.prettyURL[i]
|
|
490
|
+
}, b))
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
treeBreadcrumbs.push(
|
|
495
|
+
h('a', {
|
|
496
|
+
href: destination.prettyURL,
|
|
497
|
+
'aria-current': 'page'
|
|
498
|
+
}, metadata.breadcrumb)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
const headerElements = []
|
|
503
|
+
|
|
504
|
+
if (settings.title) {
|
|
505
|
+
headerElements.push(h('a.site-title', settings.title[0]))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (settings.description) {
|
|
509
|
+
headerElements.push(h('p.subtitle', settings.description[0]))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const treeHeader = h('header', [
|
|
513
|
+
// TODO: Logo
|
|
514
|
+
...headerElements,
|
|
515
|
+
...treeNav,
|
|
516
|
+
h('nav.breadcrumbs', {
|
|
517
|
+
'aria-label': 'Breadcrumb'
|
|
518
|
+
}, treeBreadcrumbs)
|
|
519
|
+
])
|
|
520
|
+
|
|
521
|
+
// TODO: Add page ID as a class
|
|
522
|
+
|
|
523
|
+
const treeMain = h('main.h-entry', toHast(abstract).children)
|
|
524
|
+
|
|
525
|
+
visit(treeMain, (node, index, parent) => {
|
|
526
|
+
if (node.type === "text" && parent.tagName === 'p' && parent.children.length === 1) {
|
|
527
|
+
const validURL = testURL(node.value)
|
|
528
|
+
if (validURL) {
|
|
529
|
+
const metadata = database.getURL(node.value)
|
|
530
|
+
if (metadata) {
|
|
531
|
+
parent.tagName = "article"
|
|
532
|
+
// TODO: Add url to metadata
|
|
533
|
+
parent.children = [
|
|
534
|
+
h("a", { href: node.value },
|
|
535
|
+
h("h2", metadata.title)
|
|
536
|
+
)
|
|
537
|
+
]
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// TODO: Write sidebar
|
|
544
|
+
const treeSidebar = h('nav.secondary')
|
|
545
|
+
const treeFooter = h('footer', `© ${new Date().getFullYear()}`)
|
|
546
|
+
|
|
547
|
+
const treeBody = h('body.page', [
|
|
548
|
+
treeHeader,
|
|
549
|
+
treeMain,
|
|
550
|
+
treeSidebar,
|
|
551
|
+
treeFooter
|
|
552
|
+
])
|
|
553
|
+
|
|
554
|
+
const tree = h(
|
|
555
|
+
null,
|
|
556
|
+
[
|
|
557
|
+
// TODO: Add doctype
|
|
558
|
+
h('html', [
|
|
559
|
+
treeHead,
|
|
560
|
+
treeBody
|
|
561
|
+
])
|
|
562
|
+
]
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
// Mutate
|
|
566
|
+
// Get links
|
|
567
|
+
// - section, article.thumbnail
|
|
568
|
+
|
|
569
|
+
// TODO: WRITE HTML
|
|
570
|
+
|
|
571
|
+
const data = rehype()
|
|
572
|
+
.stringify(tree)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
data
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** @type {Router} */
|
|
581
|
+
function router({ name, dir, inRootDir }) {
|
|
582
|
+
if (name.startsWith("$")) return false
|
|
583
|
+
if (dir.find(segment => segment.startsWith("$"))) return false
|
|
584
|
+
|
|
585
|
+
switch (name) {
|
|
586
|
+
case "settings":
|
|
587
|
+
return false
|
|
588
|
+
case "home":
|
|
589
|
+
if (inRootDir) {
|
|
590
|
+
return {
|
|
591
|
+
dir,
|
|
592
|
+
name: "index",
|
|
593
|
+
ext: ".html"
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
dir: dir.slice(0, -1).map(segment => segment.replaceAll(/[^\w\/]/g, "-").replaceAll(/--+/g, "-").toLowerCase()),
|
|
598
|
+
name: dir.at(-1),
|
|
599
|
+
ext: ".html"
|
|
600
|
+
}
|
|
601
|
+
default:
|
|
602
|
+
return {
|
|
603
|
+
dir: dir.map(segment => segment.replaceAll(/[^\w\/]/g, "-").replaceAll(/--+/g, "-").toLowerCase()),
|
|
604
|
+
name: name.replaceAll(/[^\w\/]/g, "-").replaceAll(/--+/g, "-").toLowerCase(),
|
|
605
|
+
ext: "html"
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @type {ReadPath} */
|
|
611
|
+
async function readImagePath(string, database) {
|
|
612
|
+
// TODO: Resize and optimize images
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
metadata: {},
|
|
616
|
+
abstract: { path: string }
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** @type {ProcessorWrite} */
|
|
621
|
+
async function writeImage(destination, database, config) {
|
|
622
|
+
const buffer = await fs.readFile(destination.path)
|
|
623
|
+
return {
|
|
624
|
+
buffer
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** @type {VotivePlugin} */
|
|
629
|
+
const vowelMarkdown = {
|
|
630
|
+
name: "vowel",
|
|
631
|
+
processors: [markdownReader, cssWriter],
|
|
632
|
+
router
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** @type {VotivePlugin} */
|
|
636
|
+
const vowelJpeg = {
|
|
637
|
+
name: "vowel-jpeg",
|
|
638
|
+
processors: [jpegLoader],
|
|
639
|
+
router: ({ name, dir, ext }) => {
|
|
640
|
+
return {
|
|
641
|
+
name, dir, ext
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
/** @type {VotiveConfig} */
|
|
649
|
+
const config = {
|
|
650
|
+
sourceFolder: ".",
|
|
651
|
+
destinationFolder: "output",
|
|
652
|
+
plugins: [
|
|
653
|
+
vowelMarkdown,
|
|
654
|
+
vowelJpeg
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
async function init() {
|
|
660
|
+
const then = performance.now()
|
|
661
|
+
// await fs.rm(config.destinationFolder, { recursive: true, force: true })
|
|
662
|
+
// await fs.mkdir(config.destinationFolder, { recursive: true })
|
|
663
|
+
const cache = await voot(config)
|
|
664
|
+
console.log(styleText("red", (performance.now() - then).toFixed(4) + "ms"))
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
export default init
|
|
669
|
+
|