vowel 0.6.3 → 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 +23 -17
- package/bin.js +87 -30
- package/cli.js +15 -0
- package/config.js +102 -16
- package/extractDate.js +70 -6
- package/index.js +14 -5
- package/package.json +42 -7
- 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 +92 -0
- 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 +104 -0
- package/plugins/html/index.js +760 -0
- 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 +287 -1274
- package/plugins/markdown/links.js +97 -0
- package/plugins/markdown/metadata.js +308 -0
- 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 -26
- package/plugins/styles/theme.js +116 -0
- package/plugins/urls/index.js +78 -0
- 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 +123 -50
- package/secretPaths.js +170 -0
- package/stylesheets/DefaultStyles.css +367 -437
- package/stylesheets/ResetStyles.css +2 -38
- package/stylesheets/SyntaxHighlightingStyles.css +269 -0
- 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/regex.js +0 -36
- package/stylesheets/brand-colors.css +0 -119
package/README.md
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
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
|
|
|
10
10
|
## Roadmap
|
|
11
11
|
|
|
12
|
+
### Priorities
|
|
13
|
+
|
|
14
|
+
- [ ] Create CONTRIBUTING.md
|
|
15
|
+
- [ ]
|
|
16
|
+
|
|
12
17
|
### Launch checklist
|
|
13
18
|
|
|
14
19
|
- [x] CLI
|
|
@@ -17,14 +22,15 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
17
22
|
- [x] Route patterns
|
|
18
23
|
- [x] Post list metadata
|
|
19
24
|
- [.] Transaction management
|
|
25
|
+
- [ ] Default 404 pages
|
|
20
26
|
|
|
21
27
|
- [.] Fixes
|
|
22
28
|
- [x] Global nav sublinks
|
|
23
29
|
- [x] Hot reload
|
|
24
|
-
- [
|
|
30
|
+
- [x] Explicit asset handling
|
|
25
31
|
- [.] Change "job" to "resources"
|
|
26
32
|
- [.] ReadResource types
|
|
27
|
-
- [
|
|
33
|
+
- [x] Processor utilities param
|
|
28
34
|
- [.] Delete old SQLite logic
|
|
29
35
|
- [.] Make metadata explicit in SQLiteTarget type
|
|
30
36
|
- [x] Add SQLite begin/commit logic
|
|
@@ -35,12 +41,12 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
35
41
|
- [x] Homepage in global nav
|
|
36
42
|
- [x] Metadata in header
|
|
37
43
|
- [x] Page list sort
|
|
38
|
-
- [
|
|
44
|
+
- [x] Limit in sort
|
|
39
45
|
- [x] Metafiles
|
|
40
46
|
- [x] Robots.txt
|
|
41
47
|
- [x] Sitemap.xml
|
|
42
48
|
- [x] RSS
|
|
43
|
-
- [
|
|
49
|
+
- [x] 404.html (just make 404.md)
|
|
44
50
|
- [ ] Markup
|
|
45
51
|
- [x] Image as figure
|
|
46
52
|
- [x] Admonitions
|
|
@@ -49,10 +55,11 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
49
55
|
- [ ] Mermaid
|
|
50
56
|
- [x] Slogan
|
|
51
57
|
- [x] TOC
|
|
58
|
+
- [x] ==mark==
|
|
52
59
|
- [ ] Non-urgent fixes / improvements
|
|
53
60
|
- [ ] Compare metadata on file update
|
|
54
61
|
- [x] Prune/cleanup step
|
|
55
|
-
- [
|
|
62
|
+
- [x] Dependency type (file, settings, folder)
|
|
56
63
|
- [ ] New systems
|
|
57
64
|
- [x] Tags
|
|
58
65
|
- [ ] External links
|
|
@@ -68,7 +75,7 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
68
75
|
- [ ] GetMany "extension" filter
|
|
69
76
|
- [ ] Delete settings logic
|
|
70
77
|
- [ ] Update settings logic
|
|
71
|
-
- [
|
|
78
|
+
- [x] Logical operators for filters
|
|
72
79
|
- [ ] Customize menus
|
|
73
80
|
- [ ] More file handling plugins
|
|
74
81
|
- [ ] Fonts
|
|
@@ -91,21 +98,20 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
91
98
|
- [ ] Styles
|
|
92
99
|
- [x] .css
|
|
93
100
|
- [x] Bundling
|
|
94
|
-
- [
|
|
95
|
-
- [
|
|
96
|
-
- [
|
|
101
|
+
- [x] PDFs
|
|
102
|
+
- [x] Videos
|
|
103
|
+
- [x] Arbitrary data
|
|
97
104
|
- [x] Add homepage to global nav
|
|
98
105
|
- [x] Page lists
|
|
99
106
|
- [x] Dates
|
|
100
107
|
- [x] robots.txt
|
|
101
108
|
- [x] sitemap.xml
|
|
102
|
-
- [
|
|
109
|
+
- [x] 404.html
|
|
103
110
|
- [ ] Better URL normalization (see notes)
|
|
104
111
|
- [x] Name casing
|
|
105
112
|
- [x] TUI
|
|
106
113
|
- [x] Create settings.md
|
|
107
114
|
- [x] Site title
|
|
108
|
-
- [x] Domain
|
|
109
115
|
- [ ] Webmentions
|
|
110
116
|
- [x] Logo
|
|
111
117
|
- [x] Wordmark
|
|
@@ -121,7 +127,7 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
121
127
|
- [x] Custom CSS
|
|
122
128
|
- [x] styles.css
|
|
123
129
|
- [ ] Any CSS file
|
|
124
|
-
- [
|
|
130
|
+
- [x] Tags
|
|
125
131
|
- [ ] Date format settings
|
|
126
132
|
- [ ] ::mark::
|
|
127
133
|
- [x] Infer images
|
|
@@ -136,7 +142,7 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
136
142
|
- [ ] Handle external links
|
|
137
143
|
- [x] Admonitions
|
|
138
144
|
- [ ] Use hgroup for site title, page title, etc
|
|
139
|
-
- [
|
|
145
|
+
- [x] Images as `<figure>`
|
|
140
146
|
- [ ] Hidden routes
|
|
141
147
|
- [ ] Frontmatter settings
|
|
142
148
|
- [ ] HTML
|
|
@@ -156,8 +162,8 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
156
162
|
- [ ] Image optimization (unpic)
|
|
157
163
|
- [ ] [SVG by mask](https://pqina.nl/blog/set-svg-background-image-fill-color/) and [CSS icons](https://antfu.me/posts/icons-in-pure-css)
|
|
158
164
|
- [ ] WYSIWYG editor
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
165
|
+
- [x] Better signals
|
|
166
|
+
- [x] File-written callback
|
|
161
167
|
- [ ] Themes
|
|
162
168
|
- [ ] Deploy
|
|
163
169
|
- [ ] Cloudflare pages
|
|
@@ -173,7 +179,7 @@ See the docs: [vowel.cc](https://vowel.cc).
|
|
|
173
179
|
- [ ] Image
|
|
174
180
|
- [ ] URL
|
|
175
181
|
- [ ] Date
|
|
176
|
-
- [
|
|
182
|
+
- [x] TOC
|
|
177
183
|
- [ ] Versioning/publishing script
|
|
178
184
|
- [ ] Verify all element types from Obsidian
|
|
179
185
|
- [ ] Order/position/kanban
|
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"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
import { shouldClear } from "./cli.js"
|
|
23
|
+
|
|
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
|
-
await fs.rm(
|
|
22
|
-
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "
|
|
34
|
+
await fs.rm(config.databasePath)
|
|
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
|
-
await fs.rm(
|
|
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()
|
|
@@ -190,23 +203,67 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Runs one registered command directly and exits - no dev server, no
|
|
208
|
+
* wizard, no cache-wiping. Reuses the exact handler a live "Publish"
|
|
209
|
+
* button would trigger over WS (see votive's runCommand/handleCommand) -
|
|
210
|
+
* this is just the other invocation path for the same registered
|
|
211
|
+
* function, meant for CI/automated use (e.g. `vowel --command deploy`).
|
|
212
|
+
* @param {string} name
|
|
213
|
+
* @param {string} [payloadJSON]
|
|
214
|
+
*/
|
|
215
|
+
async function runCLICommand(name, payloadJSON) {
|
|
216
|
+
const { runCommand } = await import("votive")
|
|
217
|
+
|
|
218
|
+
let payload
|
|
219
|
+
try {
|
|
220
|
+
payload = payloadJSON ? JSON.parse(payloadJSON) : undefined
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error(`--payload must be valid JSON: ${e.message}`)
|
|
223
|
+
process.exitCode = 1
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const data = await runCommand(config, name, payload, message => {
|
|
229
|
+
console.info(message?.message ?? message)
|
|
230
|
+
})
|
|
231
|
+
if (data !== undefined) console.info(JSON.stringify(data))
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.error(e?.message || e)
|
|
234
|
+
process.exitCode = 1
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
193
238
|
async function main() {
|
|
194
239
|
const args = parse(process.argv.slice(2))
|
|
195
240
|
|
|
196
|
-
|
|
241
|
+
if (args.command) {
|
|
242
|
+
await runCLICommand(args.command, args.payload)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const dbExists = await exists(config.databasePath)
|
|
197
247
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
248
|
+
if (shouldClear(args)) {
|
|
249
|
+
await removeDatabase(args.logging === "verbose")
|
|
250
|
+
await removeOutput(args.logging === "verbose")
|
|
251
|
+
}
|
|
252
|
+
await fs.mkdir(config.targetFolder, { recursive: true })
|
|
201
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.
|
|
202
257
|
if (!args.skip && !dbExists) {
|
|
203
|
-
const loading = votive({ ...config, verbose: false })
|
|
258
|
+
// const loading = votive({ ...config, verbose: false })
|
|
204
259
|
await wizard()
|
|
205
|
-
await (loading)
|
|
260
|
+
// await (loading)
|
|
206
261
|
}
|
|
207
262
|
|
|
208
263
|
|
|
209
|
-
|
|
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) })
|
|
210
267
|
}
|
|
211
268
|
|
|
212
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,22 +1,108 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { resolveProjectFolder } from "votive"
|
|
3
|
+
import { systemDirectoryFor } from "./systemPaths.js"
|
|
4
|
+
import { secretRouter } from "./secretPaths.js"
|
|
1
5
|
import vowelImagesPlugin from "./plugins/images/index.js"
|
|
2
6
|
import vowelStylesPlugin from "./plugins/styles/index.js"
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
7
|
+
import vowelReadMarkdownPlugin from "./plugins/markdown/index.js"
|
|
8
|
+
import vowelWriteRobotsPlugin from "./plugins/robots/index.js"
|
|
9
|
+
import vowelWriteXMLPlugin from "./plugins/xml/index.js"
|
|
6
10
|
import vowelVectorPlugin from "./plugins/vectors/index.js"
|
|
7
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"
|
|
14
|
+
import vowelWriteHTMLPlugin from "./plugins/html/index.js"
|
|
15
|
+
import vowelAtprotoPlugin from "./plugins/atproto/index.js"
|
|
16
|
+
import vowelPublishPlugin from "./plugins/publish/index.js"
|
|
17
|
+
import vowelURLsPlugin from "./plugins/urls/index.js"
|
|
8
18
|
|
|
9
19
|
/** @import {VotiveConfig} from "votive" */
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
|
|
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
|
+
}
|
|
34
|
+
|
|
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
|
+
]
|
|
50
|
+
|
|
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
|
-
|