vowel 0.6.2 → 0.6.4

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 CHANGED
@@ -9,6 +9,11 @@ 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,26 +41,27 @@ 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
- - [.] Image as figure
51
+ - [x] Image as figure
46
52
  - [x] Admonitions
47
53
  - [x] Heading anchors
48
54
  - [ ] GFM emoji
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
- - [ ] Tags
64
+ - [x] Tags
58
65
  - [ ] External links
59
66
  - [ ] Syntax highlighting
60
67
  - [ ] Social 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
@@ -18,7 +18,7 @@ import init from "./index.js"
18
18
  /** @param {boolean} verbose */
19
19
  async function removeCache(verbose) {
20
20
  try {
21
- await fs.rm("./.votive.db")
21
+ await fs.rm(config.databasePath)
22
22
  if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "cache cleared")}`)
23
23
  } catch (e) {
24
24
  if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "no database cache found")}`)
@@ -28,7 +28,7 @@ async function removeCache(verbose) {
28
28
  /** @param {boolean} verbose */
29
29
  async function removeDB(verbose) {
30
30
  try {
31
- await fs.rm("./output", { recursive: true, force: true })
31
+ await fs.rm(config.targetFolder, { recursive: true, force: true })
32
32
  if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "output cleared")}`)
33
33
  } catch (e) {
34
34
  if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", "no output cache found")}`)
@@ -190,14 +190,51 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
190
190
  }
191
191
 
192
192
 
