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.
- package/README.md +1 -1
- package/bin.js +47 -27
- package/cli.js +15 -0
- package/config.js +93 -26
- package/extractDate.js +70 -6
- package/index.js +14 -5
- package/package.json +39 -8
- package/plugins/assets/index.js +65 -0
- package/plugins/atproto/index.js +132 -0
- package/plugins/atproto/password.js +71 -0
- package/plugins/atproto/publish.js +406 -0
- package/plugins/atproto/record.js +144 -0
- package/plugins/atproto/rkey.js +54 -0
- package/plugins/deploy-cloudflare/index.js +30 -16
- package/plugins/fonts/index.js +18 -21
- package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/index.js +21145 -0
- package/plugins/html/editor/EditButton.svelte +152 -0
- package/plugins/html/editor/Editor.svelte +51 -0
- package/plugins/html/editor/Overlays.svelte +6 -0
- package/plugins/html/editor/SettingsPanel.svelte +397 -0
- package/plugins/html/editor/Toolbar.svelte +644 -0
- package/plugins/html/editor/create-session.js +250 -0
- package/plugins/html/editor/directives.js +128 -0
- package/plugins/html/editor/frontmatter.js +97 -0
- package/plugins/html/editor/ingest.js +339 -0
- package/plugins/html/editor/main.js +94 -0
- package/plugins/html/editor/nodes/Alert.svelte +21 -0
- package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
- package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
- package/plugins/html/editor/nodes/Embed.svelte +19 -0
- package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
- package/plugins/html/editor/nodes/Heading.svelte +15 -0
- package/plugins/html/editor/nodes/Highlight.svelte +11 -0
- package/plugins/html/editor/nodes/Image.svelte +18 -0
- package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
- package/plugins/html/editor/nodes/Link.svelte +17 -0
- package/plugins/html/editor/nodes/List.svelte +15 -0
- package/plugins/html/editor/nodes/ListItem.svelte +9 -0
- package/plugins/html/editor/nodes/Page.svelte +21 -0
- package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
- package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
- package/plugins/html/editor/nodes/Strong.svelte +11 -0
- package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
- package/plugins/html/editor/nodes/index.js +18 -0
- package/plugins/html/editor/rollup.config.js +35 -0
- package/plugins/html/editor/serialize.js +157 -0
- package/plugins/html/editor/settings-file.js +83 -0
- package/plugins/html/editor/socket.js +202 -0
- package/plugins/html/editor/source.js +43 -0
- package/plugins/html/image.js +31 -5
- package/plugins/html/index.js +430 -458
- package/plugins/html/partials.js +299 -0
- package/plugins/html/socialLinks.js +67 -0
- package/plugins/html/writeRules.js +377 -0
- package/plugins/icons/index.js +2 -6
- package/plugins/images/index.js +78 -55
- package/plugins/markdown/brandColors.js +10 -0
- package/plugins/markdown/colorScheme.js +64 -0
- package/plugins/markdown/emoji.js +30 -0
- package/plugins/markdown/frontmatter.js +253 -0
- package/plugins/markdown/index.js +276 -334
- package/plugins/markdown/links.js +97 -0
- package/plugins/markdown/metadata.js +229 -68
- package/plugins/markdown/readRules.js +143 -0
- package/plugins/markdown/typography.js +109 -0
- package/plugins/markdown/visitor.js +143 -0
- package/plugins/publish/index.js +34 -0
- package/plugins/robots/agents.js +79 -0
- package/plugins/robots/index.js +30 -10
- package/plugins/robots/robots.js +48 -0
- package/plugins/styles/index.js +111 -18
- package/plugins/styles/theme.js +116 -0
- package/plugins/urls/index.js +49 -62
- package/plugins/urls/urlMetadata.js +246 -0
- package/plugins/vectors/index.js +5 -9
- package/plugins/xml/entryContent.js +103 -0
- package/plugins/xml/index.js +105 -43
- package/secretPaths.js +170 -0
- package/stylesheets/DefaultStyles.css +367 -437
- package/stylesheets/ResetStyles.css +2 -38
- package/stylesheets/SyntaxHighlightingStyles.css +36 -4
- package/stylesheets/TypographyStyles.css +386 -128
- package/systemPaths.js +48 -0
- package/utils.js +56 -5
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
- package/.github/ISSUE_TEMPLATE/config.yml +0 -7
- package/docs-source/.cache.json +0 -9
- package/docs-source/.obsidian/app.json +0 -3
- package/docs-source/.obsidian/appearance.json +0 -3
- package/docs-source/.obsidian/core-plugins-migration.json +0 -30
- package/docs-source/.obsidian/core-plugins.json +0 -20
- package/docs-source/.obsidian/workspace.json +0 -168
- package/docs-source/.stackblitzrc +0 -3
- package/docs-source/.votive.db +0 -0
- package/docs-source/about.md +0 -3
- package/docs-source/blog/home.md +0 -5
- package/docs-source/blog/url-ui.md +0 -21
- package/docs-source/blog.md +0 -5
- package/docs-source/docs/.votive.db +0 -0
- package/docs-source/docs/deploy.md +0 -67
- package/docs-source/docs/file-structure.md +0 -31
- package/docs-source/docs/folder-settings.md +0 -23
- package/docs-source/docs/home.md +0 -55
- package/docs-source/docs/images.md +0 -10
- package/docs-source/docs/items.md +0 -13
- package/docs-source/docs/pages.md +0 -141
- package/docs-source/docs/settings.md +0 -4
- package/docs-source/docs/styling.md +0 -34
- package/docs-source/docs/taxonomies.md +0 -37
- package/docs-source/features/cards.md +0 -7
- package/docs-source/features/editing.md +0 -7
- package/docs-source/features/emoji.md +0 -7
- package/docs-source/features/frontmatter.md +0 -7
- package/docs-source/features/lists.md +0 -7
- package/docs-source/features/navigation.md +0 -7
- package/docs-source/features/rich-previews.md +0 -7
- package/docs-source/features/robots.md +0 -7
- package/docs-source/features/rss.md +0 -7
- package/docs-source/features/sitemap.md +0 -7
- package/docs-source/features/speed.md +0 -7
- package/docs-source/features/static.md +0 -7
- package/docs-source/features/taxonomies.md +0 -7
- package/docs-source/home.md +0 -42
- package/docs-source/output/about.html +0 -1
- package/docs-source/output/blog/url-ui.html +0 -8
- package/docs-source/output/blog.html +0 -1
- package/docs-source/output/default.css +0 -1
- package/docs-source/output/docs/deploy.html +0 -34
- package/docs-source/output/docs/file-structure.html +0 -24
- package/docs-source/output/docs/folder-settings.html +0 -20
- package/docs-source/output/docs/images.html +0 -2
- package/docs-source/output/docs/items.html +0 -6
- package/docs-source/output/docs/pages.html +0 -101
- package/docs-source/output/docs/styling.html +0 -18
- package/docs-source/output/docs/taxonomies.html +0 -20
- package/docs-source/output/docs.html +0 -28
- package/docs-source/output/features/cards.html +0 -1
- package/docs-source/output/features/editing.html +0 -1
- package/docs-source/output/features/emoji.html +0 -1
- package/docs-source/output/features/frontmatter.html +0 -1
- package/docs-source/output/features/lists.html +0 -1
- package/docs-source/output/features/navigation.html +0 -1
- package/docs-source/output/features/rich-previews.html +0 -1
- package/docs-source/output/features/robots.html +0 -1
- package/docs-source/output/features/rss.html +0 -1
- package/docs-source/output/features/sitemap.html +0 -1
- package/docs-source/output/features/speed.html +0 -1
- package/docs-source/output/features/static.html +0 -1
- package/docs-source/output/features/taxonomies.html +0 -1
- package/docs-source/output/features.html +0 -1
- package/docs-source/output/feed.xml +0 -1
- package/docs-source/output/index.html +0 -21
- package/docs-source/output/reset.css +0 -1
- package/docs-source/output/roadmap.html +0 -87
- package/docs-source/output/robots.txt +0 -14
- package/docs-source/output/sitemap.xml +0 -16
- package/docs-source/output/typography.css +0 -1
- package/docs-source/roadmap.md +0 -98
- package/docs-source/settings.md +0 -13
- package/getMetadata.js +0 -41
- package/jsconfig.json +0 -10
- package/plugins/markdown/robots.js +0 -18
- package/regex.js +0 -36
- 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
|
+
}
|