vowel 0.7.1-beta.0 → 0.7.3

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -239
  3. package/bin.js +91 -31
  4. package/cli.js +32 -0
  5. package/config.js +12 -0
  6. package/extractDate.js +1 -1
  7. package/package.json +8 -34
  8. package/pathDate.js +69 -0
  9. package/plugins/assets/index.js +1 -1
  10. package/plugins/atproto/body.js +3 -1
  11. package/plugins/atproto/index.js +5 -45
  12. package/plugins/atproto/publish.js +42 -11
  13. package/plugins/atproto/record.js +47 -4
  14. package/plugins/deploy-cloudflare/index.js +4 -1
  15. package/plugins/export/index.js +131 -0
  16. package/plugins/html/bundle/index.js +101 -44
  17. package/plugins/html/editor/Editor.svelte +9 -5
  18. package/plugins/html/editor/create-session.js +3 -1
  19. package/plugins/html/editor/directives.js +12 -0
  20. package/plugins/html/editor/frontmatter.js +77 -12
  21. package/plugins/html/editor/ingest.js +29 -31
  22. package/plugins/html/editor/main.js +5 -1
  23. package/plugins/html/editor/serialize.js +8 -4
  24. package/plugins/html/editor/socket.js +34 -4
  25. package/plugins/html/image.js +88 -27
  26. package/plugins/html/index.js +237 -109
  27. package/plugins/html/partials.js +41 -17
  28. package/plugins/html/socialLinks.js +13 -11
  29. package/plugins/html/writeRules.js +234 -42
  30. package/plugins/images/favicon.png +0 -0
  31. package/plugins/images/index.js +69 -8
  32. package/plugins/markdown/colorScheme.js +70 -14
  33. package/plugins/markdown/frontmatter.js +18 -8
  34. package/plugins/markdown/index.js +94 -25
  35. package/plugins/markdown/metadata.js +132 -32
  36. package/plugins/markdown/typography.js +11 -11
  37. package/plugins/styles/index.js +4 -4
  38. package/plugins/xml/index.js +17 -10
  39. package/stylesheets/DefaultStyles.css +163 -108
  40. package/stylesheets/TypographyStyles.css +148 -81
  41. package/utils.js +74 -21
@@ -51,10 +51,12 @@ export default function openSocket() {
51
51
 
52
52
  const target = JSON.parse(e.data)
53
53
 
54
- // Nothing here knows what a non-html target should do on change yet -
55
- // that's each content type's own concern to add, same as html's own
56
- // patch logic below isn't generic.
57
- if (target.extension !== ".html") return
54
+ // A file that isn't a page can only matter to this page as something
55
+ // it loads: an image the page named before its file existed.
56
+ if (target.extension !== ".html") {
57
+ retryImages("/" + target.path)
58
+ return
59
+ }
58
60
 
59
61
  const regex = new RegExp(window.location.pathname + "(index)?(\\.html)")
60
62
  if (!("/" + target.path).match(regex)) return
@@ -185,6 +187,34 @@ function patchBody(body, nextBody) {
185
187
  }
186
188
  }
187
189
 
190
+ /**
191
+ * A page can arrive before the image files it names: an image's
192
+ * derivatives are encoded after the page is written, and a browser never
193
+ * asks again for an image that failed. When a file lands, every image
194
+ * that failed and names it is replaced with a copy of itself, which
195
+ * asks again. A <picture> is copied whole, so its <source>s are chosen
196
+ * from again too.
197
+ * @param {string} url - the file's url, from the root
198
+ */
199
+ function retryImages(url) {
200
+ const failed = [...document.images].filter(img => img.complete && img.naturalWidth === 0)
201
+ const elements = failed.map(img => img.parentElement?.tagName === "PICTURE" ? img.parentElement : img)
202
+ const naming = elements.filter(element => urlsOf(element).includes(url))
203
+ for (const element of naming) element.replaceWith(element.cloneNode(true))
204
+ }
205
+
206
+ /**
207
+ * Every url an <img> or a <picture> could load.
208
+ * @param {Element} element
209
+ */
210
+ function urlsOf(element) {
211
+ const nodes = [element, ...element.querySelectorAll("source, img")]
212
+ return nodes.flatMap(node => {
213
+ const candidates = (node.getAttribute("srcset") ?? "").split(",").map(candidate => candidate.trim().split(/\s+/)[0])
214
+ return [node.getAttribute("src"), ...candidates].filter(Boolean)
215
+ })
216
+ }
217
+
188
218
  /**
189
219
  * The client adds elements that aren't the build's: the editor's own
190
220
  * <script type="module">, and whatever Svelte mounts into the body (the
@@ -1,10 +1,71 @@
1
- import { testURL, createImagePaths, imageSizes, imageExts } from "./../../utils.js"
1
+ import { testURL, imageSizes, imageExts } from "./../../utils.js"
2
2
  import path from "node:path"
3
3
  import { h } from 'hastscript'
4
4
  import * as unpic from "unpic"
5
5
 
6
6
  /** @import * as Votive from "votive" */
