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
@@ -0,0 +1,144 @@
1
+ import { toText } from "hast-util-to-text"
2
+ import { MARKER } from "../../secretPaths.js"
3
+ import createRkey from "./rkey.js"
4
+
5
+ /**
6
+ * Which pages become records, and what each record holds. No network:
7
+ * everything here is a function of the page and its settings, so it runs
8
+ * in the build, and the publish command only compares and sends.
9
+ */
10
+
11
+ const DOCUMENT = "site.standard.document"
12
+ const PUBLICATION = "site.standard.publication"
13
+
14
+ /** The lexicons vowel can build a record for. */
15
+ const SUPPORTED = [DOCUMENT]
16
+
17
+ /**
18
+ * The page's lexicon and, for a secret page, whether it may use it.
19
+ *
20
+ * `atproto_lexicon:` cascades from settings.md, and a page's own
21
+ * frontmatter can set it too. A page under a secret segment (`##`) is published only when
22
+ * the lexicon was set *inside* its secret scope: in its own frontmatter,
23
+ * or in a settings.md at or below the secret folder. A lexicon inherited
24
+ * from above the secret would publish a hidden page to the network by
25
+ * accident.
26
+ *
27
+ * The settings chain is the page-as-folder chain of the routed path
28
+ * (`blog/post.html` reads `""`, `blog`, `blog/post`), so level `k` holds
29
+ * the first `k` segments. Routing replaces a segment with its hash but
30
+ * keeps the depth, so the secret segment at source index `s` is level
31
+ * `s + 1`, and every level from there down is inside it.
32
+ * @param {{ source?: string, metadata: Record<string, any> }} target
33
+ * @param {{ raw: (label: string) => (unknown[] | null)[] | undefined }} settings
34
+ * @returns {string | undefined}
35
+ */
36
+ function lexiconFor(target, settings) {
37
+ const own = target.metadata.fm_atproto_lexicon
38
+ if (typeof own === "string" && own) return own
39
+
40
+ const levels = settings.raw("fm_atproto_lexicon") ?? []
41
+ const deepest = levels.findLastIndex(values => Array.isArray(values) && values.length > 0)
42
+ if (deepest === -1) return undefined
43
+ const lexicon = levels[deepest].at(-1)
44
+ if (typeof lexicon !== "string" || !lexicon) return undefined
45
+
46
+ const segments = (target.source ?? "").split(/[\\/]/)
47
+ const secret = segments.findIndex(segment => segment.includes(MARKER))
48
+ if (secret !== -1 && deepest < secret + 1) return undefined
49
+ return lexicon
50
+ }
51
+
52
+ /**
53
+ * The site's origin from `domain:`, the same base the html plugin builds
54
+ * the canonical link from. No trailing slash, per the lexicon.
55
+ * @param {unknown} domain
56
+ */
57
+ function siteOrigin(domain) {
58
+ if (typeof domain !== "string" || !domain) return undefined
59
+ const base = domain.startsWith("http") ? domain : `https://${domain}`
60
+ return new URL(base).origin
61
+ }
62
+
63
+ /**
64
+ * The site's publication: one per account and origin, its key derived
65
+ * from the origin the way a document's is from its URL, so its AT-URI is
66
+ * known before it exists. A document's `site` names it, and
67
+ * `/.well-known/site.standard.publication` serves it, which is how the
68
+ * domain claims it. The record itself is built by the publish command
69
+ * (publish.js), because its icon is an uploaded image.
70
+ * @param {string} did
71
+ * @param {string} origin
72
+ */
73
+ function publicationURI(did, origin) {
74
+ return `at://${did}/${PUBLICATION}/${createRkey(origin)}`
75
+ }
76
+
77
+ /**
78
+ * A `site.standard.document` record for the page, or the list of what it
79
+ * is missing. `title`, `publishedAt` and `site` are required by the
80
+ * lexicon; a page without a date cannot be a document. `site` is the
81
+ * publication's AT-URI; `origin` is what the page's key is made from.
82
+ * @param {{ metadata: Record<string, any> }} target
83
+ * @param {string | undefined} origin
84
+ * @param {string} [site] - the publication's AT-URI
85
+ * @returns {{ record: Record<string, unknown> } | { missing: string[] }}
86
+ */
87
+ function documentRecord(target, origin, site = origin) {
88
+ const { title, date, prettyURL, description, tags, hastAbstract } = target.metadata
89
+ const published = date ? new Date(date) : undefined
90
+ const validDate = published && !Number.isNaN(published.getTime())
91
+
92
+ const missing = [
93
+ ...(origin ? [] : ["domain"]),
94
+ ...(title ? [] : ["title"]),
95
+ ...(validDate ? [] : ["date"])
96
+ ]
97
+ if (missing.length) return { missing }
98
+
99
+ const textContent = hastAbstract ? toText(hastAbstract).trim() : ""
100
+
101
+ const record = {
102
+ $type: DOCUMENT,
103
+ site,
104
+ path: prettyURL,
105
+ title,
106
+ publishedAt: published.toISOString(),
107
+ ...(description ? { description } : {}),
108
+ ...(Array.isArray(tags) && tags.length ? { tags } : {}),
109
+ ...(textContent ? { textContent } : {})
110
+ }
111
+ return { record }
112
+ }
113
+
114
+ /**
115
+ * The page's AT-URI and record, or why it has none. `skip` is silent (the
116
+ * page is not meant to be published). `incomplete` is a page under a
117
+ * lexicon that lacks a property the lexicon requires; it is not a record,
118
+ * by design, and is only mentioned under `verbose`. `warning` is a
119
+ * misconfiguration: a lexicon vowel cannot build.
120
+ * @param {{ path: string, source?: string, metadata: Record<string, any> }} target
121
+ * @param {{ did: string, domain: unknown, settings: any }} context
122
+ * @returns {{ atUri: string, record: Record<string, unknown> } | { skip: true } | { incomplete: string } | { warning: string }}
123
+ */
124
+ function pageRecord(target, { did, domain, settings }) {
125
+ const lexicon = lexiconFor(target, settings)
126
+ if (!lexicon) return { skip: true }
127
+ if (!SUPPORTED.includes(lexicon)) {
128
+ return { warning: `lexicon "${lexicon}" is not one vowel can build a record for (supported: ${SUPPORTED.join(", ")})` }
129
+ }
130
+
131
+ const origin = siteOrigin(domain)
132
+ const site = origin ? publicationURI(did, origin) : undefined
133
+ const built = documentRecord(target, origin, site)
134
+ if ("missing" in built) {
135
+ return { incomplete: `not published to AT Protocol: a ${lexicon} record needs ${built.missing.join(", ")}` }
136
+ }
137
+
138
+ // The key is the page's canonical URL - origin and path - and frozen
139
+ // (rkey.js), whatever `site` holds.
140
+ const rkey = createRkey(new URL(String(built.record.path), origin).href)
141
+ return { atUri: `at://${did}/${lexicon}/${rkey}`, record: built.record }
142
+ }
143
+
144
+ export { DOCUMENT, PUBLICATION, SUPPORTED, lexiconFor, siteOrigin, publicationURI, documentRecord, pageRecord }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A page's record key, derived from its canonical URL: the site's origin
3
+ * and the page's published path, e.g. "https://example.com/blog/post".
4
+ *
5
+ * Deterministic, so the AT-URI is known on the first build, before any
6
+ * record exists, and the same page gets the same key on every machine. No
7
+ * ledger: the URL is the identity. The domain is part of it, so two sites
8
+ * publishing to one account never share a key, and a site that changes
9
+ * domain gets new keys: the documents now live somewhere else. The old
10
+ * records are *not* deleted by the next publish, because their `site` is
11
+ * the old domain and publish only deletes records whose `site` is this
12
+ * site's. A move that changes one page's URL, on the same domain, does
13
+ * delete that page's old record.
14
+ *
15
+ * `site.standard.document` declares `key: tid`, so the key must be shaped
16
+ * like a TID: 13 characters of base32-sortable, with the top bit clear.
17
+ * It is 63 bits of FNV-1a over the URL's UTF-8 bytes, which clears the
18
+ * top bit by construction. A real TID encodes a timestamp and this one
19
+ * does not, so keys do not sort by date. Nothing in site.standard relies
20
+ * on that.
21
+ *
22
+ * **The function is frozen once anything is published.** Changing the
23
+ * hash, its input or its encoding moves every page to a new key. The
24
+ * next publish would then delete and recreate every record.
25
+ */
26
+
27
+ const ALPHABET = "234567abcdefghijklmnopqrstuvwxyz"
28
+ const OFFSET = 0xcbf29ce484222325n
29
+ const PRIME = 0x100000001b3n
30
+ const MASK_64 = (1n << 64n) - 1n
31
+ const MASK_63 = (1n << 63n) - 1n
32
+ const LENGTH = 13
33
+
34
+ /** @param {string} text */
35
+ function fnv1a63(text) {
36
+ const bytes = Buffer.from(text, "utf8")
37
+ const hash = bytes.reduce((h, byte) => ((h ^ BigInt(byte)) * PRIME) & MASK_64, OFFSET)
38
+ return hash & MASK_63
39
+ }
40
+
41
+ /**
42
+ * @param {string} canonicalURL - e.g. "https://example.com/blog/post"
43
+ * @returns {string} a TID-shaped record key
44
+ */
45
+ function createRkey(canonicalURL) {
46
+ const hash = fnv1a63(canonicalURL)
47
+ const characters = Array.from({ length: LENGTH }, (_, index) => {
48
+ const shift = BigInt(5 * (LENGTH - 1 - index))
49
+ return ALPHABET[Number((hash >> shift) & 31n)]
50
+ })
51
+ return characters.join("")
52
+ }
53
+
54
+ export default createRkey
@@ -1,5 +1,6 @@
1
1
  import votive from "votive"
