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,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,9 @@
1
+ <script>
2
+ import { Node, NodeArrayProperty } from "svedit"
3
+
4
+ let { path } = $props()
5
+ </script>
6
+
7
+ <Node {path}>
8
+ <NodeArrayProperty tag="blockquote" path={[...path, "body"]} />
9
+ </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,11 @@
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
+ <em id={node.id} data-node-id={node.id}>{content}</em>
@@ -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,11 @@
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
+ <mark id={node.id} data-node-id={node.id}>{content}</mark>
@@ -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,11 @@
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
+ <code id={node.id} data-node-id={node.id}>{content}</code>
@@ -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,9 @@
1
+ <script>
2
+ import { Node, TextProperty } from "svedit"
3
+
4
+ let { path } = $props()
5
+ </script>
6
+
7
+ <Node {path}>
8
+ <TextProperty tag="li" path={[...path, "content"]} placeholder="List item" />
9
+ </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>
@@ -0,0 +1,9 @@
1
+ <script>
2
+ import { Node, TextProperty } from "svedit"
3
+
4
+ let { path } = $props()
5
+ </script>
6
+
7
+ <Node {path}>
8
+ <TextProperty tag="p" path={[...path, "content"]} placeholder="Paragraph" />
9
+ </Node>
@@ -0,0 +1,11 @@
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
+ <del id={node.id} data-node-id={node.id}>{content}</del>
@@ -0,0 +1,11 @@
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
+ <strong id={node.id} data-node-id={node.id}>{content}</strong>