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.
Files changed (166) hide show
  1. package/README.md +1 -1
  2. package/bin.js +47 -27
  3. package/cli.js +15 -0
  4. package/config.js +93 -26
  5. package/extractDate.js +70 -6
  6. package/index.js +14 -5
  7. package/package.json +39 -8
  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 +30 -16
  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 +31 -5
  53. package/plugins/html/index.js +430 -458
  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 +276 -334
  64. package/plugins/markdown/links.js +97 -0
  65. package/plugins/markdown/metadata.js +229 -68
  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 -18
  74. package/plugins/styles/theme.js +116 -0
  75. package/plugins/urls/index.js +49 -62
  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 +105 -43
  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 +36 -4
  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/plugins/markdown/robots.js +0 -18
  165. package/regex.js +0 -36
  166. 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 [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
 
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"
22
+ import { shouldClear } from "./cli.js"
17
23
 
18
- /** @param {boolean} verbose */
19
- async function removeCache(verbose) {
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", "cache cleared")}`)
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
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()
@@ -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 voot's runCommand/handleCommand) -
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("voot")
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
- await removeCache(args.logging === "verbose")
236
- await removeDB(args.logging === "verbose")
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
- 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) })
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 { systemDirectoryFor } from "votive/internals"
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
- const sourceFolder = "."
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
- // Vowel targets non-developer end users (via whatever app embeds it),
18
- // who could be confused by an `output`/.votive.db/.cache showing up in
19
- // their project folder - keep all three in an OS-appropriate system
20
- // directory instead, keyed per-project so multiple Vowel sites never
21
- // collide. Override any of the three below if you want them back in
22
- // the project directory (or somewhere else entirely).
23
- const systemDirectory = systemDirectoryFor(sourceFolder)
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
- export const config = {
26
- sourceFolder,
27
- targetFolder: path.join(systemDirectory, "output"),
28
- databasePath: path.join(systemDirectory, ".votive.db"),
29
- cacheDirectory: path.join(systemDirectory, ".cache"),
30
- plugins: [
31
- vowelReadMarkdownPlugin,
32
- vowelImagesPlugin,
33
- vowelStylesPlugin,
34
- vowelWriteRobotsPlugin,
35
- vowelWriteXMLPlugin,
36
- vowelVectorPlugin,
37
- vowelFontsPlugin,
38
- vowelURLsPlugin,
39
- vowelWriteHTMLPlugin
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 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
-
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "vowel",
3
3
  "type": "module",
4
- "version": "0.6.4",
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.32.0",
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
- "voot": "^0.1.0",
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 }