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,250 @@
1
+ import {
2
+ Session,
3
+ SelectAllCommand,
4
+ InsertDefaultNodeCommand,
5
+ AddNewLineCommand,
6
+ BreakTextNodeCommand,
7
+ UndoCommand,
8
+ RedoCommand,
9
+ SelectParentCommand,
10
+ ToggleMarkCommand,
11
+ define_document_schema,
12
+ fill_document_defaults,
13
+ define_keymap
14
+ } from "svedit"
15
+
16
+ import Overlays from "./Overlays.svelte"
17
+ import ingest from "./ingest.js"
18
+ import {
19
+ Page,
20
+ Paragraph,
21
+ Heading,
22
+ List,
23
+ ListItem,
24
+ Blockquote,
25
+ Alert,
26
+ CodeBlock,
27
+ ThematicBreak,
28
+ Image,
29
+ Embed,
30
+ Strong,
31
+ Emphasis,
32
+ InlineCode,
33
+ Highlight,
34
+ Strikethrough,
35
+ Link
36
+ } from "./nodes/index.js"
37
+
38
+ // The faithful subset of vowel's generated HTML - everything the MD -> HTML
39
+ // conversion emits as distinct semantic markup, plus the two node kinds that
40
+ // stand in for content it generates rather than renders (image, embed).
41
+ const INLINE_MARKS = ["strong", "emphasis", "inline_code", "highlight", "strikethrough", "link"]
42
+
43
+ const BLOCK_TYPES = [
44
+ "paragraph",
45
+ "heading",
46
+ "list",
47
+ "blockquote",
48
+ "alert",
49
+ "code_block",
50
+ "thematic_break",
51
+ "image",
52
+ "embed"
53
+ ]
54
+
55
+ const document_schema = define_document_schema({
56
+ page: {
57
+ kind: "document",
58
+ properties: {
59
+ body: {
60
+ type: "node_array",
61
+ node_types: BLOCK_TYPES,
62
+ default_node_type: "paragraph"
63
+ }
64
+ }
65
+ },
66
+ paragraph: {
67
+ kind: "text",
68
+ properties: {
69
+ content: { type: "text", mark_types: INLINE_MARKS, allow_newlines: true }
70
+ }
71
+ },
72
+ heading: {
73
+ kind: "text",
74
+ properties: {
75
+ content: { type: "text", mark_types: INLINE_MARKS, allow_newlines: false },
76
+ level: { type: "integer", min: 1, max: 6, default: 2 }
77
+ }
78
+ },
79
+ list: {
80
+ kind: "block",
81
+ properties: {
82
+ items: { type: "node_array", node_types: ["list_item"], default_node_type: "list_item" },
83
+ ordered: { type: "boolean", default: false }
84
+ }
85
+ },
86
+ list_item: {
87
+ kind: "text",
88
+ properties: {
89
+ content: { type: "text", mark_types: INLINE_MARKS, allow_newlines: false }
90
+ }
91
+ },
92
+ blockquote: {
93
+ kind: "block",
94
+ properties: {
95
+ body: { type: "node_array", node_types: BLOCK_TYPES, default_node_type: "paragraph" }
96
+ }
97
+ },
98
+ // > [!NOTE] - the variant is what the html plugin encodes in the class.
99
+ alert: {
100
+ kind: "block",
101
+ properties: {
102
+ variant: { type: "string", default: "note" },
103
+ body: { type: "node_array", node_types: BLOCK_TYPES, default_node_type: "paragraph" }
104
+ }
105
+ },
106
+ code_block: {
107
+ kind: "block",
108
+ properties: {
109
+ code: { type: "string", default: "" },
110
+ language: { type: "string", default: "" }
111
+ }
112
+ },
113
+ thematic_break: {
114
+ kind: "block",
115
+ properties: {}
116
+ },
117
+ // source is the canonical asset path from the <picture>; html is the
118
+ // served markup, kept so edit mode looks identical to the page.
119
+ image: {
120
+ kind: "block",
121
+ properties: {
122
+ source: { type: "string", default: "" },
123
+ alt: { type: "string", default: "" },
124
+ caption: { type: "string", default: "" },
125
+ html: { type: "string", default: "" }
126
+ }
127
+ },
128
+ // A directive that expanded into generated content. Opaque by design.
129
+ embed: {
130
+ kind: "block",
131
+ properties: {
132
+ directive: { type: "string", default: "" },
133
+ html: { type: "string", default: "" }
134
+ }
135
+ },
136
+ strong: { kind: "mark", properties: {} },
137
+ emphasis: { kind: "mark", properties: {} },
138
+ inline_code: { kind: "mark", properties: {} },
139
+ highlight: { kind: "mark", properties: {} },
140
+ strikethrough: { kind: "mark", properties: {} },
141
+ link: { kind: "mark", properties: { href: { type: "string", default: "" } } }
142
+ })
143
+
144
+ // Only non-numeric ids are valid in Svedit because document paths are built
145
+ // from mixed string and number segments.
146
+ function generate_id(length = 16) {
147
+ const id_alphabet = "abcdefghijklmnopqrstuvwxyz"
148
+ const random_values = crypto.getRandomValues(new Uint8Array(length))
149
+ return [...random_values].map(value => id_alphabet[value % id_alphabet.length]).join("")
150
+ }
151
+
152
+ const session_config = {
153
+ generate_id,
154
+ system_components: {
155
+ overlays: Overlays
156
+ },
157
+ node_components: {
158
+ page: Page,
159
+ paragraph: Paragraph,
160
+ heading: Heading,
161
+ list: List,
162
+ list_item: ListItem,
163
+ blockquote: Blockquote,
164
+ alert: Alert,
165
+ code_block: CodeBlock,
166
+ thematic_break: ThematicBreak,
167
+ image: Image,
168
+ embed: Embed,
169
+ strong: Strong,
170
+ emphasis: Emphasis,
171
+ inline_code: InlineCode,
172
+ highlight: Highlight,
173
+ strikethrough: Strikethrough,
174
+ link: Link
175
+ },
176
+
177
+ /**
178
+ * @param {object} context - The svedit context with session, editable, canvas.
179
+ * @returns {{ commands: object, keymap: object }}
180
+ */
181
+ create_commands_and_keymap: (context) => {
182
+ const commands = {
183
+ select_all: new SelectAllCommand(context),
184
+ insert_default_node: new InsertDefaultNodeCommand(context),
185
+ add_new_line: new AddNewLineCommand(context),
186
+ break_text_node: new BreakTextNodeCommand(context),
187
+ undo: new UndoCommand(context),
188
+ redo: new RedoCommand(context),
189
+ select_parent: new SelectParentCommand(context),
190
+ toggle_strong: new ToggleMarkCommand("strong", context),
191
+ toggle_emphasis: new ToggleMarkCommand("emphasis", context),
192
+ toggle_inline_code: new ToggleMarkCommand("inline_code", context),
193
+ toggle_highlight: new ToggleMarkCommand("highlight", context),
194
+ toggle_strikethrough: new ToggleMarkCommand("strikethrough", context)
195
+ }
196
+
197
+ const keymap = define_keymap({
198
+ "meta+a,ctrl+a": [commands.select_all],
199
+ enter: [commands.break_text_node, commands.insert_default_node],
200
+ // In case of a node cursor, fall back to inserting a default node. This
201
+ // is needed because on iOS selecting a node cursor triggers auto
202
+ // capitalization (shift pressed)
203
+ "shift+enter": [commands.add_new_line, commands.insert_default_node],
204
+ "meta+z,ctrl+z": [commands.undo],
205
+ "meta+shift+z,ctrl+shift+z": [commands.redo],
206
+ escape: [commands.select_parent],
207
+ "meta+b,ctrl+b": [commands.toggle_strong],
208
+ "meta+i,ctrl+i": [commands.toggle_emphasis]
209
+ })
210
+
211
+ return { commands, keymap }
212
+ },
213
+
214
+ inserters: {
215
+ paragraph: function (tr, content = { content: "", marks: [], annotations: [] }) {
216
+ const new_paragraph = {
217
+ id: session_config.generate_id(),
218
+ type: "paragraph",
219
+ content
220
+ }
221
+ tr.create(new_paragraph)
222
+ tr.insert_nodes([new_paragraph.id])
223
+ // NOTE: Relies on insert_nodes selecting the newly inserted node(s)
224
+ tr.set_selection({
225
+ type: "text",
226
+ path: [...tr.selection.path, tr.selection.focus_offset - 1, "content"],
227
+ anchor_offset: 0,
228
+ focus_offset: 0
229
+ })
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Builds a session from the rendered page. #content is the source of truth;
236
+ * an element the allowlist does not recognise fails the whole ingest rather
237
+ * than being guessed at, so the editor stays read-only instead of silently
238
+ * rewriting content it did not understand.
239
+ *
240
+ * @param {Element} element the #content section of the previewed page
241
+ * @returns {{session: Session|null, unrecognised: string[]}}
242
+ */
243
+ export default function create_session(element) {
244
+ const { doc, unrecognised } = ingest(element, generate_id)
245
+
246
+ if (!doc) return { session: null, unrecognised }
247
+
248
+ const filled = fill_document_defaults(doc, document_schema)
249
+ return { session: new Session(document_schema, filled, session_config), unrecognised: [] }
250
+ }
@@ -0,0 +1,128 @@
1
+ // Shared encoding for directive parameters carried in class attributes.
2
+ //
3
+ // The generated HTML is the source of truth for the editor, so a directive
4
+ // that expanded into a subtree (a glob list, a reference card) has to be
5
+ // reconstructible from attributes that are visible and worth styling - not
6
+ // from the original markdown stamped into a data attribute.
7
+ //
8
+ // This module is imported by both plugins/html/index.js (node, emit side)
9
+ // and the browser bundle (ingest side), so it must stay free of node
10
+ // builtins.
11
+
12
+ // Folder segments are joined with "_", so "_" inside a segment is escaped
13
+ // to "--". A literal "--" in a folder name therefore collides with an
14
+ // escaped "_"; that is rarer than a single hyphen, and single hyphens pass
15
+ // through untouched, which keeps the common case ("my-blog" -> "_my-blog")
16
+ // clean and styleable. Emit warns on the collision rather than mangling it.
17
+ const SEGMENT_SEPARATOR = "_"
18
+ const ESCAPED_UNDERSCORE = "--"
19
+
20
+ /** @param {string} segment */
21
+ export function escapeSegment(segment) {
22
+ return segment.replaceAll(SEGMENT_SEPARATOR, ESCAPED_UNDERSCORE)
23
+ }
24
+
25
+ /** @param {string} segment */
26
+ export function unescapeSegment(segment) {
27
+ return segment.replaceAll(ESCAPED_UNDERSCORE, SEGMENT_SEPARATOR)
28
+ }
29
+
30
+ /** @param {string} segment */
31
+ export function segmentIsAmbiguous(segment) {
32
+ return segment.includes(ESCAPED_UNDERSCORE)
33
+ }
34
+
35
+ /**
36
+ * Cumulative ancestor classes for a folder, so a stylesheet can target any
37
+ * level: "a/b/c" -> ["_", "_a", "_a_b", "_a_b_c"].
38
+ * @param {string} folder
39
+ */
40
+ export function dirClasses(folder) {
41
+ const segments = folder.split(/[\\/]/).filter(Boolean).map(escapeSegment)
42
+ const cumulative = segments.map((_, index) => {
43
+ return SEGMENT_SEPARATOR + segments.slice(0, index + 1).join(SEGMENT_SEPARATOR)
44
+ })
45
+ return [SEGMENT_SEPARATOR, ...cumulative]
46
+ }
47
+
48
+ /** @param {string[]} classList */
49
+ export function folderFromClasses(classList) {
50
+ const dirClassList = classList.filter(name => name.startsWith(SEGMENT_SEPARATOR))
51
+ if (!dirClassList.length) return null
52
+
53
+ const deepest = dirClassList.reduce((longest, name) => {
54
+ return name.length > longest.length ? name : longest
55
+ }, "")
56
+
57
+ const segments = deepest.slice(1).split(SEGMENT_SEPARATOR).filter(Boolean)
58
+ return segments.map(unescapeSegment).join("/")
59
+ }
60
+
61
+ // Values become part of a class token, so anything that would need CSS
62
+ // escaping is rejected rather than emitted. The caller decides whether to
63
+ // warn or drop the parameter.
64
+ /** @param {string} value */
65
+ export function valueIsSafe(value) {
66
+ return /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(value)
67
+ }
68
+
69
+ /**
70
+ * `view` and `properties` are the table view's parameters
71
+ * (`?view=table&properties=title,description`): one `view-<name>` token
72
+ * and one `property-<name>` token per column, in column order - class
73
+ * order is document order, so the list round-trips without a separator
74
+ * that a property name could contain.
75
+ * @param {{folder: string, recursive?: boolean, limit?: number|string, tag?: string, view?: string|null, properties?: string[]}} params
76
+ */
77
+ export function globClasses({ folder, recursive, limit, tag, view, properties = [] }) {
78
+ const classes = dirClasses(folder)
79
+ const recursiveClass = recursive ? ["recursive"] : []
80
+ const limitClass = limit ? [`limit-${limit}`] : []
81
+ const tagClass = tag && valueIsSafe(tag) ? [`tag-${tag}`] : []
82
+ const viewClass = view && valueIsSafe(view) ? [`view-${view}`] : []
83
+ const propertyClasses = properties.filter(valueIsSafe).map(name => `property-${name}`)
84
+ return [...classes, ...recursiveClass, ...limitClass, ...tagClass, ...viewClass, ...propertyClasses]
85
+ }
86
+
87
+ /**
88
+ * Inverse of globClasses. "limit" is the class spelling of the directive's
89
+ * ?count= parameter.
90
+ * @param {string[]} classList
91
+ */
92
+ export function globParams(classList) {
93
+ const folder = folderFromClasses(classList)
94
+ if (folder === null) return null
95
+
96
+ const limitClass = classList.find(name => name.startsWith("limit-"))
97
+ const tagClass = classList.find(name => name.startsWith("tag-"))
98
+ const viewClass = classList.find(name => name.startsWith("view-"))
99
+ const properties = classList
100
+ .filter(name => name.startsWith("property-"))
101
+ .map(name => name.slice("property-".length))
102
+
103
+ return {
104
+ folder,
105
+ recursive: classList.includes("recursive"),
106
+ limit: limitClass ? limitClass.slice("limit-".length) : null,
107
+ tag: tagClass ? tagClass.slice("tag-".length) : null,
108
+ view: viewClass ? viewClass.slice("view-".length) : null,
109
+ properties
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Rebuilds the markdown directive a glob list expanded from.
115
+ * @param {{folder: string, recursive: boolean, limit: string|null, tag: string|null}} params
116
+ */
117
+ export function globDirective({ folder, recursive, limit, tag, view = null, properties = [] }) {
118
+ // Built with the URL constructor and searchParams, not by hand - the
119
+ // convention for anything url-shaped. `thismessage://` is a base that
120
+ // parses without a real domain (RFC 2557); only the pathname and search
121
+ // are taken back out.
122
+ const url = new URL("/" + [folder, recursive ? "**" : "*"].filter(Boolean).join("/"), "thismessage://")
123
+ if (limit) url.searchParams.set("count", limit)
124
+ if (tag) url.searchParams.set("tag", tag)
125
+ if (view) url.searchParams.set("view", view)
126
+ if (properties.length) url.searchParams.set("properties", properties.join(","))
127
+ return decodeURIComponent(url.pathname + url.search)
128
+ }
@@ -0,0 +1,97 @@
1
+ // <main>'s children -> the page's frontmatter.
2
+ //
3
+ // The inverse of makeFrontmatter in plugins/html/index.js. Each property
4
+ // is its own single-item <dl>, except the ones vowel handles specially,
5
+ // which get their own element identified by itemprop. The title is main's
6
+ // first <h1> - the one piece of data hoisted out of the content - and is
7
+ // written back as a "#" heading rather than a frontmatter key.
8
+
9
+ /** @param {Element} element */
10
+ function scalarFrom(element) {
11
+ const text = element.textContent.trim()
12
+
13
+ if (text === "true") return true
14
+ if (text === "false") return false
15
+ if (text === "null") return null
16
+
17
+ // Number() accepts "" and whitespace, so the emptiness check has to come
18
+ // first or a blank value becomes 0.
19
+ if (text && !Number.isNaN(Number(text))) return Number(text)
20
+
21
+ return text
22
+ }
23
+
24
+ /**
25
+ * A date rendered as <time> carries an ISO datetime, but the author most
26
+ * likely wrote a plain date. Midnight UTC comes back as a date only, so
27
+ * "date: 2026-03-04" stays stable across saves.
28
+ * @param {Element} element
29
+ */
30
+ function dateFrom(element) {
31
+ const datetime = element.getAttribute("datetime")
32
+ if (!datetime) return element.textContent.trim()
33
+ if (datetime.endsWith("T00:00:00.000Z")) return datetime.slice(0, "0000-00-00".length)
34
+ return datetime
35
+ }
36
+
37
+ /** @param {Element} element the <dd> or <li> holding one value */
38
+ function valueFrom(element) {
39
+ const list = element.querySelector(":scope > ul")
40
+ if (list) return [...list.children].map(item => valueFrom(item))
41
+
42
+ const nested = element.querySelector(":scope > dl")
43
+ if (nested) return objectFrom(nested)
44
+
45
+ const time = element.querySelector(":scope > time")
46
+ if (time) return dateFrom(time)
47
+
48
+ const link = element.querySelector(":scope > a[href]")
49
+ if (link) return link.getAttribute("href")
50
+
51
+ return scalarFrom(element)
52
+ }
53
+
54
+ /** @param {Element} list a <dl> of dt/dd pairs */
55
+ function objectFrom(list) {
56
+ const terms = [...list.children]
57
+
58
+ return terms.reduce((accumulated, node, index) => {
59
+ if (node.tagName !== "DT") return accumulated
60
+
61
+ const value = terms[index + 1]
62
+ if (!value || value.tagName !== "DD") return accumulated
63
+
64
+ return { ...accumulated, [node.textContent.trim()]: valueFrom(value) }
65
+ }, {})
66
+ }
67
+
68
+ /**
69
+ * @param {Element} main
70
+ * @returns {{title: string|null, properties: object}}
71
+ */
72
+ export default function readFrontmatter(main) {
73
+ const children = [...main.children]
74
+
75
+ const heading = children.find(child => child.tagName === "H1")
76
+ const date = children.find(child => child.matches('time[itemprop="date"]'))
77
+ const image = children.find(child => child.matches('picture[itemprop="image"]'))
78
+ const description = children.find(child => child.matches('p[itemprop="description"]'))
79
+
80
+ const known = {
81
+ ...(date ? { date: dateFrom(date) } : {}),
82
+ ...(image ? { image: image.getAttribute("data-original") } : {}),
83
+ ...(description ? { description: description.textContent.trim() } : {})
84
+ }
85
+
86
+ // Every remaining single-item <dl> is one generic property. The
87
+ // breadcrumb and contents navs and section#content are skipped by not
88
+ // matching.
89
+ const generic = children
90
+ .filter(child => child.tagName === "DL")
91
+ .reduce((accumulated, list) => ({ ...accumulated, ...objectFrom(list) }), {})
92
+
93
+ return {
94
+ title: heading ? heading.textContent.trim() : null,
95
+ properties: { ...known, ...generic }
96
+ }
97
+ }