@riducms/plugin-richtext 0.1.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.
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import type { FieldAuthoringHost } from "@riducms/plugin";
3
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
4
+ import { $insertNodeToNearestRoot, mergeRegister } from "@lexical/utils";
5
+ import {
6
+ $createParagraphNode,
7
+ $getNodeByKey,
8
+ $getPreviousSelection,
9
+ $getRoot,
10
+ $getSelection,
11
+ $isElementNode,
12
+ $isParagraphNode,
13
+ $isRangeSelection,
14
+ COMMAND_PRIORITY_EDITOR,
15
+ type NodeKey,
16
+ } from "lexical";
17
+ import { useLexicalComposerContext } from "@hvniel/lexical-svelte";
18
+
19
+ import {
20
+ OPEN_RELATIONSHIP_BROWSER_COMMAND,
21
+ REMOVE_RELATIONSHIP_COMMAND,
22
+ } from "@plugin-richtext/menu/rich-text-commands";
23
+ import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
24
+ import {
25
+ createRelationshipNode,
26
+ isRelationshipNode,
27
+ RelationshipNode,
28
+ } from "@plugin-richtext/relationship/rich-text-relationship-node";
29
+
30
+ let {
31
+ authoring,
32
+ config,
33
+ field,
34
+ }: {
35
+ authoring: FieldAuthoringHost | undefined;
36
+ config: RichTextConfig;
37
+ field: SchemaField;
38
+ } = $props();
39
+ const editor = useLexicalComposerContext()[0];
40
+ let browserOpen = $state(false);
41
+ let targetCollection = $state<SchemaCollection>();
42
+ let documentID = $state<string>();
43
+ let browserMode = $state<"edit" | "insert" | "replace">("insert");
44
+ let replaceNodeKey: NodeKey | undefined;
45
+ const relationshipCollections = $derived(
46
+ (authoring?.collections ?? []).filter(
47
+ (collection) =>
48
+ config.relationshipCollections.length === 0 ||
49
+ config.relationshipCollections.includes(collection.slug)
50
+ )
51
+ );
52
+ const browserField = $derived(
53
+ targetCollection === undefined ? undefined : createBrowserField(field, targetCollection)
54
+ );
55
+
56
+ $effect(() => {
57
+ if (!editor.hasNodes([RelationshipNode])) {
58
+ throw new Error("RichTextRelationshipPlugin requires RelationshipNode to be registered");
59
+ }
60
+ return mergeRegister(
61
+ editor.registerCommand(
62
+ OPEN_RELATIONSHIP_BROWSER_COMMAND,
63
+ (payload) => {
64
+ const collection = relationshipCollections.find(
65
+ (candidate) => candidate.slug === payload.collectionSlug
66
+ );
67
+ if (collection === undefined) return false;
68
+ browserMode =
69
+ payload.mode ??
70
+ (payload.nodeKey !== undefined
71
+ ? "replace"
72
+ : payload.documentID !== undefined
73
+ ? "edit"
74
+ : "insert");
75
+ targetCollection = collection;
76
+ documentID = payload.documentID;
77
+ replaceNodeKey = browserMode === "replace" ? payload.nodeKey : undefined;
78
+ browserOpen = true;
79
+ return true;
80
+ },
81
+ COMMAND_PRIORITY_EDITOR
82
+ ),
83
+ editor.registerCommand(
84
+ REMOVE_RELATIONSHIP_COMMAND,
85
+ (nodeKey) => {
86
+ const node = $getNodeByKey(nodeKey);
87
+ if (!isRelationshipNode(node)) return false;
88
+ const next = node.getNextSibling();
89
+ const previous = node.getPreviousSibling();
90
+ node.remove();
91
+ if ($isElementNode(next)) next.selectStart();
92
+ else if ($isElementNode(previous)) previous.selectEnd();
93
+ else {
94
+ const paragraph = $createParagraphNode();
95
+ $getRoot().append(paragraph);
96
+ paragraph.select();
97
+ }
98
+ return true;
99
+ },
100
+ COMMAND_PRIORITY_EDITOR
101
+ )
102
+ );
103
+ });
104
+
105
+ function commit(ids: string[]) {
106
+ const id = ids[0];
107
+ const collection = targetCollection;
108
+ if (id === undefined || collection === undefined) return;
109
+ if (browserMode === "edit") {
110
+ closeBrowser();
111
+ return;
112
+ }
113
+ editor.update(() => {
114
+ if (replaceNodeKey !== undefined) {
115
+ const current = $getNodeByKey(replaceNodeKey);
116
+ if (isRelationshipNode(current)) current.setReference(collection.slug, id);
117
+ return;
118
+ }
119
+ const selection = $getSelection() ?? $getPreviousSelection();
120
+ if (!$isRangeSelection(selection)) $getRoot().selectEnd();
121
+ const focusNode = $isRangeSelection(selection) ? selection.focus.getNode() : undefined;
122
+ const relationship = createRelationshipNode({
123
+ documentID: id,
124
+ relationTo: collection.slug,
125
+ });
126
+ $insertNodeToNearestRoot(relationship);
127
+ if ($isParagraphNode(focusNode) && focusNode.getChildrenSize() === 0) focusNode.remove();
128
+ const paragraph = $createParagraphNode();
129
+ relationship.insertAfter(paragraph);
130
+ paragraph.select();
131
+ });
132
+ closeBrowser();
133
+ }
134
+
135
+ function closeBrowser() {
136
+ browserOpen = false;
137
+ browserMode = "insert";
138
+ documentID = undefined;
139
+ replaceNodeKey = undefined;
140
+ queueMicrotask(() => editor.focus());
141
+ }
142
+
143
+ function createBrowserField(field: SchemaField, collection: SchemaCollection): SchemaField {
144
+ return {
145
+ id: `${field.id}-richtext-relationship`,
146
+ name: `${field.name}Relationship`,
147
+ path: `${field.path}.__relationship`,
148
+ type: "relationship",
149
+ category: "relationship",
150
+ required: false,
151
+ unique: false,
152
+ admin: { label: collection.labels.singular },
153
+ relationship: {
154
+ collectionId: collection.id,
155
+ collectionSlug: collection.slug,
156
+ onDelete: "nullify",
157
+ },
158
+ };
159
+ }
160
+ </script>
161
+
162
+ {#if browserOpen && authoring !== undefined && targetCollection !== undefined && browserField !== undefined}
163
+ {const ReferenceBrowser = authoring.referenceBrowser}
164
+ <ReferenceBrowser
165
+ open
166
+ field={browserField}
167
+ collection={targetCollection}
168
+ hasMany={false}
169
+ selectedIDs={documentID === undefined ? [] : [documentID]}
170
+ {...browserMode === "edit" && documentID !== undefined ? { initialDocumentID: documentID } : {}}
171
+ onCommit={commit}
172
+ onClose={closeBrowser}
173
+ />
174
+ {/if}
@@ -0,0 +1,83 @@
1
+ .ridu-richtext-tags span + span::before {
2
+ margin-inline-end: 0.5rem;
3
+ color: var(--ink-faint);
4
+ content: "·";
5
+ }
6
+
7
+ .ridu-richtext-content ::selection {
8
+ background: color-mix(in srgb, var(--primary) 28%, transparent);
9
+ color: var(--foreground);
10
+ }
11
+
12
+ .ridu-richtext-content > .ridu-richtext-heading:first-child {
13
+ margin-top: 0;
14
+ }
15
+
16
+ .ridu-richtext-list-item::marker {
17
+ color: var(--brand-primary-soft);
18
+ }
19
+
20
+ .ridu-richtext-checklist .ridu-richtext-list-item {
21
+ position: relative;
22
+ padding-inline-start: 1.75rem;
23
+ }
24
+
25
+ .ridu-richtext-checklist .ridu-richtext-list-item::before {
26
+ position: absolute;
27
+ top: 0.42rem;
28
+ inset-inline-start: 0;
29
+ width: 0.9rem;
30
+ height: 0.9rem;
31
+ border: 1px solid var(--border);
32
+ border-radius: 0.25rem;
33
+ content: "";
34
+ }
35
+
36
+ .ridu-richtext-checklist .ridu-richtext-list-item-checked::before {
37
+ border-color: var(--primary);
38
+ background: var(--primary);
39
+ box-shadow: inset 0 0 0 2px var(--background);
40
+ }
41
+
42
+ .ridu-richtext-rule.is-selected {
43
+ border-top-color: color-mix(in srgb, var(--primary) 60%, transparent);
44
+ }
45
+
46
+ /* Lexical toggles this inline; keep the toolbar measurable while preventing interaction. */
47
+ .ridu-richtext-block-toolbar[style*="display: none"] {
48
+ opacity: 0 !important;
49
+ pointer-events: none;
50
+ }
51
+
52
+ /* Lexical positions the menu 4px into the canvas; cancel that offset and center the grip on its rule. */
53
+ .ridu-richtext-block-toolbar {
54
+ margin-inline-start: calc(-4px - 0.4rem);
55
+ }
56
+
57
+ .ridu-richtext-block-grip span {
58
+ width: 2px;
59
+ height: 2px;
60
+ border-radius: 50%;
61
+ background: currentColor;
62
+ }
63
+
64
+ .ridu-richtext-upload-preview.is-loading::after {
65
+ position: absolute;
66
+ inset: 0;
67
+ background: color-mix(in srgb, var(--foreground) 3%, transparent);
68
+ content: "";
69
+ }
70
+
71
+ @keyframes ridu-richtext-menu-in {
72
+ from {
73
+ opacity: 0;
74
+ transform: translateY(0.25rem) scale(0.985);
75
+ }
76
+ }
77
+
78
+ @keyframes ridu-richtext-toolbar-in {
79
+ from {
80
+ opacity: 0;
81
+ transform: translateY(0.2rem) scale(0.985);
82
+ }
83
+ }
@@ -0,0 +1,322 @@
1
+ <script lang="ts">
2
+ import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
3
+ import { mergeRegister } from "@lexical/utils";
4
+ import { getAdminI18n } from "@riducms/plugin";
5
+ import {
6
+ Button,
7
+ Input,
8
+ PopoverContent,
9
+ PopoverRoot,
10
+ PopoverTrigger,
11
+ ToolbarButton,
12
+ ToolbarRoot,
13
+ } from "@riducms/ui";
14
+ import { Portal, useLexicalComposerContext, useLexicalEditable } from "@hvniel/lexical-svelte";
15
+ import {
16
+ $getSelection,
17
+ $isRangeSelection,
18
+ COMMAND_PRIORITY_LOW,
19
+ SELECTION_CHANGE_COMMAND,
20
+ type TextFormatType,
21
+ } from "lexical";
22
+ import { tick } from "svelte";
23
+
24
+ import ToolbarIcon from "@plugin-richtext/toolbar/toolbar-icon.svelte";
25
+
26
+ const editor = useLexicalComposerContext()[0];
27
+ const isEditable = useLexicalEditable();
28
+ const i18n = getAdminI18n();
29
+ const formatButtons: readonly {
30
+ format: TextFormatType;
31
+ icon: "bold" | "code" | "italic" | "strike" | "subscript" | "superscript" | "underline";
32
+ label: string;
33
+ }[] = [
34
+ { format: "bold", icon: "bold", label: i18n.t("plugin.richtext:editor.bold") },
35
+ { format: "italic", icon: "italic", label: i18n.t("plugin.richtext:editor.italic") },
36
+ { format: "underline", icon: "underline", label: i18n.t("plugin.richtext:editor.underline") },
37
+ {
38
+ format: "strikethrough",
39
+ icon: "strike",
40
+ label: i18n.t("plugin.richtext:editor.strikethrough"),
41
+ },
42
+ { format: "subscript", icon: "subscript", label: i18n.t("plugin.richtext:editor.subscript") },
43
+ {
44
+ format: "superscript",
45
+ icon: "superscript",
46
+ label: i18n.t("plugin.richtext:editor.superscript"),
47
+ },
48
+ { format: "code", icon: "code", label: i18n.t("plugin.richtext:editor.inlineCode") },
49
+ ];
50
+ const toolbarButtonClass =
51
+ "size-[28.8px] rounded-[3px] p-0 text-foreground-muted hover:bg-control-hover hover:text-foreground focus-visible:bg-control-hover focus-visible:text-foreground focus-visible:outline-none data-[active]:bg-primary/12 data-[active]:text-brand-primary-soft";
52
+ let visible = $state(false);
53
+ let formats = $state.raw<Set<TextFormatType>>(new Set());
54
+ let link = $state(false);
55
+ let linkURL = $state("");
56
+ let linkOpen = $state(false);
57
+ let linkDraft = $state("");
58
+ let left = $state(0);
59
+ let top = $state(0);
60
+ let toolbar = $state<HTMLDivElement | null>(null);
61
+ let linkInput = $state<HTMLInputElement | null>(null);
62
+ let restoreLinkFocus = true;
63
+
64
+ // @lexical-scope
65
+ function $updateToolbar() {
66
+ if (linkOpen) return;
67
+ const selection = $getSelection();
68
+ const domSelection = editor._window?.getSelection() ?? window.getSelection();
69
+ const root = editor.getRootElement();
70
+ if (
71
+ !isEditable() ||
72
+ editor.isComposing() ||
73
+ !$isRangeSelection(selection) ||
74
+ selection.isCollapsed() ||
75
+ selection.getTextContent().replaceAll("\n", "") === "" ||
76
+ domSelection === null ||
77
+ domSelection.rangeCount === 0 ||
78
+ root === null ||
79
+ !root.contains(domSelection.anchorNode)
80
+ ) {
81
+ visible = false;
82
+ return;
83
+ }
84
+
85
+ formats = new Set(
86
+ formatButtons.map(({ format }) => format).filter((format) => selection.hasFormat(format))
87
+ );
88
+ const anchorNode = selection.anchor.getNode();
89
+ let linkNode = $isLinkNode(anchorNode) ? anchorNode : undefined;
90
+ const parent = anchorNode.getParent();
91
+ if (linkNode === undefined && $isLinkNode(parent)) linkNode = parent;
92
+ link = linkNode !== undefined;
93
+ linkURL = linkNode?.getURL() ?? "";
94
+ visible = true;
95
+ positionToolbar(domSelection.getRangeAt(0).getBoundingClientRect());
96
+ }
97
+
98
+ function positionToolbar(selectionRect: DOMRect) {
99
+ const width = toolbar?.offsetWidth ?? 190;
100
+ const height = toolbar?.offsetHeight ?? 38;
101
+ left = Math.max(
102
+ 12,
103
+ Math.min(
104
+ window.innerWidth - width - 12,
105
+ selectionRect.left + selectionRect.width / 2 - width / 2
106
+ )
107
+ );
108
+ const preferredTop = selectionRect.top - height - 9;
109
+ const maximumTop = window.innerHeight - height - 12;
110
+ top = Math.max(12, Math.min(maximumTop, preferredTop));
111
+ }
112
+
113
+ function updateFromEditor() {
114
+ editor.getEditorState().read($updateToolbar, { editor });
115
+ }
116
+
117
+ function preserveSelection(event: MouseEvent) {
118
+ if (event.target instanceof HTMLInputElement) return;
119
+ event.preventDefault();
120
+ }
121
+
122
+ // @lexical-scope
123
+ function formatText(format: TextFormatType) {
124
+ editor.update(() => {
125
+ const selection = $getSelection();
126
+ if ($isRangeSelection(selection)) selection.formatText(format);
127
+ });
128
+ queueMicrotask(updateFromEditor);
129
+ }
130
+
131
+ async function openLinkEditor() {
132
+ linkDraft = linkURL || "https://";
133
+ restoreLinkFocus = true;
134
+ linkOpen = true;
135
+ await tick();
136
+ window.setTimeout(() => {
137
+ linkInput?.focus();
138
+ linkInput?.select();
139
+ });
140
+ }
141
+
142
+ async function handleLinkOpenChange(open: boolean) {
143
+ if (open) await openLinkEditor();
144
+ else closeLinkEditor();
145
+ }
146
+
147
+ function sanitizeURL(value: string) {
148
+ const trimmed = value.trim();
149
+ if (/^(?:https?:|mailto:|tel:)/i.test(trimmed)) return trimmed;
150
+ return `https://${trimmed}`;
151
+ }
152
+
153
+ function applyLink() {
154
+ if (linkDraft.trim() === "" || linkDraft === "https://") return;
155
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeURL(linkDraft));
156
+ closeLinkEditor();
157
+ queueMicrotask(() => editor.focus());
158
+ }
159
+
160
+ function removeLink() {
161
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
162
+ closeLinkEditor();
163
+ queueMicrotask(() => editor.focus());
164
+ }
165
+
166
+ function closeLinkEditor() {
167
+ linkOpen = false;
168
+ }
169
+
170
+ function handleLinkKeydown(event: KeyboardEvent) {
171
+ if (event.key === "Enter") {
172
+ event.preventDefault();
173
+ applyLink();
174
+ }
175
+ }
176
+
177
+ function focusLinkInput(event: Event) {
178
+ event.preventDefault();
179
+ linkInput?.focus();
180
+ linkInput?.select();
181
+ }
182
+
183
+ function restoreEditorFocus(event: Event) {
184
+ event.preventDefault();
185
+ const shouldRestore = restoreLinkFocus;
186
+ restoreLinkFocus = true;
187
+ if (shouldRestore) queueMicrotask(() => editor.focus());
188
+ }
189
+
190
+ function preserveOutsideFocus(event: Event) {
191
+ restoreLinkFocus = false;
192
+ const target =
193
+ event.target instanceof Element
194
+ ? event.target.closest<HTMLElement>(
195
+ "button, a[href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
196
+ )
197
+ : null;
198
+ if (target !== null) window.setTimeout(() => target.focus());
199
+ }
200
+
201
+ $effect(() => {
202
+ if (toolbar === null) return;
203
+ updateFromEditor();
204
+ });
205
+
206
+ $effect(() => {
207
+ document.addEventListener("selectionchange", updateFromEditor);
208
+ document.addEventListener("scroll", updateFromEditor, true);
209
+ window.addEventListener("resize", updateFromEditor);
210
+ const unregister = mergeRegister(
211
+ editor.registerUpdateListener(({ editorState }) => editorState.read($updateToolbar)),
212
+ editor.registerCommand(
213
+ SELECTION_CHANGE_COMMAND,
214
+ () => {
215
+ $updateToolbar();
216
+ return false;
217
+ },
218
+ COMMAND_PRIORITY_LOW
219
+ )
220
+ );
221
+ return () => {
222
+ unregister();
223
+ document.removeEventListener("selectionchange", updateFromEditor);
224
+ document.removeEventListener("scroll", updateFromEditor, true);
225
+ window.removeEventListener("resize", updateFromEditor);
226
+ };
227
+ });
228
+ </script>
229
+
230
+ {#if visible}
231
+ <Portal>
232
+ <ToolbarRoot
233
+ bind:ref={toolbar}
234
+ class="fixed z-80 gap-[0.1rem] rounded-[4px] border border-control-border bg-popover p-1 shadow-[var(--shadow-popover)] animate-[ridu-richtext-toolbar-in_150ms_ease] motion-reduce:animate-none"
235
+ aria-label={i18n.t("plugin.richtext:editor.formatText")}
236
+ tabindex={-1}
237
+ style={`left: ${left}px; top: ${top}px;`}
238
+ onmousedown={preserveSelection}
239
+ >
240
+ {#each formatButtons as button (button.format)}
241
+ <ToolbarButton
242
+ class={toolbarButtonClass}
243
+ type="button"
244
+ active={formats.has(button.format)}
245
+ aria-label={button.label}
246
+ aria-pressed={formats.has(button.format)}
247
+ onclick={() => formatText(button.format)}
248
+ >
249
+ <ToolbarIcon name={button.icon} />
250
+ </ToolbarButton>
251
+ {/each}
252
+ <span class="mx-[0.15rem] h-[1.15rem] w-px bg-border" aria-hidden="true"></span>
253
+ <PopoverRoot open={linkOpen} onOpenChange={handleLinkOpenChange}>
254
+ <PopoverTrigger>
255
+ {#snippet child({ props })}
256
+ <ToolbarButton
257
+ {...props}
258
+ class={toolbarButtonClass}
259
+ type="button"
260
+ active={link}
261
+ aria-label={i18n.t(
262
+ link ? "plugin.richtext:editor.editLink" : "plugin.richtext:editor.addLink"
263
+ )}
264
+ aria-pressed={link}
265
+ >
266
+ <ToolbarIcon name="link" />
267
+ </ToolbarButton>
268
+ {/snippet}
269
+ </PopoverTrigger>
270
+
271
+ <PopoverContent
272
+ class="grid w-68 gap-[0.45rem] p-[0.65rem]"
273
+ role="dialog"
274
+ aria-label={i18n.t("plugin.richtext:editor.editLink")}
275
+ trapFocus
276
+ side="bottom"
277
+ align="start"
278
+ onmousedown={preserveSelection}
279
+ onOpenAutoFocus={focusLinkInput}
280
+ onCloseAutoFocus={restoreEditorFocus}
281
+ onInteractOutside={preserveOutsideFocus}
282
+ >
283
+ <label
284
+ class="font-mono text-[10px] tracking-[0.12em] text-foreground-faint uppercase"
285
+ for="ridu-richtext-link-input"
286
+ >
287
+ {i18n.t("plugin.richtext:editor.linkURL")}
288
+ </label>
289
+ <Input
290
+ id="ridu-richtext-link-input"
291
+ class="h-[2.2rem] px-[0.65rem] font-mono text-xs"
292
+ bind:ref={linkInput}
293
+ bind:value={linkDraft}
294
+ aria-label={i18n.t("plugin.richtext:editor.linkURL")}
295
+ onkeydown={handleLinkKeydown}
296
+ />
297
+ <div class="flex justify-end gap-[0.35rem]">
298
+ {#if link}<Button
299
+ variant="ghost"
300
+ size="sm"
301
+ class="me-auto h-7 px-[0.55rem] text-[11px] text-destructive hover:text-foreground"
302
+ onclick={removeLink}
303
+ >
304
+ {i18n.t("plugin.richtext:editor.remove")}
305
+ </Button>{/if}
306
+ <Button
307
+ variant="ghost"
308
+ size="sm"
309
+ class="h-7 px-[0.55rem] text-[11px]"
310
+ onclick={closeLinkEditor}
311
+ >
312
+ {i18n.t("plugin.richtext:editor.cancel")}
313
+ </Button>
314
+ <Button size="sm" class="h-7 px-[0.55rem] text-[11px]" onclick={applyLink}>
315
+ {i18n.t("plugin.richtext:editor.apply")}
316
+ </Button>
317
+ </div>
318
+ </PopoverContent>
319
+ </PopoverRoot>
320
+ </ToolbarRoot>
321
+ </Portal>
322
+ {/if}
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ type IconName =
3
+ "bold" | "code" | "italic" | "link" | "strike" | "subscript" | "superscript" | "underline";
4
+
5
+ let { name, size = 15 }: { name: IconName; size?: number } = $props();
6
+ </script>
7
+
8
+ <svg
9
+ width={size}
10
+ height={size}
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ stroke-width="1.8"
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ aria-hidden="true"
18
+ >
19
+ {#if name === "bold"}
20
+ <path d="M7 5h6a4 4 0 0 1 0 8H7z" />
21
+ <path d="M7 13h7a3.5 3.5 0 0 1 0 7H7z" />
22
+ {:else if name === "italic"}
23
+ <path d="M10 5h7" />
24
+ <path d="M7 19h7" />
25
+ <path d="m14 5-4 14" />
26
+ {:else if name === "link"}
27
+ <path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1" />
28
+ <path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1" />
29
+ {:else if name === "strike"}
30
+ <path d="M16 5.5A5.5 5.5 0 0 0 12 4c-2.2 0-4 1.2-4 3s1.6 2.5 4 3" />
31
+ <path d="M8 18.5a6 6 0 0 0 4 1.5c2.2 0 4-1.2 4-3 0-1.4-1-2.2-2.6-2.6" />
32
+ <path d="M4 12h16" />
33
+ {:else if name === "underline"}
34
+ <path d="M7 4v6a5 5 0 0 0 10 0V4" />
35
+ <path d="M5 21h14" />
36
+ {:else if name === "subscript"}
37
+ <path d="m5 6 8 10M13 6 5 16" />
38
+ <path d="M16 17c.5-1 3-1 3 .5 0 1-3 2-3 3h3" />
39
+ {:else if name === "superscript"}
40
+ <path d="m5 8 8 10M13 8 5 18" />
41
+ <path d="M16 4c.5-1 3-1 3 .5 0 1-3 2-3 3h3" />
42
+ {:else if name === "code"}
43
+ <path d="m8 9-3 3 3 3" />
44
+ <path d="m16 9 3 3-3 3" />
45
+ <path d="m14 5-4 14" />
46
+ {/if}
47
+ </svg>