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.
Files changed (165) hide show
  1. package/README.md +23 -17
  2. package/bin.js +87 -30
  3. package/cli.js +15 -0
  4. package/config.js +102 -16
  5. package/extractDate.js +70 -6
  6. package/index.js +14 -5
  7. package/package.json +42 -7
  8. package/plugins/assets/index.js +65 -0
  9. package/plugins/atproto/index.js +132 -0
  10. package/plugins/atproto/password.js +71 -0
  11. package/plugins/atproto/publish.js +406 -0
  12. package/plugins/atproto/record.js +144 -0
  13. package/plugins/atproto/rkey.js +54 -0
  14. package/plugins/deploy-cloudflare/index.js +92 -0
  15. package/plugins/fonts/index.js +18 -21
  16. package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
  17. package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
  18. package/plugins/html/bundle/index.js +21145 -0
  19. package/plugins/html/editor/EditButton.svelte +152 -0
  20. package/plugins/html/editor/Editor.svelte +51 -0
  21. package/plugins/html/editor/Overlays.svelte +6 -0
  22. package/plugins/html/editor/SettingsPanel.svelte +397 -0
  23. package/plugins/html/editor/Toolbar.svelte +644 -0
  24. package/plugins/html/editor/create-session.js +250 -0
  25. package/plugins/html/editor/directives.js +128 -0
  26. package/plugins/html/editor/frontmatter.js +97 -0
  27. package/plugins/html/editor/ingest.js +339 -0
  28. package/plugins/html/editor/main.js +94 -0
  29. package/plugins/html/editor/nodes/Alert.svelte +21 -0
  30. package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
  31. package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
  32. package/plugins/html/editor/nodes/Embed.svelte +19 -0
  33. package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
  34. package/plugins/html/editor/nodes/Heading.svelte +15 -0
  35. package/plugins/html/editor/nodes/Highlight.svelte +11 -0
  36. package/plugins/html/editor/nodes/Image.svelte +18 -0
  37. package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
  38. package/plugins/html/editor/nodes/Link.svelte +17 -0
  39. package/plugins/html/editor/nodes/List.svelte +15 -0
  40. package/plugins/html/editor/nodes/ListItem.svelte +9 -0
  41. package/plugins/html/editor/nodes/Page.svelte +21 -0
  42. package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
  43. package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
  44. package/plugins/html/editor/nodes/Strong.svelte +11 -0
  45. package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
  46. package/plugins/html/editor/nodes/index.js +18 -0
  47. package/plugins/html/editor/rollup.config.js +35 -0
  48. package/plugins/html/editor/serialize.js +157 -0
  49. package/plugins/html/editor/settings-file.js +83 -0
  50. package/plugins/html/editor/socket.js +202 -0
  51. package/plugins/html/editor/source.js +43 -0
  52. package/plugins/html/image.js +104 -0
  53. package/plugins/html/index.js +760 -0
  54. package/plugins/html/partials.js +299 -0
  55. package/plugins/html/socialLinks.js +67 -0
  56. package/plugins/html/writeRules.js +377 -0
  57. package/plugins/icons/index.js +2 -6
  58. package/plugins/images/index.js +78 -55
  59. package/plugins/markdown/brandColors.js +10 -0
  60. package/plugins/markdown/colorScheme.js +64 -0
  61. package/plugins/markdown/emoji.js +30 -0
  62. package/plugins/markdown/frontmatter.js +253 -0
  63. package/plugins/markdown/index.js +287 -1274
  64. package/plugins/markdown/links.js +97 -0
  65. package/plugins/markdown/metadata.js +308 -0
  66. package/plugins/markdown/readRules.js +143 -0
  67. package/plugins/markdown/typography.js +109 -0
  68. package/plugins/markdown/visitor.js +143 -0
  69. package/plugins/publish/index.js +34 -0
  70. package/plugins/robots/agents.js +79 -0
  71. package/plugins/robots/index.js +30 -10
  72. package/plugins/robots/robots.js +48 -0
  73. package/plugins/styles/index.js +111 -26
  74. package/plugins/styles/theme.js +116 -0
  75. package/plugins/urls/index.js +78 -0
  76. package/plugins/urls/urlMetadata.js +246 -0
  77. package/plugins/vectors/index.js +5 -9
  78. package/plugins/xml/entryContent.js +103 -0
  79. package/plugins/xml/index.js +123 -50
  80. package/secretPaths.js +170 -0
  81. package/stylesheets/DefaultStyles.css +367 -437
  82. package/stylesheets/ResetStyles.css +2 -38
  83. package/stylesheets/SyntaxHighlightingStyles.css +269 -0
  84. package/stylesheets/TypographyStyles.css +386 -128
  85. package/systemPaths.js +48 -0
  86. package/utils.js +56 -5
  87. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
  88. package/.github/ISSUE_TEMPLATE/config.yml +0 -7
  89. package/docs-source/.cache.json +0 -9
  90. package/docs-source/.obsidian/app.json +0 -3
  91. package/docs-source/.obsidian/appearance.json +0 -3
  92. package/docs-source/.obsidian/core-plugins-migration.json +0 -30
  93. package/docs-source/.obsidian/core-plugins.json +0 -20
  94. package/docs-source/.obsidian/workspace.json +0 -168
  95. package/docs-source/.stackblitzrc +0 -3
  96. package/docs-source/.votive.db +0 -0
  97. package/docs-source/about.md +0 -3
  98. package/docs-source/blog/home.md +0 -5
  99. package/docs-source/blog/url-ui.md +0 -21
  100. package/docs-source/blog.md +0 -5
  101. package/docs-source/docs/.votive.db +0 -0
  102. package/docs-source/docs/deploy.md +0 -67
  103. package/docs-source/docs/file-structure.md +0 -31
  104. package/docs-source/docs/folder-settings.md +0 -23
  105. package/docs-source/docs/home.md +0 -55
  106. package/docs-source/docs/images.md +0 -10
  107. package/docs-source/docs/items.md +0 -13
  108. package/docs-source/docs/pages.md +0 -141
  109. package/docs-source/docs/settings.md +0 -4
  110. package/docs-source/docs/styling.md +0 -34
  111. package/docs-source/docs/taxonomies.md +0 -37
  112. package/docs-source/features/cards.md +0 -7
  113. package/docs-source/features/editing.md +0 -7
  114. package/docs-source/features/emoji.md +0 -7
  115. package/docs-source/features/frontmatter.md +0 -7
  116. package/docs-source/features/lists.md +0 -7
  117. package/docs-source/features/navigation.md +0 -7
  118. package/docs-source/features/rich-previews.md +0 -7
  119. package/docs-source/features/robots.md +0 -7
  120. package/docs-source/features/rss.md +0 -7
  121. package/docs-source/features/sitemap.md +0 -7
  122. package/docs-source/features/speed.md +0 -7
  123. package/docs-source/features/static.md +0 -7
  124. package/docs-source/features/taxonomies.md +0 -7
  125. package/docs-source/home.md +0 -42
  126. package/docs-source/output/about.html +0 -1
  127. package/docs-source/output/blog/url-ui.html +0 -8
  128. package/docs-source/output/blog.html +0 -1
  129. package/docs-source/output/default.css +0 -1
  130. package/docs-source/output/docs/deploy.html +0 -34
  131. package/docs-source/output/docs/file-structure.html +0 -24
  132. package/docs-source/output/docs/folder-settings.html +0 -20
  133. package/docs-source/output/docs/images.html +0 -2
  134. package/docs-source/output/docs/items.html +0 -6
  135. package/docs-source/output/docs/pages.html +0 -101
  136. package/docs-source/output/docs/styling.html +0 -18
  137. package/docs-source/output/docs/taxonomies.html +0 -20
  138. package/docs-source/output/docs.html +0 -28
  139. package/docs-source/output/features/cards.html +0 -1
  140. package/docs-source/output/features/editing.html +0 -1
  141. package/docs-source/output/features/emoji.html +0 -1
  142. package/docs-source/output/features/frontmatter.html +0 -1
  143. package/docs-source/output/features/lists.html +0 -1
  144. package/docs-source/output/features/navigation.html +0 -1
  145. package/docs-source/output/features/rich-previews.html +0 -1
  146. package/docs-source/output/features/robots.html +0 -1
  147. package/docs-source/output/features/rss.html +0 -1
  148. package/docs-source/output/features/sitemap.html +0 -1
  149. package/docs-source/output/features/speed.html +0 -1
  150. package/docs-source/output/features/static.html +0 -1
  151. package/docs-source/output/features/taxonomies.html +0 -1
  152. package/docs-source/output/features.html +0 -1
  153. package/docs-source/output/feed.xml +0 -1
  154. package/docs-source/output/index.html +0 -21
  155. package/docs-source/output/reset.css +0 -1
  156. package/docs-source/output/roadmap.html +0 -87
  157. package/docs-source/output/robots.txt +0 -14
  158. package/docs-source/output/sitemap.xml +0 -16
  159. package/docs-source/output/typography.css +0 -1
  160. package/docs-source/roadmap.md +0 -98
  161. package/docs-source/settings.md +0 -13
  162. package/getMetadata.js +0 -41
  163. package/jsconfig.json +0 -10
  164. package/regex.js +0 -36
  165. 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 [Voot](https://github.com/samlfair/voot)
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
- - [.] Explicit asset handling
30
+ - [x] Explicit asset handling
25
31
  - [.] Change "job" to "resources"
26
32
  - [.] ReadResource types
27
- - [.] Processor utilities param
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
- - [.] Limit in sort
44
+ - [x] Limit in sort
39
45
  - [x] Metafiles
40
46
  - [x] Robots.txt
41
47
  - [x] Sitemap.xml
42
48
  - [x] RSS
43
- - [ ] 404.html (just make 404.md)
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
- - [ ] Dependency type (file, settings, folder)
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
- - [ ] Logical operators for filters
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
- - [ ] PDFs
95
- - [ ] Videos
96
- - [ ] Arbitrary data
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
- - [ ] 404.html
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
- - [ ] Tags
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
- - [ ] Images as `<figure>`
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
- - [ ] Better signals
160
- - [ ] File-written callback
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
- - [ ] TOC
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 { config } from "./config.js"
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
- /** @param {boolean} verbose */
19
- async function removeCache(verbose) {
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("./.votive.db")
22
- if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "cache cleared")}`)
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 cache found")}`)
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 removeDB(verbose) {
42
+ async function removeOutput(verbose) {
30
43
  try {
31
- await fs.rm("./output", { recursive: true, force: true })
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 cache found")}`)
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 websiteTitle = await text({
78
- message: "Website title",
92
+ const websiteName = await text({
93
+ message: "Website name",
79
94
  placeholder: "My Cool Website"
80
95
  })
81
96
 
82
- frontmatter.push(`title: ${websiteTitle}`)
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
- await fs.writeFile("home.md", `# Home\n\nWelcome home!`, "utf-8")
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
- const dbExists = await exists(".votive.db")
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
- await removeCache(args.logging === "verbose")
199
- await removeDB(args.logging === "verbose")
200
- await fs.mkdir(config.destinationFolder, { recursive: true })
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
- init(args)
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 vowelMarkdownPlugin from "./plugins/markdown/index.js"
4
- import vowelRobotsPlugin from "./plugins/robots/index.js"
5
- import vowelXMLPlugin from "./plugins/xml/index.js"
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
- export const config = {
11
- sourceFolder: ".",
12
- destinationFolder: "output",
13
- plugins: [
14
- vowelMarkdownPlugin,
15
- vowelImagesPlugin,
16
- vowelStylesPlugin,
17
- vowelRobotsPlugin,
18
- vowelXMLPlugin,
19
- vowelVectorPlugin,
20
- vowelFontsPlugin
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 extractDate(text) {
5
- const dateRegex = /(?:\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
25
+ function dateSpan(text) {
26
+ return DATE_PATTERN.exec(text)?.[0]
27
+ }
6
28
 
7
- const dateMatch = dateRegex.exec(text)
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
- const time = groups.time ? `${data.hours}:${data.minutes}:${data.seconds}${data.timezone || ""}` : null
77
- const dateString = `${data.year} ${data.month} ${data.day}${time ? " " + time : ""}`
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(dateString)
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 voot from "voot"
2
- import { config } from "./config.js"
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 cache = await voot({ ...config, ...options })
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
-