2
2
  import { spawn } from "node:child_process"
3
+ import path from "node:path"
3
4
 
4
5
  /**
5
6
  * Publishes a full build to Cloudflare Pages via `wrangler pages deploy`.
@@ -8,7 +9,7 @@ import { spawn } from "node:child_process"
8
9
  * happens to already be running against this config - a command's
9
10
  * context (config, notify) has no reference to a live server's own build
10
11
  * 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
+ * CLI (no server at all, see votive's runCommand) or a live "Publish"
12
13
  * button over WS. Modest redundant cost when a dev server IS already
13
14
  * running (bundle() is cheap when nothing's stale), traded for one
14
15
  * deploy command that works the same everywhere.
@@ -25,25 +26,37 @@ import { spawn } from "node:child_process"
25
26
  * @param {{ config: import("votive").VotiveConfig & { cloudflareProjectName?: string }, notify: (message: object) => void }} context
26
27
  */
27
28
  async function deployToCloudflarePages(payload, { config, notify }) {
28
- if (!config.cloudflareProjectName) throw new Error("config.cloudflareProjectName is required to deploy")
29
-
30
29
  notify({ status: "progress", message: "Building..." })
31
30
 
32
- const queue = await votive({ ...config, verbose: false })
33
- const { runBuffers, runFetches } = await queue()
34
-
35
31
  // 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()
32
+ // there's no "next edit" to stay responsive for - so the deferred
33
+ // work (image derivatives, link previews) is awaited, and the site is
34
+ // closed: the database is a cache on disk, and this process is done
35
+ // with it.
36
+ const site = await votive({ ...config, verbose: false })
37
+ await (await site.build()).deferred
38
+ const setting = site.database.setting.getByFolder("").lastNonNull("fm_cloudflare_project")
39
+ await site.close()
40
+
41
+ // The Pages project, from the command's payload (`vowel --command
42
+ // deploy --payload '{"projectName":"my-site"}'`, or a Publish button's
43
+ // body), the root settings.md's `cloudflare_project:`, or the config an
44
+ // embedder built.
45
+ const projectName = payload?.projectName ?? setting ?? config.cloudflareProjectName
46
+ if (!projectName) throw new Error("a Cloudflare Pages project name is required to deploy: set cloudflare_project in the root settings.md, or pass {\"projectName\": \"…\"} as the payload")
47
+
48
+ // `{"dryRun": true}` builds and stops short of wrangler.
49
+ if (payload?.dryRun) {
50
+ notify({ status: "done", message: `Would deploy ${config.targetFolder} to Cloudflare Pages project ${projectName}` })
51
+ return { deployed: false, projectName, dryRun: true }
52
+ }
40
53
 
41
54
  notify({ status: "progress", message: `Publishing ${config.targetFolder} to Cloudflare Pages...` })
42
55
 
43
56
  return new Promise((resolve, reject) => {
44
57
  const proc = spawn(
45
58
  "wrangler",
46
- ["pages", "deploy", config.targetFolder, "--project-name", config.cloudflareProjectName],
59
+ ["pages", "deploy", config.targetFolder, "--project-name", projectName],
47
60
  { stdio: ["ignore", "pipe", "pipe"] }
48
61
  )
49
62
 
@@ -55,17 +68,17 @@ async function deployToCloudflarePages(payload, { config, notify }) {
55
68
 
56
69
  proc.on("error", reject) // e.g. wrangler isn't installed
57
70
  proc.on("exit", code => {
58
- if (code === 0) resolve({ deployed: true })
71
+ if (code === 0) resolve({ deployed: true, projectName })
59
72
  else reject(new Error(`wrangler exited with code ${code}`))
60
73
  })
61
74
  })
62
75
  }
63
76
 
64
77
  /**
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.
78
+ * In vowel's default plugin list (config.js), so `vowel --command
79
+ * deploy` reaches it from any project. Registering a command costs
80
+ * nothing until it is invoked, and invoking it needs a project name,
81
+ * so a site that deploys elsewhere never meets it.
69
82
  * @type {import("votive").VotivePlugin}
70
83
  */
71
84
  const vowelDeployCloudflarePlugin = {
@@ -76,3 +89,4 @@ const vowelDeployCloudflarePlugin = {
76
89
  }
77
90
 
78
91
  export default vowelDeployCloudflarePlugin
92
+ export { deployToCloudflarePages }
@@ -1,38 +1,35 @@
1
- import { readFile } from "node:fs/promises"
2
1
  /** @import * as Votive from "votive" */
3
2
 
4
- /** @type {Votive.ProcessorWrite} */
5
- async function writeFile(target, settings, api, config) {
6
- const buffer = await readFile(target.abstract.filePath)
7
- return {
8
- data: buffer,
9
- }
10
- }
11
-
12
- function readFont(filePath) {
13
- return {
14
- abstract: { filePath },
15
- metadata: {}
16
- }
3
+ /**
4
+ * A font the project ships is copied straight through: target.buffer()
5
+ * resolves the source against sourceFolder at the moment of reading, so
6
+ * nothing about this machine is ever stored. The @font-face that uses
7
+ * it is the author's, in their own stylesheet.
8
+ *
9
+ * Vowel bundles no fonts of its own any more - the families and their
10
+ * stubs went with the typography ramps into TypographyStyles.css, which
11
+ * names a brand family through --font-*-brand and falls back to the
12
+ * system stack. Fonts by url: tasks/2-in-progress/load-fonts.md.
13
+ * @type {Votive.ProcessorWrite}
14
+ */
15
+ function writeFile(target) {
16
+ return { data: target.buffer() }
17
17
  }
18
18
 
19
19
  /** @type {Votive.VotiveProcessor} */
20
20
  const fontsReader = {
21
21
  format: "buffer",
22
22
  extensions: [".woff", ".woff2", ".ttf", ".otf"],
23
- readFile: readFont,
23
+ router: ({ name, dir, ext }) => ({ name, dir, ext }),
24
+ // A font carries no metadata worth inferring.
25
+ readFile: () => ({ metadata: {} }),
24
26
  writeFile
25
27
  }
26
28
 
27
29
  /** @type {Votive.VotivePlugin} */
28
30
  const vowelFontsPlugin = {
29
31
  name: "vowel-fonts",
30
- processors: [fontsReader],
31
- router: ({ name, dir, ext }) => {
32
- return {
33
- name, dir, ext
34
- }
35
- }
32
+ processors: [fontsReader]
36
33
  }
37
34
 
38
35
  export default vowelFontsPlugin
@@ -0,0 +1,21 @@
1
+ @layer svedit-defaults {
2
+ :root {
3
+ /* Primary accent for active editing controls, carets, and selections. */
4
+ --editing: oklch(60% 0.22 283);
5
+ /* Low-opacity version of the editing accent for passive highlights. */
6
+ --editing-muted: oklch(from var(--editing) l c h / 0.1);
7
+ /* Low-emphasis strokes and outlines, such as inactive node gaps. */
8
+ --editing-stroke: color-mix(in oklch, currentColor 25%, transparent);
9
+ /* Foreground color for content displayed on the editing accent. */
10
+ --editing-foreground: oklch(100% 0 0);
11
+ }
12
+ }
13
+
14
+ @layer svedit-defaults {
15
+ @keyframes node-caret-blink {
16
+ 0%, 60% { opacity: 1; }
17
+ 68% { opacity: 0; }
18
+ 88% { opacity: 0; }
19
+ 100% { opacity: 1; }
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ @layer svedit-defaults {
2
+ :root {
3
+ /* Primary accent for active editing controls, carets, and selections. */
4
+ --editing: oklch(60% 0.22 283);
5
+ /* Low-opacity version of the editing accent for passive highlights. */
6
+ --editing-muted: oklch(from var(--editing) l c h / 0.1);
7
+ /* Low-emphasis strokes and outlines, such as inactive node gaps. */
8
+ --editing-stroke: color-mix(in oklch, currentColor 25%, transparent);
9
+ /* Foreground color for content displayed on the editing accent. */
10
+ --editing-foreground: oklch(100% 0 0);
11
+ }
12
+ }
13
+
14
+ @layer svedit-defaults {
15
+ @keyframes node-caret-blink {
16
+ 0%, 60% { opacity: 1; }
17
+ 68% { opacity: 0; }
18
+ 88% { opacity: 0; }
19
+ 100% { opacity: 1; }
20
+ }
21
+ }