193
+ /**
194
+ * Runs one registered command directly and exits - no dev server, no
195
+ * wizard, no cache-wiping. Reuses the exact handler a live "Publish"
196
+ * button would trigger over WS (see voot's runCommand/handleCommand) -
197
+ * this is just the other invocation path for the same registered
198
+ * function, meant for CI/automated use (e.g. `vowel --command deploy`).
199
+ * @param {string} name
200
+ * @param {string} [payloadJSON]
201
+ */
202
+ async function runCLICommand(name, payloadJSON) {
203
+ const { runCommand } = await import("voot")
204
+
205
+ let payload
206
+ try {
207
+ payload = payloadJSON ? JSON.parse(payloadJSON) : undefined
208
+ } catch (e) {
209
+ console.error(`--payload must be valid JSON: ${e.message}`)
210
+ process.exitCode = 1
211
+ return
212
+ }
213
+
214
+ try {
215
+ const data = await runCommand(config, name, payload, message => {
216
+ console.info(message?.message ?? message)
217
+ })
218
+ if (data !== undefined) console.info(JSON.stringify(data))
219
+ } catch (e) {
220
+ console.error(e?.message || e)
221
+ process.exitCode = 1
222
+ }
223
+ }
224
+
193
225
  async function main() {
194
226
  const args = parse(process.argv.slice(2))
195
227
 
196
- const dbExists = await exists(".votive.db")
228
+ if (args.command) {
229
+ await runCLICommand(args.command, args.payload)
230
+ return
231
+ }
232
+
233
+ const dbExists = await exists(config.databasePath)
197
234
 
198
235
  await removeCache(args.logging === "verbose")
199
236
  await removeDB(args.logging === "verbose")
200
- await fs.mkdir(config.destinationFolder, { recursive: true })
237
+ await fs.mkdir(config.targetFolder, { recursive: true })
201
238
 
202
239
  if (!args.skip && !dbExists) {
203
240
  const loading = votive({ ...config, verbose: false })
package/config.js CHANGED
@@ -1,22 +1,41 @@
1
+ import path from "node:path"
2
+ import { systemDirectoryFor } from "votive/internals"
1
3
  import vowelImagesPlugin from "./plugins/images/index.js"
2
4
  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"
5
+ import vowelReadMarkdownPlugin from "./plugins/markdown/index.js"
6
+ import vowelWriteRobotsPlugin from "./plugins/robots/index.js"
7
+ import vowelWriteXMLPlugin from "./plugins/xml/index.js"
6
8
  import vowelVectorPlugin from "./plugins/vectors/index.js"
7
9
  import vowelFontsPlugin from "./plugins/fonts/index.js"
10
+ import vowelWriteHTMLPlugin from "./plugins/html/index.js"
11
+ import vowelURLsPlugin from "./plugins/urls/index.js"
8
12
 
9
13
  /** @import {VotiveConfig} from "votive" */
14
+
15
+ const sourceFolder = "."
16
+
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)
24
+
10
25
  export const config = {
11
- sourceFolder: ".",
12
- destinationFolder: "output",
26
+ sourceFolder,
27
+ targetFolder: path.join(systemDirectory, "output"),
28
+ databasePath: path.join(systemDirectory, ".votive.db"),
29
+ cacheDirectory: path.join(systemDirectory, ".cache"),
13
30
  plugins: [
14
- vowelMarkdownPlugin,
31
+ vowelReadMarkdownPlugin,
15
32
  vowelImagesPlugin,
16
33
  vowelStylesPlugin,
17
- vowelRobotsPlugin,
18
- vowelXMLPlugin,
34
+ vowelWriteRobotsPlugin,
35
+ vowelWriteXMLPlugin,
19
36
  vowelVectorPlugin,
20
- vowelFontsPlugin
37
+ vowelFontsPlugin,
38
+ vowelURLsPlugin,
39
+ vowelWriteHTMLPlugin
21
40
  ]
22
41
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vowel",
3
3
  "type": "module",
4
- "version": "0.6.2",
4
+ "version": "0.6.4",
5
5
  "bin": "bin.js",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -28,6 +28,7 @@
28
28
  "hast-util-from-html": "^2.0.3",
29
29
  "hast-util-to-string": "^3.0.1",
30
30
  "hastscript": "^9.0.1",
31
+ "highlight.js": "^11.12.0",
31
32
  "js-yaml": "^4.1.0",
32
33
  "lightningcss": "^1.32.0",
33
34
  "mdast": "^2.3.2",
@@ -37,6 +38,7 @@
37
38
  "mdast-util-gfm-strikethrough": "^2.0.0",
38
39
  "mdast-util-gfm-table": "^2.0.0",
39
40
  "mdast-util-gfm-task-list-item": "^2.0.0",
41
+ "mdast-util-highlight-mark": "^1.2.2",
40
42
  "mdast-util-to-hast": "^13.2.1",
41
43
  "mdast-util-to-markdown": "^2.1.2",
42
44
  "mdast-util-to-string": "^4.0.0",
@@ -44,10 +46,12 @@
44
46
  "micromark-extension-gfm-footnote": "^2.1.0",
45
47
  "micromark-extension-gfm-table": "^2.1.1",
46
48
  "micromark-extension-gfm-task-list-item": "^2.1.0",
49
+ "micromark-extension-highlight-mark": "^1.2.0",
47
50
  "postcss": "^8.5.9",
48
51
  "rehype": "^13.0.2",
49
52
  "rehype-document": "^7.0.3",
50
53
  "rehype-github-alert": "^1.0.0",
54
+ "rehype-highlight": "^7.0.2",
51
55
  "rehype-parse": "^9.0.1",
52
56
  "rehype-preset-minify": "^7.0.1",
53
57
  "rehype-slug": "^6.0.0",
@@ -0,0 +1,78 @@
1
+ import votive from "votive"
2
+ import { spawn } from "node:child_process"
3
+
4
+ /**
5
+ * Publishes a full build to Cloudflare Pages via `wrangler pages deploy`.
6
+ *
7
+ * Always runs a complete build first, regardless of whether a dev server
8
+ * happens to already be running against this config - a command's
9
+ * context (config, notify) has no reference to a live server's own build
10
+ * queue, and this needs to behave identically whether triggered from the
11
+ * CLI (no server at all, see voot's runCommand) or a live "Publish"
12
+ * button over WS. Modest redundant cost when a dev server IS already
13
+ * running (bundle() is cheap when nothing's stale), traded for one
14
+ * deploy command that works the same everywhere.
15
+ *
16
+ * Shells out to `wrangler` rather than calling Cloudflare's REST API
17
+ * directly - Pages deploys require content-hashing every asset and
18
+ * building a manifest in Cloudflare's specific format, which wrangler
19
+ * already implements and maintains. `wrangler` reads
20
+ * CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID from the environment
21
+ * natively; this never touches credentials itself, and none should ever
22
+ * be added to `config` (it isn't a place secrets should live).
23
+ *
24
+ * @param {any} payload
25
+ * @param {{ config: import("votive").VotiveConfig & { cloudflareProjectName?: string }, notify: (message: object) => void }} context
26
+ */
27
+ async function deployToCloudflarePages(payload, { config, notify }) {
28
+ if (!config.cloudflareProjectName) throw new Error("config.cloudflareProjectName is required to deploy")
29
+
30
+ notify({ status: "progress", message: "Building..." })
31
+
32
+ const queue = await votive({ ...config, verbose: false })
33
+ const { runBuffers, runFetches } = await queue()
34
+
35
+ // Unlike a dev server's own rebuilds, a deploy needs everything done -
36
+ // there's no "next edit" to stay responsive for, so these are awaited
37
+ // fully instead of fired via runDeferred().
38
+ if (runBuffers) await runBuffers()
39
+ if (runFetches) await runFetches()
40
+
41
+ notify({ status: "progress", message: `Publishing ${config.targetFolder} to Cloudflare Pages...` })
42
+
43
+ return new Promise((resolve, reject) => {
44
+ const proc = spawn(
45
+ "wrangler",
46
+ ["pages", "deploy", config.targetFolder, "--project-name", config.cloudflareProjectName],
47
+ { stdio: ["ignore", "pipe", "pipe"] }
48
+ )
49
+
50
+ // Piped, not "inherit" - this can run with no terminal attached at
51
+ // all (triggered over WS from a browser), so wrangler's own output
52
+ // has to go through notify() to reach whoever's actually watching.
53
+ proc.stdout.on("data", chunk => notify({ status: "progress", message: chunk.toString() }))
54
+ proc.stderr.on("data", chunk => notify({ status: "progress", message: chunk.toString() }))
55
+
56
+ proc.on("error", reject) // e.g. wrangler isn't installed
57
+ proc.on("exit", code => {
58
+ if (code === 0) resolve({ deployed: true })
59
+ else reject(new Error(`wrangler exited with code ${code}`))
60
+ })
61
+ })
62
+ }
63
+
64
+ /**
65
+ * Not part of vowel's default plugin list (see config.js) - deploying to
66
+ * Cloudflare Pages specifically is an opt-in choice a site adds for
67
+ * itself, along with its own `cloudflareProjectName`, not something
68
+ * every vowel site should get by default.
69
+ * @type {import("votive").VotivePlugin}
70
+ */
71
+ const vowelDeployCloudflarePlugin = {
72
+ name: "vowel-deploy-cloudflare",
73
+ commands: {
74
+ deploy: deployToCloudflarePages
75
+ }
76
+ }
77
+
78
+ export default vowelDeployCloudflarePlugin
@@ -2,8 +2,8 @@ import { readFile } from "node:fs/promises"
2
2
  /** @import * as Votive from "votive" */
3
3
 
4
4
  /** @type {Votive.ProcessorWrite} */
5
- async function writeFile(destination, database, config) {
6
- const buffer = await readFile(destination.abstract.filePath)
5
+ async function writeFile(target, settings, api, config) {
6
+ const buffer = await readFile(target.abstract.filePath)
7
7
  return {
8
8
  data: buffer,
9
9
  }
@@ -0,0 +1,78 @@
1
+ import { testURL, createImagePaths, imageSizes, imageExts } from "./../../utils.js"
2
+ import path from "node:path"
3
+ import { h } from 'hastscript'
4
+ import * as unpic from "unpic"
5
+
6
+ /** @import * as Votive from "votive" */
7
+
8
+ /**
9
+ * @param {string} imagePath
10
+ * @param {import("votive").PluginAPI} api
11
+ * @param {string} alt
12
+ * @param {boolean} [itemprop]
13
+ */
14
+ function createDynamicImage(imagePath, api, alt, itemprop) {
15
+ // TODO: Hardcode image height and width
16
+ const parsed = path.parse(imagePath);
17
+ const isURL = testURL(imagePath)
18
+ const isImg = !parsed.ext.search(/^.(png|jpeg|jpg)$/);
19
+
20
+ if (isURL) {
21
+ const parsedImageURL = unpic.parseUrl(imagePath)
22
+ if (parsedImageURL) {
23
+
24
+ const sources = [...imageExts, "jpeg"].map((format, index) => {
25
+
26
+ const isImg = index === imageExts.length
27
+ // TODO: Ignore images that have options already specified
28
+ const urls = imageSizes.map(size => {
29
+ return unpic.transformUrl({
30
+ format: format,
31
+ url: parsedImageURL.src,
32
+ provider: parsedImageURL.cdn,
33
+ width: size
34
+ })
35
+ })
36
+
37
+ const sizes = urls.map((url, index) => `${url} ${imageSizes[index]}w`).join(", ")
38
+
39
+ return h(isImg ? "img" : "source", {
40
+ type: "image/" + format,
41
+ srcset: sizes,
42
+ src: isImg && urls.at(-1),
43
+ loading: isImg && "lazy",
44
+ sizes: "100vw",
45
+ alt: isImg && alt
46
+ })
47
+ })
48
+
49
+ return h("picture", { itemprop: itemprop && "image" }, sources)
50
+ }
51
+ }
52
+
53
+ if (!isImg) return
54
+
55
+ const relativePath = imagePath.startsWith("/") ? path.relative("/", imagePath) : imagePath
56
+ const image = api.target(relativePath)
57
+ if (!image) return
58
+ const formats = createImagePaths(image.abstract.sourcePath, "./", image.abstract.uuid)
59
+
60
+ const sources = formats.map((format, index) => {
61
+ const isImg = index === formats.length - 1
62
+
63
+ const sizes = format.map((size, index) => `/${size} ${imageSizes[index]}w`).join(", ")
64
+ const type = "image/" + path.extname(format[0]).slice(1)
65
+ return h(isImg ? "img" : "source", {
66
+ type,
67
+ srcset: sizes,
68
+ loading: isImg && "lazy",
69
+ src: isImg && "/" + format.at(-1),
70
+ sizes: "100vw",
71
+ alt: isImg && alt
72
+ })
73
+ })
74
+
75
+ return h("picture", { itemprop: itemprop && "image" }, sources)
76
+ }
77
+
78
+ export default createDynamicImage