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,339 @@
|
|
|
1
|
+
// HTML -> Svedit document.
|
|
2
|
+
//
|
|
3
|
+
// #content is the source of truth: this walks the rendered DOM and builds
|
|
4
|
+
// the Svedit node graph from it. Nothing here parses markdown.
|
|
5
|
+
//
|
|
6
|
+
// The tag table is a closed allowlist. An element it does not recognise
|
|
7
|
+
// fails the whole ingest rather than being guessed at, because a wrong
|
|
8
|
+
// guess round-trips to markdown that silently replaces the author's
|
|
9
|
+
// content on save.
|
|
10
|
+
|
|
11
|
+
import { globParams, globDirective } from "./directives.js"
|
|
12
|
+
|
|
13
|
+
const TEXT_NODE = 3
|
|
14
|
+
const ELEMENT_NODE = 1
|
|
15
|
+
|
|
16
|
+
// Inline elements that become marks. Marks are ranges over a plain string,
|
|
17
|
+
// so everything below collapses to offsets during the inline walk.
|
|
18
|
+
const MARK_TYPES = {
|
|
19
|
+
STRONG: "strong",
|
|
20
|
+
B: "strong",
|
|
21
|
+
EM: "emphasis",
|
|
22
|
+
I: "emphasis",
|
|
23
|
+
CODE: "inline_code",
|
|
24
|
+
MARK: "highlight",
|
|
25
|
+
DEL: "strikethrough",
|
|
26
|
+
S: "strikethrough",
|
|
27
|
+
A: "link"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HEADING_LEVELS = {
|
|
31
|
+
H1: 1,
|
|
32
|
+
H2: 2,
|
|
33
|
+
H3: 3,
|
|
34
|
+
H4: 4,
|
|
35
|
+
H5: 5,
|
|
36
|
+
H6: 6
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {Element} element */
|
|
40
|
+
function classList(element) {
|
|
41
|
+
return [...element.classList]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A block whose subtree was expanded from a compact directive. Its
|
|
46
|
+
* parameters live in the class attribute (see directives.js); its
|
|
47
|
+
* children are generated and must not be edited as prose.
|
|
48
|
+
* @param {Element} element
|
|
49
|
+
*/
|
|
50
|
+
function expansionDirective(element) {
|
|
51
|
+
const classes = classList(element)
|
|
52
|
+
|
|
53
|
+
// A glob list is a <ul>, or a <table> in its ?view=table form; the
|
|
54
|
+
// parameters are in the classes either way.
|
|
55
|
+
if ((element.tagName === "UL" || element.tagName === "TABLE") && classes.some(name => name.startsWith("_"))) {
|
|
56
|
+
const params = globParams(classes)
|
|
57
|
+
return params && globDirective(params)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (element.tagName === "ARTICLE" && classes.includes("reference")) {
|
|
61
|
+
const link = element.querySelector("a[href]")
|
|
62
|
+
return link && link.getAttribute("href")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Two transforms produce a bare-URL card: the rich a.link-preview built
|
|
66
|
+
// from url metadata, and the plainer <article> fallback.
|
|
67
|
+
if (element.tagName === "A" && classes.includes("link-preview")) {
|
|
68
|
+
return element.getAttribute("href")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (element.tagName === "ARTICLE" && classes.includes("link-preview")) {
|
|
72
|
+
const link = element.querySelector("a[href]")
|
|
73
|
+
return link && link.getAttribute("href")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Flattens inline DOM into Svedit's { content, marks } shape.
|
|
81
|
+
*
|
|
82
|
+
* Marks are mutually exclusive within a property, so nested inline
|
|
83
|
+
* formatting cannot be represented faithfully. Outermost wins: once inside
|
|
84
|
+
* a mark, descendants contribute their text but no further marks.
|
|
85
|
+
*
|
|
86
|
+
* @param {Node} domNode
|
|
87
|
+
* @param {{createMark: (type: string, element: Element) => string}} context
|
|
88
|
+
* @param {boolean} insideMark
|
|
89
|
+
*/
|
|
90
|
+
function inlineFrom(domNode, context, insideMark) {
|
|
91
|
+
if (domNode.nodeType === TEXT_NODE) {
|
|
92
|
+
return { content: domNode.nodeValue, marks: [] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (domNode.nodeType !== ELEMENT_NODE) {
|
|
96
|
+
return { content: "", marks: [] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (domNode.tagName === "BR") {
|
|
100
|
+
return { content: "\n", marks: [] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const markType = insideMark ? null : MARK_TYPES[domNode.tagName]
|
|
104
|
+
const children = [...domNode.childNodes].map(child => {
|
|
105
|
+
return inlineFrom(child, context, insideMark || Boolean(markType))
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const combined = children.reduce((accumulated, result) => {
|
|
109
|
+
const shifted = result.marks.map(mark => ({
|
|
110
|
+
...mark,
|
|
111
|
+
start_offset: mark.start_offset + accumulated.content.length,
|
|
112
|
+
end_offset: mark.end_offset + accumulated.content.length
|
|
113
|
+
}))
|
|
114
|
+
return {
|
|
115
|
+
content: accumulated.content + result.content,
|
|
116
|
+
marks: [...accumulated.marks, ...shifted]
|
|
117
|
+
}
|
|
118
|
+
}, { content: "", marks: [] })
|
|
119
|
+
|
|
120
|
+
if (!markType) return combined
|
|
121
|
+
|
|
122
|
+
const markId = context.createMark(markType, domNode)
|
|
123
|
+
return {
|
|
124
|
+
content: combined.content,
|
|
125
|
+
marks: [{ start_offset: 0, end_offset: combined.content.length, node_id: markId }]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whitespace at a block's edges is layout, not content: the hast
|
|
131
|
+
* conversion leaves a trailing newline inside <li> and <p>, which would
|
|
132
|
+
* serialize back as a blank line and grow the file on every save. Marks
|
|
133
|
+
* are clamped into the trimmed range and dropped if they collapse.
|
|
134
|
+
* @param {{content: string, marks: object[]}} value
|
|
135
|
+
*/
|
|
136
|
+
function trimValue({ content, marks }) {
|
|
137
|
+
const leading = content.length - content.trimStart().length
|
|
138
|
+
const trimmed = content.trim()
|
|
139
|
+
|
|
140
|
+
const clamped = marks
|
|
141
|
+
.map(mark => ({
|
|
142
|
+
...mark,
|
|
143
|
+
start_offset: Math.max(0, Math.min(mark.start_offset - leading, trimmed.length)),
|
|
144
|
+
end_offset: Math.max(0, Math.min(mark.end_offset - leading, trimmed.length))
|
|
145
|
+
}))
|
|
146
|
+
.filter(mark => mark.end_offset > mark.start_offset)
|
|
147
|
+
|
|
148
|
+
return { content: trimmed, marks: clamped, annotations: [] }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @param {Element} element */
|
|
152
|
+
function textValue(element, context) {
|
|
153
|
+
return trimValue(inlineFrom(element, context, false))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @param {Element} element */
|
|
157
|
+
function pictureIn(element) {
|
|
158
|
+
if (element.tagName === "PICTURE") return element
|
|
159
|
+
return element.querySelector("picture")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {Element} element
|
|
164
|
+
* @param {object} context
|
|
165
|
+
* @returns {string|null} the created node's id, or null if unrecognised
|
|
166
|
+
*/
|
|
167
|
+
function blockFrom(element, context) {
|
|
168
|
+
const directive = expansionDirective(element)
|
|
169
|
+
if (directive) {
|
|
170
|
+
return context.create({
|
|
171
|
+
type: "embed",
|
|
172
|
+
directive,
|
|
173
|
+
html: element.outerHTML
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const tag = element.tagName
|
|
178
|
+
|
|
179
|
+
if (tag === "P") {
|
|
180
|
+
// The image transform replaces an <img> in place, so a paragraph whose
|
|
181
|
+
// only element is a <picture> is an image block, not prose.
|
|
182
|
+
const picture = pictureIn(element)
|
|
183
|
+
if (picture && !element.textContent.trim()) return imageFrom(picture, null, element, context)
|
|
184
|
+
|
|
185
|
+
return context.create({
|
|
186
|
+
type: "paragraph",
|
|
187
|
+
content: textValue(element, context)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (HEADING_LEVELS[tag]) {
|
|
192
|
+
return context.create({
|
|
193
|
+
type: "heading",
|
|
194
|
+
level: HEADING_LEVELS[tag],
|
|
195
|
+
content: textValue(element, context)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (tag === "UL" || tag === "OL") {
|
|
200
|
+
const items = [...element.children].map(child => {
|
|
201
|
+
if (child.tagName !== "LI") return null
|
|
202
|
+
return context.create({
|
|
203
|
+
type: "list_item",
|
|
204
|
+
content: textValue(child, context)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
if (items.some(id => !id)) return null
|
|
209
|
+
|
|
210
|
+
return context.create({
|
|
211
|
+
type: "list",
|
|
212
|
+
ordered: tag === "OL",
|
|
213
|
+
items: { nodes: items, marks: [], annotations: [] }
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (tag === "ASIDE" && classList(element).includes("alert")) {
|
|
218
|
+
// > [!NOTE] became <aside class="alert note">, with the marker line
|
|
219
|
+
// replaced by an injected <h2>. The variant is the class; the injected
|
|
220
|
+
// heading is dropped and regenerated on the way out.
|
|
221
|
+
const variant = classList(element).find(name => name !== "alert")
|
|
222
|
+
const body = [...element.children]
|
|
223
|
+
.filter(child => child.tagName !== "H2")
|
|
224
|
+
.map(child => blockFrom(child, context))
|
|
225
|
+
|
|
226
|
+
if (body.some(id => !id)) return null
|
|
227
|
+
|
|
228
|
+
return context.create({
|
|
229
|
+
type: "alert",
|
|
230
|
+
variant: variant || "note",
|
|
231
|
+
body: { nodes: body, marks: [], annotations: [] }
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (tag === "BLOCKQUOTE") {
|
|
236
|
+
const body = [...element.children].map(child => blockFrom(child, context))
|
|
237
|
+
if (body.some(id => !id)) return null
|
|
238
|
+
|
|
239
|
+
return context.create({
|
|
240
|
+
type: "blockquote",
|
|
241
|
+
body: { nodes: body, marks: [], annotations: [] }
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (tag === "PRE") {
|
|
246
|
+
const code = element.querySelector("code")
|
|
247
|
+
const language = code
|
|
248
|
+
? (classList(code).find(name => name.startsWith("language-")) || "").slice("language-".length)
|
|
249
|
+
: ""
|
|
250
|
+
|
|
251
|
+
// textContent strips the highlighter's span tree, which is presentation.
|
|
252
|
+
// The fence supplies its own trailing newline on the way out.
|
|
253
|
+
return context.create({
|
|
254
|
+
type: "code_block",
|
|
255
|
+
language,
|
|
256
|
+
code: (code || element).textContent.replace(/\n+$/, "")
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (tag === "HR") {
|
|
261
|
+
return context.create({ type: "thematic_break" })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (tag === "FIGURE") {
|
|
265
|
+
const picture = pictureIn(element)
|
|
266
|
+
const caption = element.querySelector("figcaption")
|
|
267
|
+
if (picture) return imageFrom(picture, caption, element, context)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (tag === "PICTURE") {
|
|
271
|
+
return imageFrom(element, null, element, context)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
context.unrecognised.push(tag.toLowerCase() + (element.className ? `.${element.className}` : ""))
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {Element} picture
|
|
280
|
+
* @param {Element|null} caption
|
|
281
|
+
* @param {Element} outer the element whose markup is kept for rendering
|
|
282
|
+
*/
|
|
283
|
+
function imageFrom(picture, caption, outer, context) {
|
|
284
|
+
const img = picture.querySelector("img")
|
|
285
|
+
const source = picture.getAttribute("data-original")
|
|
286
|
+
|
|
287
|
+
if (!source) {
|
|
288
|
+
context.unrecognised.push("picture (no data-original)")
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return context.create({
|
|
293
|
+
type: "image",
|
|
294
|
+
source,
|
|
295
|
+
alt: img ? (img.getAttribute("alt") || "") : "",
|
|
296
|
+
caption: caption ? caption.textContent : "",
|
|
297
|
+
html: outer.outerHTML
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Builds a Svedit document from a rendered #content element.
|
|
303
|
+
*
|
|
304
|
+
* @param {Element} contentElement
|
|
305
|
+
* @param {() => string} generateId
|
|
306
|
+
* @returns {{doc: object|null, unrecognised: string[]}}
|
|
307
|
+
*/
|
|
308
|
+
export default function ingest(contentElement, generateId) {
|
|
309
|
+
const nodes = {}
|
|
310
|
+
const unrecognised = []
|
|
311
|
+
|
|
312
|
+
const context = {
|
|
313
|
+
unrecognised,
|
|
314
|
+
create(node) {
|
|
315
|
+
const id = generateId()
|
|
316
|
+
nodes[id] = { ...node, id }
|
|
317
|
+
return id
|
|
318
|
+
},
|
|
319
|
+
createMark(type, element) {
|
|
320
|
+
const properties = type === "link"
|
|
321
|
+
? { href: element.getAttribute("href") || "" }
|
|
322
|
+
: {}
|
|
323
|
+
return context.create({ type, ...properties })
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const body = [...contentElement.children].map(child => blockFrom(child, context))
|
|
328
|
+
|
|
329
|
+
if (unrecognised.length) return { doc: null, unrecognised }
|
|
330
|
+
|
|
331
|
+
const pageId = generateId()
|
|
332
|
+
nodes[pageId] = {
|
|
333
|
+
id: pageId,
|
|
334
|
+
type: "page",
|
|
335
|
+
body: { nodes: body.filter(Boolean), marks: [], annotations: [] }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { doc: { document_id: pageId, nodes }, unrecognised: [] }
|
|
339
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte"
|
|
2
|
+
import Editor from "./Editor.svelte"
|
|
3
|
+
import EditButton from "./EditButton.svelte"
|
|
4
|
+
import SettingsPanel from "./SettingsPanel.svelte"
|
|
5
|
+
import createSession from "./create-session.js"
|
|
6
|
+
import readFrontmatter from "./frontmatter.js"
|
|
7
|
+
import openSocket from "./socket.js"
|
|
8
|
+
import { getSource } from "./source.js"
|
|
9
|
+
|
|
10
|
+
openSocket()
|
|
11
|
+
|
|
12
|
+
// section#content holds the rendered markdown body and nothing else - the
|
|
13
|
+
// header, nav, aside and footer around it are generated, so the editable
|
|
14
|
+
// region is exactly this element. The editor ingests its HTML as the
|
|
15
|
+
// document's source of truth (see ingest.js).
|
|
16
|
+
const content = document.getElementById("content")
|
|
17
|
+
|
|
18
|
+
const noFrontmatter = { title: null, properties: {} }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Swaps the server-rendered body for the editor. A previewed page runs
|
|
22
|
+
* none of this until the button is pressed, so what a reader loads is the
|
|
23
|
+
* page plus one button.
|
|
24
|
+
*
|
|
25
|
+
* The order here is the contract. Ingest reads the rendered DOM, so it
|
|
26
|
+
* runs before anything clears it; the clear runs before mount(), because
|
|
27
|
+
* mount() appends its render anchor *into* the target. Clearing a mount
|
|
28
|
+
* target after mounting detaches that anchor, and every later render then
|
|
29
|
+
* lands in a detached fragment - which leaves section#content empty with
|
|
30
|
+
* no error anywhere.
|
|
31
|
+
*
|
|
32
|
+
* @returns {string[]} the elements ingest did not recognise - empty when
|
|
33
|
+
* the editor started. A page holding one stays read-only rather than
|
|
34
|
+
* being guessed at, because a wrong guess round trips to markdown that
|
|
35
|
+
* silently replaces the author's content on save.
|
|
36
|
+
*/
|
|
37
|
+
function startEditing() {
|
|
38
|
+
const { session, unrecognised } = createSession(content)
|
|
39
|
+
|
|
40
|
+
if (!session) {
|
|
41
|
+
console.warn(
|
|
42
|
+
"[vowel] editor stayed read-only: #content holds elements the ingest allowlist " +
|
|
43
|
+
"does not recognise:", unrecognised
|
|
44
|
+
)
|
|
45
|
+
return unrecognised
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The title and frontmatter live in <main>, alongside section#content
|
|
49
|
+
// rather than inside it. They round trip unchanged for now - reading
|
|
50
|
+
// them is what lets a save reproduce the whole file.
|
|
51
|
+
const main = content.closest("main")
|
|
52
|
+
const frontmatter = main ? readFrontmatter(main) : noFrontmatter
|
|
53
|
+
|
|
54
|
+
// Svedit renders the document itself rather than hydrating over the
|
|
55
|
+
// server output, so the original children go before it mounts.
|
|
56
|
+
content.replaceChildren()
|
|
57
|
+
mount(Editor, { target: content, props: { session, frontmatter } })
|
|
58
|
+
|
|
59
|
+
// Tells the live-reload patch (socket.js) to leave <main> alone: the
|
|
60
|
+
// editor's own content is the newer of the two, and a rebuild it
|
|
61
|
+
// triggered by saving must not replace it under the cursor.
|
|
62
|
+
document.documentElement.dataset.vowelEditing = "true"
|
|
63
|
+
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The panel is mounted and unmounted rather than hidden: it reads
|
|
68
|
+
// settings.md when it appears, so reopening it picks up an edit made
|
|
69
|
+
// elsewhere in the meantime and there is nothing to invalidate.
|
|
70
|
+
const settings = { panel: null }
|
|
71
|
+
|
|
72
|
+
function closeSettings() {
|
|
73
|
+
if (!settings.panel) return
|
|
74
|
+
unmount(settings.panel)
|
|
75
|
+
settings.panel = null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toggleSettings() {
|
|
79
|
+
if (settings.panel) return closeSettings()
|
|
80
|
+
settings.panel = mount(SettingsPanel, {
|
|
81
|
+
target: document.body,
|
|
82
|
+
props: { onclose: closeSettings }
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Gated: the editor is not part of 1.0, and mounts only when the dev
|
|
87
|
+
// server was started with `vowel --editor`. The socket above is the
|
|
88
|
+
// live-reload client and runs regardless.
|
|
89
|
+
if (content && getSource().editor) {
|
|
90
|
+
mount(EditButton, {
|
|
91
|
+
target: document.body,
|
|
92
|
+
props: { onedit: startEditing, onsettings: toggleSettings }
|
|
93
|
+
})
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Node, NodeArrayProperty } from "svedit"
|
|
4
|
+
|
|
5
|
+
const svedit = getContext("svedit")
|
|
6
|
+
|
|
7
|
+
let { path } = $props()
|
|
8
|
+
|
|
9
|
+
const node = $derived(svedit.session.get(path))
|
|
10
|
+
const variant = $derived(node.variant || "note")
|
|
11
|
+
const label = $derived(variant.charAt(0).toUpperCase() + variant.slice(1))
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- Mirrors the html plugin's alert markup, including the heading it
|
|
15
|
+
injects in place of the consumed [!NOTE] marker. -->
|
|
16
|
+
<Node {path}>
|
|
17
|
+
<aside class="alert {variant}">
|
|
18
|
+
<h2 contenteditable="false">{label}</h2>
|
|
19
|
+
<NodeArrayProperty path={[...path, "body"]} />
|
|
20
|
+
</aside>
|
|
21
|
+
</Node>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Node, CustomProperty } from "svedit"
|
|
4
|
+
|
|
5
|
+
const svedit = getContext("svedit")
|
|
6
|
+
|
|
7
|
+
let { path } = $props()
|
|
8
|
+
|
|
9
|
+
const node = $derived(svedit.session.get(path))
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<!-- Read-only for now: the code is a string property, not a text
|
|
13
|
+
property, so it has no inline editing surface yet. -->
|
|
14
|
+
<Node {path}>
|
|
15
|
+
<CustomProperty tag="pre" path={[...path, "code"]}>
|
|
16
|
+
<code contenteditable="false" class={node.language ? `language-${node.language}` : ""}
|
|
17
|
+
>{node.code}</code>
|
|
18
|
+
</CustomProperty>
|
|
19
|
+
</Node>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Node, CustomProperty } from "svedit"
|
|
4
|
+
|
|
5
|
+
const svedit = getContext("svedit")
|
|
6
|
+
|
|
7
|
+
let { path } = $props()
|
|
8
|
+
|
|
9
|
+
const node = $derived(svedit.session.get(path))
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<!-- An expansion of a compact directive. Its children were generated from
|
|
13
|
+
other targets, so it is selectable and deletable as a unit but never
|
|
14
|
+
editable as prose - it collapses back to node.directive on save. -->
|
|
15
|
+
<Node {path}>
|
|
16
|
+
<CustomProperty path={[...path, "directive"]}>
|
|
17
|
+
<div contenteditable="false" class="embed" title={node.directive}>{@html node.html}</div>
|
|
18
|
+
</CustomProperty>
|
|
19
|
+
</Node>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Node, TextProperty } from "svedit"
|
|
4
|
+
|
|
5
|
+
const svedit = getContext("svedit")
|
|
6
|
+
|
|
7
|
+
let { path } = $props()
|
|
8
|
+
|
|
9
|
+
const node = $derived(svedit.session.get(path))
|
|
10
|
+
const tag = $derived(`h${node.level || 2}`)
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<Node {path}>
|
|
14
|
+
<TextProperty {tag} path={[...path, "content"]} placeholder="Heading" />
|
|
15
|
+
</Node>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Node, CustomProperty } from "svedit"
|
|
4
|
+
|
|
5
|
+
const svedit = getContext("svedit")
|
|
6
|
+
|
|
7
|
+
let { path } = $props()
|
|
8
|
+
|
|
9
|
+
const node = $derived(svedit.session.get(path))
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<!-- The served <picture> markup is kept verbatim so edit mode matches the
|
|
13
|
+
page: the responsive derivatives cannot be rebuilt from source + alt. -->
|
|
14
|
+
<Node {path}>
|
|
15
|
+
<CustomProperty path={[...path, "source"]}>
|
|
16
|
+
<div contenteditable="false">{@html node.html}</div>
|
|
17
|
+
</CustomProperty>
|
|
18
|
+
</Node>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
|
|
4
|
+
const svedit = getContext("svedit")
|
|
5
|
+
|
|
6
|
+
let { path, content } = $props()
|
|
7
|
+
|
|
8
|
+
const node = $derived(svedit.session.get(path))
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<!-- A div while editing: an <a> inside contenteditable causes unexpected
|
|
12
|
+
selection behaviour (see svedit's README). -->
|
|
13
|
+
{#if svedit.editable}
|
|
14
|
+
<div class="link" data-node-id={node.id} data-href={node.href}>{content}</div>
|
|
15
|
+
{:else}
|
|
16
|
+
<a id={node.id} data-node-id={node.id} href={node.href}>{content}</a>
|
|
17
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Node, NodeArrayProperty } from "svedit"
|
|
4
|
+
|
|
5
|
+
const svedit = getContext("svedit")
|
|
6
|
+
|
|
7
|
+
let { path } = $props()
|
|
8
|
+
|
|
9
|
+
const node = $derived(svedit.session.get(path))
|
|
10
|
+
const tag = $derived(node.ordered ? "ol" : "ul")
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<Node {path}>
|
|
14
|
+
<NodeArrayProperty {tag} path={[...path, "items"]} />
|
|
15
|
+
</Node>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Node, NodeArrayProperty } from 'svedit';
|
|
3
|
+
|
|
4
|
+
let { path } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<Node {path}>
|
|
8
|
+
<div class="page">
|
|
9
|
+
<NodeArrayProperty class="body-node-array" path={[...path, 'body']} />
|
|
10
|
+
</div>
|
|
11
|
+
</Node>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
:global {
|
|
15
|
+
.body-node-array {
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: 1fr;
|
|
18
|
+
--row: 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</style>
|