7
7
 
8
+ /**
9
+ * The image target a path names, from the root. Lowercased: every vowel
10
+ * target path is (config.js's router), and the author may well have
11
+ * typed the file's real name.
12
+ * @param {string} imagePath
13
+ * @param {import("votive").PluginAPI} api
14
+ */
15
+ function findImage(imagePath, api) {
16
+ const relativePath = (imagePath.startsWith("/") ? path.relative("/", imagePath) : imagePath).toLowerCase()
17
+ return api.target(relativePath)
18
+ }
19
+
20
+ /**
21
+ * The derivative to link where one file goes rather than a srcset: in
22
+ * the image's own format, at `size` (one of imageSizes). Undefined for
23
+ * an image without derivatives (plugins/images: a format that isn't
24
+ * optimised, the root favicon.png, or an image not read yet).
25
+ * @param {any} image - the image's target
26
+ * @param {number} size
27
+ */
28
+ function derivativeURL(image, size) {
29
+ const format = path.extname(image.path).slice(1)
30
+ const derivative = image.metadata?.derivatives?.find(entry => entry.format === format && entry.size === size)
31
+ if (!derivative) return
32
+ return "/" + derivative.path
33
+ }
34
+
35
+ /**
36
+ * The url to put where an image path goes as one file - og:image, the
37
+ * logo, an icon. A png or jpeg is not published at full size
38
+ * (plugins/images), so the path names a derivative; anything else, an
39
+ * external url included, is returned as it is. A tracked read, so the
40
+ * page is rewritten when the image is read or changes.
41
+ * @param {string | undefined} imagePath - from the root
42
+ * @param {import("votive").PluginAPI} api
43
+ * @param {number} size - one of imageSizes
44
+ */
45
+ function imageURL(imagePath, api, size) {
46
+ if (!imagePath || testURL(imagePath) || !/\.(png|jpe?g)$/i.test(imagePath)) return imagePath
47
+ const image = findImage(imagePath, api)
48
+ if (!image) return imagePath
49
+ return derivativeURL(image, size) ?? imagePath
50
+ }
51
+
52
+ /**
53
+ * The page image's dominant colour as a style for the element that
54
+ * holds it - a page's <main>, a card's <article> - so CSS outside the
55
+ * image can use it (text that contrasts with it, say). The <picture>
56
+ * carries it too, for its own placeholder. Nothing here decides that
57
+ * anything should use it. Undefined for no image, an external one, or
58
+ * one not read yet.
59
+ * @param {string | undefined} imagePath - from the root
60
+ * @param {import("votive").PluginAPI} api
61
+ */
62
+ function dominantStyle(imagePath, api) {
63
+ if (!imagePath || testURL(imagePath)) return
64
+ const dominant = findImage(imagePath, api)?.metadata.dominant
65
+ if (!dominant) return
66
+ return `--color-image-dominant: ${dominant}`
67
+ }
68
+
8
69
  /**
9
70
  * @param {string} imagePath
10
71
  * @param {import("votive").PluginAPI} api
@@ -45,42 +106,39 @@ function createDynamicImage(imagePath, api, alt, itemprop) {
45
106
  })
46
107
  })
47
108
 
48
- // The derivative paths in srcset are hash-based, so the original asset
49
- // path cannot be recovered from them. Carried here rather than as a
50
- // <source>, which is a loading candidate a browser could select.
51
- return h("picture", { itemprop: itemprop && "image", "data-original": imagePath }, sources)
109
+ return h("picture", { itemprop: itemprop && "image" }, sources)
52
110
  }
53
111
  }
54
112
 
55
113
  if (!isImg) return
56
114
 
57
- // Lowercased: every vowel target path is (config.js's router), and
58
- // the author may well have typed the file's real name.
59
- const relativePath = (imagePath.startsWith("/") ? path.relative("/", imagePath) : imagePath).toLowerCase()
60
- const image = api.target(relativePath)
115
+ const image = findImage(imagePath, api)
61
116
  if (!image) return
62
- // Until the image has been read (a deferred buffer read) it has no
63
- // hash and no derivatives; a plain <img> stands in, and the tracked
64
- // read of `hash` rewrites this page when they arrive. Derivatives are
65
- // named from the *target* path, as the images processor declares them.
66
- if (!image.metadata.hash) return h("img", { src: "/" + image.path, alt, loading: "lazy" })
67
- const formats = createImagePaths(image.path, "./", image.metadata.hash)
117
+ // An image without derivatives is published as it is (the root
118
+ // favicon.png), and a plain <img> links it. The tracked read of
119
+ // `derivatives` rewrites this page if they arrive.
120
+ const { derivatives } = image.metadata
121
+ if (!derivatives) return h("img", { src: "/" + image.path, alt, loading: "lazy" })
122
+ // One group per format, in the order the read lists them, which is
123
+ // <picture> order: avif, webp, then the image's own format as the <img>.
124
+ const formats = [...new Set(derivatives.map(entry => entry.format))]
125
+ .map(format => derivatives.filter(entry => entry.format === format))
68
126
 
69
127
  // Intrinsic size, measured at read (plugins/images): the browser
70
128
  // reserves the right box before the bytes arrive, and the reset's
71
129
  // `img { height: auto }` keeps the ratio when the width is capped.
72
- const { width, height, dominant } = image.metadata
130
+ const { width, height, dominant, placeholder } = image.metadata
73
131
 
74
132
  const sources = formats.map((format, index) => {
75
133
  const isImg = index === formats.length - 1
76
134
 
77
- const sizes = format.map((size, index) => `/${size} ${imageSizes[index]}w`).join(", ")
78
- const type = "image/" + path.extname(format[0]).slice(1)
135
+ const sizes = format.map(entry => `/${entry.path} ${entry.size}w`).join(", ")
136
+ const type = "image/" + format[0].format
79
137
  return h(isImg ? "img" : "source", {
80
138
  type,
81
139
  srcset: sizes,
82
140
  loading: isImg && "lazy",
83
- src: isImg && "/" + format.at(-1),
141
+ src: isImg && "/" + format.at(-1).path,
84
142
  sizes: "100vw",
85
143
  width: isImg && width,
86
144
  height: isImg && height,
@@ -88,17 +146,20 @@ function createDynamicImage(imagePath, api, alt, itemprop) {
88
146
  })
89
147
  })
90
148
 
91
- // The derivative paths in srcset are hash-based, so the original asset
92
- // path cannot be recovered from them. Carried here rather than as a
93
- // <source>, which is a loading candidate a browser could select.
94
- // --dominant is the image's dominant colour, for a stylesheet to use
95
- // as the placeholder behind a lazy image (`picture { background:
96
- // var(--dominant) }`); nothing here decides that it should.
149
+ // Two values for a stylesheet to show behind the image while it loads:
150
+ // its dominant colour, and a tiny blurry copy (plugins/images; none
151
+ // for an image with transparency). Nothing here decides that it should.
152
+ const style = [
153
+ dominant && `--color-image-dominant: ${dominant}`,
154
+ // Unquoted: base64 has no character url() needs quoted, and the
155
+ // minifier would spell each quote as `&#34`.
156
+ placeholder && `--url-image-placeholder: url(${placeholder})`
157
+ ].filter(Boolean).join("; ")
97
158
  return h("picture", {
98
159
  itemprop: itemprop && "image",
99
- "data-original": imagePath,
100
- style: dominant && `--dominant: ${dominant}`
160
+ style: style || undefined
101
161
  }, sources)
102
162
  }
103
163
 
164
+ export { dominantStyle, imageURL, derivativeURL }
104
165
  export default createDynamicImage