vowel 0.6.4 → 0.7.0-beta.0
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 +1 -1
- package/bin.js +47 -27
- package/cli.js +15 -0
- package/config.js +93 -26
- package/extractDate.js +70 -6
- package/index.js +14 -5
- package/package.json +39 -8
- package/plugins/assets/index.js +65 -0
- package/plugins/atproto/index.js +132 -0
- package/plugins/atproto/password.js +71 -0
- package/plugins/atproto/publish.js +406 -0
- package/plugins/atproto/record.js +144 -0
- package/plugins/atproto/rkey.js +54 -0
- package/plugins/deploy-cloudflare/index.js +30 -16
- package/plugins/fonts/index.js +18 -21
- package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/index.js +21145 -0
- package/plugins/html/editor/EditButton.svelte +152 -0
- package/plugins/html/editor/Editor.svelte +51 -0
- package/plugins/html/editor/Overlays.svelte +6 -0
- package/plugins/html/editor/SettingsPanel.svelte +397 -0
- package/plugins/html/editor/Toolbar.svelte +644 -0
- package/plugins/html/editor/create-session.js +250 -0
- package/plugins/html/editor/directives.js +128 -0
- package/plugins/html/editor/frontmatter.js +97 -0
- package/plugins/html/editor/ingest.js +339 -0
- package/plugins/html/editor/main.js +94 -0
- package/plugins/html/editor/nodes/Alert.svelte +21 -0
- package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
- package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
- package/plugins/html/editor/nodes/Embed.svelte +19 -0
- package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
- package/plugins/html/editor/nodes/Heading.svelte +15 -0
- package/plugins/html/editor/nodes/Highlight.svelte +11 -0
- package/plugins/html/editor/nodes/Image.svelte +18 -0
- package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
- package/plugins/html/editor/nodes/Link.svelte +17 -0
- package/plugins/html/editor/nodes/List.svelte +15 -0
- package/plugins/html/editor/nodes/ListItem.svelte +9 -0
- package/plugins/html/editor/nodes/Page.svelte +21 -0
- package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
- package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
- package/plugins/html/editor/nodes/Strong.svelte +11 -0
- package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
- package/plugins/html/editor/nodes/index.js +18 -0
- package/plugins/html/editor/rollup.config.js +35 -0
- package/plugins/html/editor/serialize.js +157 -0
- package/plugins/html/editor/settings-file.js +83 -0
- package/plugins/html/editor/socket.js +202 -0
- package/plugins/html/editor/source.js +43 -0
- package/plugins/html/image.js +31 -5
- package/plugins/html/index.js +430 -458
- package/plugins/html/partials.js +299 -0
- package/plugins/html/socialLinks.js +67 -0
- package/plugins/html/writeRules.js +377 -0
- package/plugins/icons/index.js +2 -6
- package/plugins/images/index.js +78 -55
- package/plugins/markdown/brandColors.js +10 -0
- package/plugins/markdown/colorScheme.js +64 -0
- package/plugins/markdown/emoji.js +30 -0
- package/plugins/markdown/frontmatter.js +253 -0
- package/plugins/markdown/index.js +276 -334
- package/plugins/markdown/links.js +97 -0
- package/plugins/markdown/metadata.js +229 -68
- package/plugins/markdown/readRules.js +143 -0
- package/plugins/markdown/typography.js +109 -0
- package/plugins/markdown/visitor.js +143 -0
- package/plugins/publish/index.js +34 -0
- package/plugins/robots/agents.js +79 -0
- package/plugins/robots/index.js +30 -10
- package/plugins/robots/robots.js +48 -0
- package/plugins/styles/index.js +111 -18
- package/plugins/styles/theme.js +116 -0
- package/plugins/urls/index.js +49 -62
- package/plugins/urls/urlMetadata.js +246 -0
- package/plugins/vectors/index.js +5 -9
- package/plugins/xml/entryContent.js +103 -0
- package/plugins/xml/index.js +105 -43
- package/secretPaths.js +170 -0
- package/stylesheets/DefaultStyles.css +367 -437
- package/stylesheets/ResetStyles.css +2 -38
- package/stylesheets/SyntaxHighlightingStyles.css +36 -4
- package/stylesheets/TypographyStyles.css +386 -128
- package/systemPaths.js +48 -0
- package/utils.js +56 -5
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
- package/.github/ISSUE_TEMPLATE/config.yml +0 -7
- package/docs-source/.cache.json +0 -9
- package/docs-source/.obsidian/app.json +0 -3
- package/docs-source/.obsidian/appearance.json +0 -3
- package/docs-source/.obsidian/core-plugins-migration.json +0 -30
- package/docs-source/.obsidian/core-plugins.json +0 -20
- package/docs-source/.obsidian/workspace.json +0 -168
- package/docs-source/.stackblitzrc +0 -3
- package/docs-source/.votive.db +0 -0
- package/docs-source/about.md +0 -3
- package/docs-source/blog/home.md +0 -5
- package/docs-source/blog/url-ui.md +0 -21
- package/docs-source/blog.md +0 -5
- package/docs-source/docs/.votive.db +0 -0
- package/docs-source/docs/deploy.md +0 -67
- package/docs-source/docs/file-structure.md +0 -31
- package/docs-source/docs/folder-settings.md +0 -23
- package/docs-source/docs/home.md +0 -55
- package/docs-source/docs/images.md +0 -10
- package/docs-source/docs/items.md +0 -13
- package/docs-source/docs/pages.md +0 -141
- package/docs-source/docs/settings.md +0 -4
- package/docs-source/docs/styling.md +0 -34
- package/docs-source/docs/taxonomies.md +0 -37
- package/docs-source/features/cards.md +0 -7
- package/docs-source/features/editing.md +0 -7
- package/docs-source/features/emoji.md +0 -7
- package/docs-source/features/frontmatter.md +0 -7
- package/docs-source/features/lists.md +0 -7
- package/docs-source/features/navigation.md +0 -7
- package/docs-source/features/rich-previews.md +0 -7
- package/docs-source/features/robots.md +0 -7
- package/docs-source/features/rss.md +0 -7
- package/docs-source/features/sitemap.md +0 -7
- package/docs-source/features/speed.md +0 -7
- package/docs-source/features/static.md +0 -7
- package/docs-source/features/taxonomies.md +0 -7
- package/docs-source/home.md +0 -42
- package/docs-source/output/about.html +0 -1
- package/docs-source/output/blog/url-ui.html +0 -8
- package/docs-source/output/blog.html +0 -1
- package/docs-source/output/default.css +0 -1
- package/docs-source/output/docs/deploy.html +0 -34
- package/docs-source/output/docs/file-structure.html +0 -24
- package/docs-source/output/docs/folder-settings.html +0 -20
- package/docs-source/output/docs/images.html +0 -2
- package/docs-source/output/docs/items.html +0 -6
- package/docs-source/output/docs/pages.html +0 -101
- package/docs-source/output/docs/styling.html +0 -18
- package/docs-source/output/docs/taxonomies.html +0 -20
- package/docs-source/output/docs.html +0 -28
- package/docs-source/output/features/cards.html +0 -1
- package/docs-source/output/features/editing.html +0 -1
- package/docs-source/output/features/emoji.html +0 -1
- package/docs-source/output/features/frontmatter.html +0 -1
- package/docs-source/output/features/lists.html +0 -1
- package/docs-source/output/features/navigation.html +0 -1
- package/docs-source/output/features/rich-previews.html +0 -1
- package/docs-source/output/features/robots.html +0 -1
- package/docs-source/output/features/rss.html +0 -1
- package/docs-source/output/features/sitemap.html +0 -1
- package/docs-source/output/features/speed.html +0 -1
- package/docs-source/output/features/static.html +0 -1
- package/docs-source/output/features/taxonomies.html +0 -1
- package/docs-source/output/features.html +0 -1
- package/docs-source/output/feed.xml +0 -1
- package/docs-source/output/index.html +0 -21
- package/docs-source/output/reset.css +0 -1
- package/docs-source/output/roadmap.html +0 -87
- package/docs-source/output/robots.txt +0 -14
- package/docs-source/output/sitemap.xml +0 -16
- package/docs-source/output/typography.css +0 -1
- package/docs-source/roadmap.md +0 -98
- package/docs-source/settings.md +0 -13
- package/getMetadata.js +0 -41
- package/jsconfig.json +0 -10
- package/plugins/markdown/robots.js +0 -18
- package/regex.js +0 -36
- package/stylesheets/brand-colors.css +0 -119
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*Markdown websites in milliseconds*
|
|
4
4
|
|
|
5
5
|
- Bundled with [Votive](https://github.com/samlfair/votive)
|
|
6
|
-
- Served with [
|
|
6
|
+
- Served with [Votive](https://github.com/samlfair/votive)'s dev server
|
|
7
7
|
|
|
8
8
|
See the docs: [vowel.cc](https://vowel.cc).
|
|
9
9
|
|
package/bin.js
CHANGED
|
@@ -6,7 +6,12 @@ import { parse } from "@bomb.sh/args"
|
|
|
6
6
|
import t from "@bomb.sh/tab"
|
|
7
7
|
import votive from "votive"
|
|
8
8
|
import { styleText } from "node:util"
|
|
9
|
-
import {
|
|
9
|
+
import { createConfig } from "./config.js"
|
|
10
|
+
|
|
11
|
+
// The CLI's own boundary: cwd is the project folder by convention (you
|
|
12
|
+
// `cd` into your blog and run `vowel`), so "." is the right default here
|
|
13
|
+
// specifically - see createConfig's own doc comment.
|
|
14
|
+
const config = createConfig(".")
|
|
10
15
|
|
|
11
16
|
/** @param {number} ms */
|
|
12
17
|
function wait(ms) {
|
|
@@ -14,27 +19,37 @@ function wait(ms) {
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
import init from "./index.js"
|
|
22
|
+
import { shouldClear } from "./cli.js"
|
|
17
23
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Discards the build database. Only under --clear (alias --reset): an
|
|
26
|
+
* ordinary launch is a warm start, reusing the database and the output,
|
|
27
|
+
* so a second launch on an unchanged site has nothing to do. The wipe on
|
|
28
|
+
* every launch was a development convenience, and it hid the on-disk
|
|
29
|
+
* database path from the CLI entirely.
|
|
30
|
+
* @param {boolean} verbose
|
|
31
|
+
*/
|
|
32
|
+
async function removeDatabase(verbose) {
|
|
20
33
|
try {
|
|
21
34
|
await fs.rm(config.databasePath)
|
|
22
|
-
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "
|
|
35
|
+
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "database cleared")}`)
|
|
23
36
|
} catch (e) {
|
|
24
|
-
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "no database
|
|
37
|
+
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "no database found")}`)
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
/** @param {boolean} verbose */
|
|
29
|
-
async function
|
|
42
|
+
async function removeOutput(verbose) {
|
|
30
43
|
try {
|
|
31
44
|
await fs.rm(config.targetFolder, { recursive: true, force: true })
|
|
32
45
|
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "output cleared")}`)
|
|
33
46
|
} catch (e) {
|
|
34
|
-
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "no output
|
|
47
|
+
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "no output found")}`)
|
|
35
48
|
}
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
|
|
52
|
+
|
|
38
53
|
async function exists(filePath) {
|
|
39
54
|
try {
|
|
40
55
|
await fs.stat(filePath)
|
|
@@ -74,21 +89,12 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
74
89
|
|
|
75
90
|
box(`Change these configurations by editing 'settings.md'.`)
|
|
76
91
|
|
|
77
|
-
const
|
|
78
|
-
message: "Website
|
|
92
|
+
const websiteName = await text({
|
|
93
|
+
message: "Website name",
|
|
79
94
|
placeholder: "My Cool Website"
|
|
80
95
|
})
|
|
81
96
|
|
|
82
|
-
frontmatter.push(`
|
|
83
|
-
|
|
84
|
-
const tagline = await text({
|
|
85
|
-
message: "Website tagline (leave empty to skip)",
|
|
86
|
-
placeholder: "All the news that's fit to blog"
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
if (tagline) {
|
|
90
|
-
frontmatter.push(`tagline: ${tagline}`)
|
|
91
|
-
}
|
|
97
|
+
frontmatter.push(`name: ${websiteName}`)
|
|
92
98
|
|
|
93
99
|
box(`Vowel comes loaded with themes. You can override these themes by writing styles in a 'styles.css' file at the root of your project.`)
|
|
94
100
|
|
|
@@ -150,6 +156,12 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
150
156
|
message: "Generate homepage"
|
|
151
157
|
})
|
|
152
158
|
|
|
159
|
+
// A tagline is a page's; the homepage's is the site's hero.
|
|
160
|
+
const tagline = await text({
|
|
161
|
+
message: "Homepage tagline (leave empty to skip)",
|
|
162
|
+
placeholder: "All the news that's fit to blog"
|
|
163
|
+
})
|
|
164
|
+
|
|
153
165
|
const spin = spinner()
|
|
154
166
|
|
|
155
167
|
spin.start('Setting up')
|
|
@@ -167,7 +179,8 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
167
179
|
const homeExists = await exists("home.md")
|
|
168
180
|
|
|
169
181
|
if (!homeExists) {
|
|
170
|
-
|
|
182
|
+
const homeFrontmatter = tagline ? `---\ntagline: ${tagline}\n---\n` : ""
|
|
183
|
+
await fs.writeFile("home.md", `${homeFrontmatter}# Home\n\nWelcome home!`, "utf-8")
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
spin.clear()
|
|
@@ -193,14 +206,14 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
193
206
|
/**
|
|
194
207
|
* Runs one registered command directly and exits - no dev server, no
|
|
195
208
|
* wizard, no cache-wiping. Reuses the exact handler a live "Publish"
|
|
196
|
-
* button would trigger over WS (see
|
|
209
|
+
* button would trigger over WS (see votive's runCommand/handleCommand) -
|
|
197
210
|
* this is just the other invocation path for the same registered
|
|
198
211
|
* function, meant for CI/automated use (e.g. `vowel --command deploy`).
|
|
199
212
|
* @param {string} name
|
|
200
213
|
* @param {string} [payloadJSON]
|
|
201
214
|
*/
|
|
202
215
|
async function runCLICommand(name, payloadJSON) {
|
|
203
|
-
const { runCommand } = await import("
|
|
216
|
+
const { runCommand } = await import("votive")
|
|
204
217
|
|
|
205
218
|
let payload
|
|
206
219
|
try {
|
|
@@ -232,18 +245,25 @@ async function main() {
|
|
|
232
245
|
|
|
233
246
|
const dbExists = await exists(config.databasePath)
|
|
234
247
|
|
|
235
|
-
|
|
236
|
-
|
|
248
|
+
if (shouldClear(args)) {
|
|
249
|
+
await removeDatabase(args.logging === "verbose")
|
|
250
|
+
await removeOutput(args.logging === "verbose")
|
|
251
|
+
}
|
|
237
252
|
await fs.mkdir(config.targetFolder, { recursive: true })
|
|
238
253
|
|
|
254
|
+
// The wizard runs on a first launch only: no database yet is the one
|
|
255
|
+
// signal that legitimately means "new project", and --clear does not
|
|
256
|
+
// change that - a cleared project is still one that was set up.
|
|
239
257
|
if (!args.skip && !dbExists) {
|
|
240
|
-
const loading = votive({ ...config, verbose: false })
|
|
258
|
+
// const loading = votive({ ...config, verbose: false })
|
|
241
259
|
await wizard()
|
|
242
|
-
await (loading)
|
|
260
|
+
// await (loading)
|
|
243
261
|
}
|
|
244
262
|
|
|
245
263
|
|
|
246
|
-
|
|
264
|
+
// `--editor` is deliberately undocumented: the in-page editor is not
|
|
265
|
+
// part of 1.0. It reaches the html plugin as config.editor.
|
|
266
|
+
init({ ...args, editor: Boolean(args.editor) })
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
|
package/cli.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decisions the CLI makes from its arguments, kept out of bin.js so they
|
|
3
|
+
* can be tested without launching a server.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether this launch starts cold. `--clear` and `--reset` are the same
|
|
8
|
+
* flag; anything else is a warm start that reuses the database and the
|
|
9
|
+
* output, so a second launch on an unchanged site has nothing to do.
|
|
10
|
+
* The wipe on every launch was a development convenience.
|
|
11
|
+
* @param {Record<string, unknown>} args - as @bomb.sh/args parses them
|
|
12
|
+
*/
|
|
13
|
+
export function shouldClear(args) {
|
|
14
|
+
return Boolean(args?.clear || args?.reset)
|
|
15
|
+
}
|
package/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
-
import {
|
|
2
|
+
import { resolveProjectFolder } from "votive"
|
|
3
|
+
import { systemDirectoryFor } from "./systemPaths.js"
|
|
4
|
+
import { secretRouter } from "./secretPaths.js"
|
|
3
5
|
import vowelImagesPlugin from "./plugins/images/index.js"
|
|
4
6
|
import vowelStylesPlugin from "./plugins/styles/index.js"
|
|
5
7
|
import vowelReadMarkdownPlugin from "./plugins/markdown/index.js"
|
|
@@ -7,35 +9,100 @@ import vowelWriteRobotsPlugin from "./plugins/robots/index.js"
|
|
|
7
9
|
import vowelWriteXMLPlugin from "./plugins/xml/index.js"
|
|
8
10
|
import vowelVectorPlugin from "./plugins/vectors/index.js"
|
|
9
11
|
import vowelFontsPlugin from "./plugins/fonts/index.js"
|
|
12
|
+
import vowelAssetsPlugin from "./plugins/assets/index.js"
|
|
13
|
+
import vowelDeployCloudflarePlugin from "./plugins/deploy-cloudflare/index.js"
|
|
10
14
|
import vowelWriteHTMLPlugin from "./plugins/html/index.js"
|
|
15
|
+
import vowelAtprotoPlugin from "./plugins/atproto/index.js"
|
|
16
|
+
import vowelPublishPlugin from "./plugins/publish/index.js"
|
|
11
17
|
import vowelURLsPlugin from "./plugins/urls/index.js"
|
|
12
18
|
|
|
13
19
|
/** @import {VotiveConfig} from "votive" */
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Vowel's routing cascade: secret segments hashed, then the whole path
|
|
23
|
+
* lowercased. Votive stores a routed path verbatim, and only the
|
|
24
|
+
* markdown router lowercases its own output - images, fonts, styles and
|
|
25
|
+
* vectors pass their names through - so this is the one place that
|
|
26
|
+
* makes every vowel url lowercase. After the hash, not before: the
|
|
27
|
+
* hash input is the path as the author typed it (secretPaths.js pins
|
|
28
|
+
* the values), and a hex digest is lowercase already.
|
|
29
|
+
* @param {string} sourcePath
|
|
30
|
+
*/
|
|
31
|
+
function vowelRouter(sourcePath) {
|
|
32
|
+
return secretRouter(sourcePath).toLowerCase()
|
|
33
|
+
}
|
|
16
34
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
const defaultPlugins = [
|
|
36
|
+
vowelReadMarkdownPlugin,
|
|
37
|
+
vowelImagesPlugin,
|
|
38
|
+
vowelStylesPlugin,
|
|
39
|
+
vowelWriteRobotsPlugin,
|
|
40
|
+
vowelWriteXMLPlugin,
|
|
41
|
+
vowelVectorPlugin,
|
|
42
|
+
vowelFontsPlugin,
|
|
43
|
+
vowelAssetsPlugin,
|
|
44
|
+
vowelDeployCloudflarePlugin,
|
|
45
|
+
vowelURLsPlugin,
|
|
46
|
+
vowelAtprotoPlugin,
|
|
47
|
+
vowelPublishPlugin,
|
|
48
|
+
vowelWriteHTMLPlugin
|
|
49
|
+
]
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Builds a real config for a given project folder - a function, not a
|
|
53
|
+
* module-level constant, because the folder isn't known until a caller
|
|
54
|
+
* supplies one. The old shape (a static `export const config` built from
|
|
55
|
+
* a hardcoded `sourceFolder = "."`) only worked for the CLI, where cwd
|
|
56
|
+
* genuinely is the project folder; an embedder (vowel-desktop, a future
|
|
57
|
+
* host) has its own folder that has nothing to do with cwd, and needs
|
|
58
|
+
* its own targetFolder/databasePath/cacheDirectory to follow it - not a
|
|
59
|
+
* value baked in at `import vowel from "vowel"` time. See
|
|
60
|
+
* tasks/desktop-app-architecture.md.
|
|
61
|
+
*
|
|
62
|
+
* Vowel targets non-developer end users (via whatever app embeds it),
|
|
63
|
+
* who could be confused by an `output`/.votive.db/.cache showing up in
|
|
64
|
+
* their project folder - keep all three in an OS-appropriate system
|
|
65
|
+
* directory instead, keyed per-project so multiple Vowel sites never
|
|
66
|
+
* collide.
|
|
67
|
+
*
|
|
68
|
+
* `overrides.plugins`, if given, is *added* to vowel's default plugin
|
|
69
|
+
* list rather than replacing it - an embedder registering its own
|
|
70
|
+
* native-only commands (a folder picker, "reveal in Finder") shouldn't
|
|
71
|
+
* have to reimplement markdown/html/images/etc. support just to add one
|
|
72
|
+
* plugin. Override any of targetFolder/databasePath/cacheDirectory via
|
|
73
|
+
* `overrides` directly if you want them back in the project directory
|
|
74
|
+
* (or somewhere else entirely).
|
|
75
|
+
* @param {string} [sourceFolder] - defaults to the current working
|
|
76
|
+
* directory, correct for the CLI. An embedder should always pass an
|
|
77
|
+
* explicit, real folder (e.g. from a native folder-picker) - cwd has
|
|
78
|
+
* no meaningful relationship to "the user's project" there.
|
|
79
|
+
* @param {Partial<VotiveConfig> & { plugins?: VotiveConfig["plugins"] }} [overrides]
|
|
80
|
+
* @returns {VotiveConfig}
|
|
81
|
+
*/
|
|
82
|
+
function createConfig(sourceFolder = ".", overrides = {}) {
|
|
83
|
+
const resolvedSourceFolder = resolveProjectFolder(sourceFolder)
|
|
84
|
+
const systemDirectory = systemDirectoryFor(resolvedSourceFolder)
|
|
85
|
+
const { plugins: extraPlugins, ...rest } = overrides
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
sourceFolder: resolvedSourceFolder,
|
|
89
|
+
// Applied before every processor's own router, so a secret folder
|
|
90
|
+
// hides its images and fonts too - not just its pages. See
|
|
91
|
+
// secretPaths.js for why this cannot live in a processor.
|
|
92
|
+
router: vowelRouter,
|
|
93
|
+
// Fetched link previews live in the project, one YAML file per host,
|
|
94
|
+
// so they survive a cache wipe, travel with the project, and reach
|
|
95
|
+
// coworkers through the repo. Visible rather than votive's hidden
|
|
96
|
+
// default, because the author is meant to find and edit them: the
|
|
97
|
+
// file is what the site shows. `$` keeps it out of routing, and
|
|
98
|
+
// votive keeps it out of the source scan.
|
|
99
|
+
urlStore: path.join(resolvedSourceFolder, "$links"),
|
|
100
|
+
targetFolder: path.join(systemDirectory, "output"),
|
|
101
|
+
databasePath: path.join(systemDirectory, ".votive.db"),
|
|
102
|
+
cacheDirectory: path.join(systemDirectory, ".cache"),
|
|
103
|
+
plugins: [...defaultPlugins, ...(extraPlugins ?? [])],
|
|
104
|
+
...rest
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { createConfig }
|
package/extractDate.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
+
const MONTH_INDEXES = {
|
|
2
|
+
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
|
|
3
|
+
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Months arrive either as a name ("January", "Sept") or as a number.
|
|
8
|
+
* @param {string} month
|
|
9
|
+
*/
|
|
10
|
+
function monthIndex(month) {
|
|
11
|
+
const named = MONTH_INDEXES[String(month).slice(0, 3).toLowerCase()]
|
|
12
|
+
if (named !== undefined) return named
|
|
13
|
+
return Number(month) - 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Deliberately unanchored: a caller may want the date out of a longer
|
|
17
|
+
// string. Use dateSpan() to tell "this text *is* a date" from "this text
|
|
18
|
+
// mentions one".
|
|
19
|
+
const DATE_PATTERN = /(?:\b(?<first_segment>(?<first_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<first_YYYY>\d\d\d\d)|(?<first_do>(?:\d|[0-3]\d)(?:st|nd|rd|th))|(?<first_dd_or_MM_or_yy>[0-3]\d)|(?<first_d_or_M>\d))(?<first_delimiter>\.| |, |\/|-)?)(?:\b(?<second_segment>(?<second_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<second_YYYY>\d\d\d\d)|(?<second_do>(\d\d)(:?st|nd|rd|th))|(?<second_dd_or_MM_or_yy>[0-3]\d)|(?<second_d_or_M>\d))(?<second_delimiter>\.| |, |\/|-)?)(?:\b(?<third_segment>(?<third_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<third_YYYY>\d\d\d\d)|(?<third_do>(\d\d)(?:st|nd|rd|th))|(?<third_dd_or_MM_or_yy>[0-3]\d)|(?<third_d_or_M>\d)))(?<time>(?<time_delimiter>T|\ | at )(?<H_or_HH>[0-2]?\d):(?<MM>[0-6]\d)(?::(?<SS>00))?(?:\.(?<SSS>\d\d\d))?(?<Z>Z|(?:\+(\d\d):(\d\d)))?(?<meridiam_indicator>am|pm)?)?/i
|
|
20
|
+
|
|
1
21
|
/**
|
|
22
|
+
* The part of `text` that looks like a date, or undefined.
|
|
2
23
|
* @param {string} text
|
|
3
24
|
*/
|
|
4
|
-
function
|
|
5
|
-
|
|
25
|
+
function dateSpan(text) {
|
|
26
|
+
return DATE_PATTERN.exec(text)?.[0]
|
|
27
|
+
}
|
|
6
28
|
|
|
7
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} text
|
|
31
|
+
*/
|
|
32
|
+
function extractDate(text) {
|
|
33
|
+
const dateMatch = DATE_PATTERN.exec(text)
|
|
8
34
|
if(!dateMatch) return
|
|
9
35
|
|
|
10
36
|
const data = {
|
|
@@ -73,11 +99,49 @@ function extractDate(text) {
|
|
|
73
99
|
data.timezone = groups.Z
|
|
74
100
|
}
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
102
|
+
// A bare "YYYY M D" string is parsed as local time, which shifts the
|
|
103
|
+
// date by a day in any timezone east of UTC and by an odd LMT offset
|
|
104
|
+
// before standardization (Amsterdam in the year 1000 is +00:17:30). The
|
|
105
|
+
// date an author writes has no timezone, so build it as UTC and only
|
|
106
|
+
// honour an offset when one was written explicitly.
|
|
107
|
+
const year = Number(data.year)
|
|
108
|
+
const month = monthIndex(data.month)
|
|
109
|
+
const day = parseInt(data.day, 10)
|
|
110
|
+
|
|
111
|
+
const hours = data.hours ? Number(data.hours) : 0
|
|
112
|
+
const minutes = data.minutes ? Number(data.minutes) : 0
|
|
113
|
+
const seconds = data.seconds ? Number(data.seconds) : 0
|
|
114
|
+
|
|
115
|
+
// Date.UTC rolls out-of-range parts over into neighbouring months and
|
|
116
|
+
// years, so an ISBN's segments would silently become a real date. The
|
|
117
|
+
// string parse this replaced rejected them, and callers still rely on
|
|
118
|
+
// that: the frontmatter renderer only treats a value as a date when the
|
|
119
|
+
// result is valid.
|
|
120
|
+
const partsAreSane =
|
|
121
|
+
Number.isInteger(year) &&
|
|
122
|
+
Number.isInteger(month) && month >= 0 && month <= 11 &&
|
|
123
|
+
Number.isInteger(day) && day >= 1 && day <= 31
|
|
124
|
+
|
|
125
|
+
if (!partsAreSane) return new Date(NaN)
|
|
126
|
+
|
|
127
|
+
if (data.timezone) {
|
|
128
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0")
|
|
129
|
+
const stamp = `${pad(year, 4)}-${pad(month + 1)}-${pad(day)}` +
|
|
130
|
+
`T${pad(hours)}:${pad(minutes)}:${pad(seconds)}${data.timezone}`
|
|
131
|
+
return new Date(stamp)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = new Date(Date.UTC(year, month, day, hours, minutes, seconds))
|
|
135
|
+
|
|
136
|
+
// Catches the rollover that survives the range check - 31 February.
|
|
137
|
+
const rolled =
|
|
138
|
+
result.getUTCFullYear() !== year ||
|
|
139
|
+
result.getUTCMonth() !== month ||
|
|
140
|
+
result.getUTCDate() !== day
|
|
78
141
|
|
|
79
|
-
return new Date(
|
|
142
|
+
return rolled ? new Date(NaN) : result
|
|
80
143
|
}
|
|
81
144
|
|
|
82
145
|
export default extractDate
|
|
146
|
+
export { extractDate, dateSpan }
|
|
83
147
|
|
package/index.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { startServer } from "votive"
|
|
2
|
+
import { createConfig } from "./config.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {object} options
|
|
6
6
|
* @property {"verbose" | "essential" | "silent"} logging
|
|
7
|
+
* @property {string} [sourceFolder] - defaults to the current working
|
|
8
|
+
* directory (see createConfig) - a caller with its own project folder
|
|
9
|
+
* (vowel-desktop, a future host) should always pass this explicitly.
|
|
10
|
+
* @returns {Promise<{port: number, server: import("node:http").Server, close: () => Promise<void>}>}
|
|
11
|
+
* startServer's own return, passed through. `port` is the bound port, which can
|
|
12
|
+
* differ from the one requested; `close()` releases the watchers and the
|
|
13
|
+
* listener, which a host serving more than one project over a session
|
|
14
|
+
* needs before opening the next one.
|
|
7
15
|
*/
|
|
8
|
-
async function init(options) {
|
|
9
|
-
const
|
|
16
|
+
async function init(options = {}) {
|
|
17
|
+
const { sourceFolder, ...overrides } = options
|
|
18
|
+
const config = createConfig(sourceFolder, overrides)
|
|
19
|
+
return startServer(config)
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
|
|
13
23
|
export default init
|
|
14
|
-
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vowel",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0-beta.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=26"
|
|
7
|
+
},
|
|
5
8
|
"bin": "bin.js",
|
|
6
9
|
"main": "index.js",
|
|
7
10
|
"scripts": {
|
|
8
|
-
"test": "node --test tests/*.js"
|
|
11
|
+
"test": "node --test tests/*.js",
|
|
12
|
+
"build:editor": "cd plugins/html/editor && rollup -c"
|
|
9
13
|
},
|
|
10
14
|
"keywords": [
|
|
11
15
|
"markdown",
|
|
@@ -15,22 +19,35 @@
|
|
|
15
19
|
"author": "Sam Littlefair",
|
|
16
20
|
"license": "MIT",
|
|
17
21
|
"description": "Markdown websites",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
24
|
+
"linkedom": "^0.18.13",
|
|
25
|
+
"rollup": "^4.0.0",
|
|
26
|
+
"rollup-plugin-cleanup": "^3.2.1",
|
|
27
|
+
"rollup-plugin-svelte": "^7.2.3",
|
|
28
|
+
"svelte": "^5.56.9"
|
|
29
|
+
},
|
|
18
30
|
"dependencies": {
|
|
19
31
|
"@bomb.sh/args": "^0.3.1",
|
|
20
32
|
"@bomb.sh/tab": "^0.0.15",
|
|
21
33
|
"@clack/prompts": "^1.4.0",
|
|
22
34
|
"@jsdevtools/rehype-toc": "^3.0.2",
|
|
23
35
|
"autoprefixer": "^10.4.27",
|
|
36
|
+
"colorhorse": "^0.3.1",
|
|
24
37
|
"cssnano": "^7.1.4",
|
|
38
|
+
"emojilib": "^4.0.3",
|
|
25
39
|
"encoding-sniffer": "^1.0.2",
|
|
26
40
|
"fast-xml-parser": "^5.3.6",
|
|
27
41
|
"faye-websocket": "^0.11.4",
|
|
42
|
+
"github-slugger": "^2.0.0",
|
|
28
43
|
"hast-util-from-html": "^2.0.3",
|
|
29
44
|
"hast-util-to-string": "^3.0.1",
|
|
45
|
+
"hast-util-to-text": "^4.0.2",
|
|
30
46
|
"hastscript": "^9.0.1",
|
|
31
47
|
"highlight.js": "^11.12.0",
|
|
32
48
|
"js-yaml": "^4.1.0",
|
|
33
|
-
"lightningcss": "^1.
|
|
49
|
+
"lightningcss": "^1.33.0",
|
|
50
|
+
"lowlight": "^3.3.0",
|
|
34
51
|
"mdast": "^2.3.2",
|
|
35
52
|
"mdast-normalize-headings": "^4.0.0",
|
|
36
53
|
"mdast-util-from-markdown": "^2.0.2",
|
|
@@ -51,7 +68,6 @@
|
|
|
51
68
|
"rehype": "^13.0.2",
|
|
52
69
|
"rehype-document": "^7.0.3",
|
|
53
70
|
"rehype-github-alert": "^1.0.0",
|
|
54
|
-
"rehype-highlight": "^7.0.2",
|
|
55
71
|
"rehype-parse": "^9.0.1",
|
|
56
72
|
"rehype-preset-minify": "^7.0.1",
|
|
57
73
|
"rehype-slug": "^6.0.0",
|
|
@@ -62,7 +78,9 @@
|
|
|
62
78
|
"remark-html": "^16.0.1",
|
|
63
79
|
"remark-parse": "^11.0.0",
|
|
64
80
|
"remark-rehype": "^11.1.2",
|
|
81
|
+
"rollup-plugin-import-css": "^4.2.1",
|
|
65
82
|
"sharp": "^0.35.3",
|
|
83
|
+
"svedit": "^0.14.0",
|
|
66
84
|
"unified": "^11.0.5",
|
|
67
85
|
"unist-util-find": "^3.0.0",
|
|
68
86
|
"unist-util-is": "^6.0.0",
|
|
@@ -73,12 +91,25 @@
|
|
|
73
91
|
"unist-util-visit-parents": "^6.0.0",
|
|
74
92
|
"unpic": "^4.2.2",
|
|
75
93
|
"url-metadata": "^5.4.1",
|
|
76
|
-
"
|
|
77
|
-
"votive": "^0.4.0",
|
|
94
|
+
"votive": "0.5.0-beta.0",
|
|
78
95
|
"xast-util-sitemap": "^2.0.0",
|
|
79
96
|
"xast-util-to-xml": "^4.0.0",
|
|
80
97
|
"xml": "^1.0.1",
|
|
81
98
|
"yaml": "^2.8.2",
|
|
82
99
|
"yamljs": "^0.3.0"
|
|
83
|
-
}
|
|
84
|
-
|
|
100
|
+
},
|
|
101
|
+
"files": [
|
|
102
|
+
"bin.js",
|
|
103
|
+
"cli.js",
|
|
104
|
+
"config.js",
|
|
105
|
+
"extractDate.js",
|
|
106
|
+
"index.js",
|
|
107
|
+
"secretPaths.js",
|
|
108
|
+
"systemPaths.js",
|
|
109
|
+
"utils.js",
|
|
110
|
+
"plugins",
|
|
111
|
+
"stylesheets",
|
|
112
|
+
"!plugins/html/bundle.js",
|
|
113
|
+
"!plugins/html/editorClient.js"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { statSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
/** @import * as Votive from "votive" */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Files the site publishes as they are: video, data and documents an
|
|
8
|
+
* author links to from a page. Copied through byte for byte - like a
|
|
9
|
+
* font - with one rule: **a file over the size limit is not published.**
|
|
10
|
+
* Cloudflare Pages refuses any single file over 25 MiB, and a deploy
|
|
11
|
+
* that fails on one video is worse than a build that says so up front.
|
|
12
|
+
* The read (deferred, like every buffer read) stats the source rather
|
|
13
|
+
* than reading it, logs one error naming the file and its size, and
|
|
14
|
+
* makes the target virtual, so nothing lands on disk and nothing links
|
|
15
|
+
* to it as though it were there. The limit is `config.assetSizeLimit`
|
|
16
|
+
* in bytes for a host that allows more.
|
|
17
|
+
*
|
|
18
|
+
* Per Sam (Sept 19): copy through mp4 and csv, with a reasonable file
|
|
19
|
+
* size limit. The list is what people link to from a page and expect
|
|
20
|
+
* to be there; adding an extension is adding it here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const EXTENSIONS = [".mp4", ".webm", ".csv", ".json", ".pdf"]
|
|
24
|
+
|
|
25
|
+
/** 25 MiB: Cloudflare Pages' per-file limit. */
|
|
26
|
+
const DEFAULT_SIZE_LIMIT = 25 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
/** @param {number} bytes */
|
|
29
|
+
const megabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @type {Votive.ProcessorRead}
|
|
33
|
+
*/
|
|
34
|
+
function readAsset(source, { config }) {
|
|
35
|
+
const limit = Number(config.assetSizeLimit) || DEFAULT_SIZE_LIMIT
|
|
36
|
+
const { size } = statSync(path.join(config.sourceFolder, source.path))
|
|
37
|
+
if (size <= limit) return { metadata: { size } }
|
|
38
|
+
|
|
39
|
+
config.log?.("error", `${source.path} is ${megabytes(size)}; files over ${megabytes(limit)} are not published (Cloudflare Pages refuses them). Host it elsewhere and link to it.`)
|
|
40
|
+
return { metadata: { size, oversized: true }, write: false }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @type {Votive.ProcessorWrite} */
|
|
44
|
+
function writeAsset(target) {
|
|
45
|
+
if (target.metadata?.oversized) return undefined
|
|
46
|
+
return { data: target.buffer() }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @type {Votive.VotiveProcessor} */
|
|
50
|
+
const assetsProcessor = {
|
|
51
|
+
format: "buffer",
|
|
52
|
+
extensions: EXTENSIONS,
|
|
53
|
+
router: ({ name, dir, ext }) => ({ name, dir, ext }),
|
|
54
|
+
readFile: readAsset,
|
|
55
|
+
writeFile: writeAsset
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @type {Votive.VotivePlugin} */
|
|
59
|
+
const vowelAssetsPlugin = {
|
|
60
|
+
name: "vowel-assets",
|
|
61
|
+
processors: [assetsProcessor]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default vowelAssetsPlugin
|
|
65
|
+
export { EXTENSIONS, DEFAULT_SIZE_LIMIT }
|