feat: update edra to 2.3.0 (#618)

* feat: Add mathematics extension and toolbar commands

This commit introduces the mathematics extension for inline and block expressions. It also adds corresponding toolbar commands and updates the drag handle menu to include "Turn Into" and "Add Node" options. The placeholder styling and blockquote styles have also been refined.

Co-authored-by: best <best@classroomio.com>

* Update Tiptap and KaTeX dependencies

Co-authored-by: best <best@classroomio.com>

* Downgrade svelte-tiptap to v2.2.0

Co-authored-by: best <best@classroomio.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Rotimi Best
2025-12-25 14:22:39 +01:00
committed by GitHub
co-authored by Cursor Agent
parent 50d6ee3111
commit 2779be6494
9 changed files with 299 additions and 44 deletions
+21 -21
View File
@@ -37,33 +37,33 @@
"vaul-svelte": "1.0.0-next.7" "vaul-svelte": "1.0.0-next.7"
}, },
"dependencies": { "dependencies": {
"@aarkue/tiptap-math-extension": "^1.4.0",
"@floating-ui/dom": "^1.7.4", "@floating-ui/dom": "^1.7.4",
"@tiptap/core": "^3.7.1", "@tiptap/core": "^3.10.7",
"@tiptap/extension-bubble-menu": "^3.7.1", "@tiptap/extension-bubble-menu": "^3.10.7",
"@tiptap/extension-code-block-lowlight": "^3.7.1", "@tiptap/extension-code-block-lowlight": "^3.10.7",
"@tiptap/extension-highlight": "^3.7.1", "@tiptap/extension-highlight": "^3.10.7",
"@tiptap/extension-image": "^3.7.1", "@tiptap/extension-image": "^3.10.7",
"@tiptap/extension-list": "^3.7.1", "@tiptap/extension-list": "^3.10.7",
"@tiptap/extension-subscript": "^3.7.1", "@tiptap/extension-mathematics": "^3.10.7",
"@tiptap/extension-superscript": "^3.7.1", "@tiptap/extension-subscript": "^3.10.7",
"@tiptap/extension-table": "^3.7.1", "@tiptap/extension-superscript": "^3.10.7",
"@tiptap/extension-text-align": "^3.7.1", "@tiptap/extension-table": "^3.10.7",
"@tiptap/extension-text-style": "^3.7.1", "@tiptap/extension-text-align": "^3.10.7",
"@tiptap/extension-typography": "^3.7.1", "@tiptap/extension-text-style": "^3.10.7",
"@tiptap/extensions": "^3.7.1", "@tiptap/extension-typography": "^3.10.7",
"@tiptap/pm": "^3.7.1", "@tiptap/extensions": "^3.10.7",
"@tiptap/starter-kit": "^3.7.1", "@tiptap/markdown": "^3.10.7",
"@tiptap/suggestion": "^3.7.1", "@tiptap/pm": "^3.10.7",
"@tiptap/starter-kit": "^3.10.7",
"@tiptap/suggestion": "^3.10.7",
"d3-array": "^3.2.4", "d3-array": "^3.2.4",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"dompurify": "^3.3.0", "dompurify": "^3.3.0",
"katex": "^0.16.22", "katex": "^0.16.25",
"lowlight": "^3.3.0", "lowlight": "^3.3.0",
"svelte-tiptap": "^3.0.0", "svelte-tiptap": "^2.2.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-extension-auto-joiner": "^0.1.3", "tiptap-extension-auto-joiner": "^0.1.3"
"tiptap-markdown": "^0.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"bits-ui": "^2.14.4" "bits-ui": "^2.14.4"
@@ -26,6 +26,10 @@ import Table from '@lucide/svelte/icons/table';
import Underline from '@lucide/svelte/icons/underline'; import Underline from '@lucide/svelte/icons/underline';
import Undo from '@lucide/svelte/icons/undo-2'; import Undo from '@lucide/svelte/icons/undo-2';
import Video from '@lucide/svelte/icons/video'; import Video from '@lucide/svelte/icons/video';
import Radical from '@lucide/svelte/icons/radical';
import SquareRadical from '@lucide/svelte/icons/square-radical';
import { isTextSelection } from '@tiptap/core';
import Pilcrow from '@lucide/svelte/icons/pilcrow';
import { isMac } from '../utils'; import { isMac } from '../utils';
const commands: Record<string, EdraToolBarCommands[]> = { const commands: Record<string, EdraToolBarCommands[]> = {
@@ -64,6 +68,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 1 }).run(); editor.chain().focus().toggleHeading({ level: 1 }).run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 1 }).run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleHeading({ level: 1 }); return editor.can().toggleHeading({ level: 1 });
}, },
@@ -79,6 +86,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 2 }).run(); editor.chain().focus().toggleHeading({ level: 2 }).run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 2 }).run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleHeading({ level: 2 }); return editor.can().toggleHeading({ level: 2 });
}, },
@@ -94,6 +104,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 3 }).run(); editor.chain().focus().toggleHeading({ level: 3 }).run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 3 }).run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleHeading({ level: 3 }); return editor.can().toggleHeading({ level: 3 });
}, },
@@ -109,6 +122,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 4 }).run(); editor.chain().focus().toggleHeading({ level: 4 }).run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 4 }).run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleHeading({ level: 4 }); return editor.can().toggleHeading({ level: 4 });
}, },
@@ -136,6 +152,24 @@ const commands: Record<string, EdraToolBarCommands[]> = {
return editor.isActive('link'); return editor.isActive('link');
} }
}, },
{
icon: Pilcrow,
name: 'paragraph',
tooltip: 'Paragraph',
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}0`,
onClick: (editor) => {
editor.chain().focus().setParagraph().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setParagraph().run();
},
clickable: (editor) => {
return editor.can().setParagraph();
},
isActive: (editor) => {
return editor.isActive('paragraph');
}
},
{ {
icon: Bold, icon: Bold,
name: 'bold', name: 'bold',
@@ -144,6 +178,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleBold().run(); editor.chain().focus().toggleBold().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('bold').run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleBold(); return editor.can().toggleBold();
}, },
@@ -159,6 +196,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleItalic().run(); editor.chain().focus().toggleItalic().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('italic').run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleItalic(); return editor.can().toggleItalic();
}, },
@@ -174,6 +214,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleUnderline().run(); editor.chain().focus().toggleUnderline().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('underline').run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleUnderline(); return editor.can().toggleUnderline();
}, },
@@ -189,6 +232,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleStrike().run(); editor.chain().focus().toggleStrike().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('strike').run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleStrike(); return editor.can().toggleStrike();
}, },
@@ -204,6 +250,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleBlockquote().run(); editor.chain().focus().toggleBlockquote().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleBlockquote().run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleBlockquote(); return editor.can().toggleBlockquote();
}, },
@@ -219,6 +268,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleCode().run(); editor.chain().focus().toggleCode().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleCodeBlock().run();
},
clickable: (editor) => { clickable: (editor) => {
return editor.can().toggleCode(); return editor.can().toggleCode();
}, },
@@ -320,6 +372,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleBulletList().run(); editor.chain().focus().toggleBulletList().run();
}, },
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleBulletList().run();
},
isActive: (editor) => editor.isActive('bulletList') isActive: (editor) => editor.isActive('bulletList')
}, },
{ {
@@ -330,7 +385,15 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleOrderedList().run(); editor.chain().focus().toggleOrderedList().run();
}, },
isActive: (editor) => editor.isActive('orderedList') turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleOrderedList().run();
},
clickable: (editor) => {
return editor.can().toggleOrderedList();
},
isActive: (editor) => {
return editor.isActive('orderedList');
}
}, },
{ {
icon: ListChecks, icon: ListChecks,
@@ -340,7 +403,15 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => { onClick: (editor) => {
editor.chain().focus().toggleTaskList().run(); editor.chain().focus().toggleTaskList().run();
}, },
isActive: (editor) => editor.isActive('taskList') turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleTaskList().run();
},
clickable: (editor) => {
return editor.can().toggleTaskList();
},
isActive: (editor) => {
return editor.isActive('taskList');
}
} }
], ],
media: [ media: [
@@ -398,6 +469,34 @@ const commands: Record<string, EdraToolBarCommands[]> = {
}, },
isActive: (editor) => editor.isActive('table') isActive: (editor) => editor.isActive('table')
} }
],
math: [
{
icon: Radical,
name: 'mathematics',
tooltip: 'Inline Expression',
onClick: (editor) => {
let latex = 'a^2 + b^2 = c^2';
const chain = editor.chain().focus();
if (isTextSelection(editor.view.state.selection)) {
const { from, to } = editor.view.state.selection;
latex = editor.view.state.doc.textBetween(from, to);
chain.deleteRange({ from, to });
}
chain.insertInlineMath({ latex }).run();
},
isActive: (editor) => editor.isActive('inlineMath')
},
{
icon: SquareRadical,
name: 'mathematics',
tooltip: 'Block Expression',
onClick: (editor) => {
const latex = 'a^2 + b^2 = c^2';
editor.chain().focus().insertBlockMath({ latex }).run();
},
isActive: (editor) => editor.isActive('blockMath')
}
] ]
}; };
@@ -7,6 +7,7 @@ export interface EdraToolBarCommands {
tooltip?: string; tooltip?: string;
shortCut?: string; shortCut?: string;
onClick?: (editor: Editor) => void; onClick?: (editor: Editor) => void;
turnInto?: (editor: Editor, pos: number) => void;
isActive?: (editor: Editor) => boolean; isActive?: (editor: Editor) => boolean;
clickable?: (editor: Editor) => boolean; clickable?: (editor: Editor) => boolean;
} }
+45 -6
View File
@@ -5,11 +5,12 @@
} }
/* For Placeholder */ /* For Placeholder */
.tiptap .is-empty::before {
.tiptap .is-empty:not(blockquote.is-empty)::before {
pointer-events: none; pointer-events: none;
float: left; float: left;
height: 0; height: 0;
color: var(--border-color-hover); color: var(--color-muted-foreground);
content: attr(data-placeholder); content: attr(data-placeholder);
} }
@@ -66,12 +67,12 @@
} }
.tiptap blockquote::before { .tiptap blockquote::before {
content: ''; content: ' ';
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 0.25rem; width: 0.4rem;
background-color: var(--blockquote-border); background-color: var(--blockquote-border);
border-radius: 0.5rem; border-radius: 0.5rem;
} }
@@ -208,8 +209,8 @@ input[type='checkbox'] {
align-items: center; align-items: center;
cursor: grab; cursor: grab;
opacity: 100; opacity: 100;
transition-property: opacity; transition-property: all;
transition-duration: 200ms; transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 1, 1); transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
color: var(--border-color-hover); color: var(--border-color-hover);
} }
@@ -452,3 +453,41 @@ input[type='checkbox'] {
.tiptap iframe { .tiptap iframe {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
} }
/* Mathematics extension styles */
.tiptap .tiptap-mathematics-render {
padding: 0 0.25rem;
border-radius: 0.25rem;
}
/* Editable math block */
.tiptap .tiptap-mathematics-render--editable {
cursor: pointer;
transition: background 0.2s;
}
.tiptap .tiptap-mathematics-render--editable:hover {
background: var(--code-bg);
}
/* Inline math */
.tiptap .tiptap-mathematics-render[data-type='inline-math'] {
display: inline-block;
}
/* Block math */
.tiptap .tiptap-mathematics-render[data-type='block-math'] {
display: block;
margin: 1rem 0;
padding: 0.5rem;
text-align: center;
font-size: 1.25rem;
}
/* Error styles */
.tiptap .tiptap-mathematics-render.inline-math-error,
.tiptap .tiptap-mathematics-render.block-math-error {
background: var(--code-bg);
color: var(--color-destructive);
border: 1px solid darkred;
}
+34 -13
View File
@@ -12,10 +12,12 @@ import SearchAndReplace from './extensions/FindAndReplace';
import { TaskItem, TaskList } from '@tiptap/extension-list'; import { TaskItem, TaskList } from '@tiptap/extension-list';
import { Table, TableCell, TableRow, TableHeader } from './extensions/table'; import { Table, TableCell, TableRow, TableHeader } from './extensions/table';
import { Placeholder } from '@tiptap/extensions'; import { Placeholder } from '@tiptap/extensions';
import { Markdown } from 'tiptap-markdown'; import { Markdown } from '@tiptap/markdown';
import MathExtension from '@aarkue/tiptap-math-extension'; import MathMatics from '@tiptap/extension-mathematics';
import AutoJoiner from 'tiptap-extension-auto-joiner'; import AutoJoiner from 'tiptap-extension-auto-joiner';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import { InlineMathReplacer } from './extensions/InlineMathReplacer';
export default ( export default (
element?: HTMLElement, element?: HTMLElement,
@@ -82,22 +84,41 @@ export default (
nested: true nested: true
}), }),
SearchAndReplace, SearchAndReplace,
MathExtension.configure({ evaluation: true }), InlineMathReplacer,
MathMatics.configure({
blockOptions: {
onClick: (node, pos) => {
const newCalculation = prompt('Enter new calculation:', node.attrs.latex);
if (newCalculation) {
editor
.chain()
.setNodeSelection(pos)
.updateBlockMath({ latex: newCalculation })
.focus()
.run();
}
}
},
inlineOptions: {
onClick: (node, pos) => {
const newCalculation = prompt('Enter new calculation:', node.attrs.latex);
if (newCalculation) {
editor
.chain()
.setNodeSelection(pos)
.updateInlineMath({ latex: newCalculation })
.focus()
.run();
}
}
}
}),
AutoJoiner, AutoJoiner,
Table, Table,
TableHeader, TableHeader,
TableRow, TableRow,
TableCell, TableCell,
Markdown.configure({ Markdown,
html: true,
tightLists: true,
tightListClass: 'tight',
bulletListMarker: '-',
linkify: true,
breaks: true,
transformPastedText: true,
transformCopiedText: false
}),
...(extensions ?? []) ...(extensions ?? [])
], ],
@@ -0,0 +1,21 @@
import { textInputRule } from '@tiptap/core';
import { InlineMath } from '@tiptap/extension-mathematics';
export const InlineMathReplacer = InlineMath.extend({
name: 'inlineMathReplacer',
addInputRules() {
return [
textInputRule({
find: /\$\$([^$]+)\$\$/,
replace: ({ match, commands }) => {
const latex = match[1];
// Insert the inline math node with the LaTeX content
commands.insertInlineMath({
latex
});
return '';
}
})
];
}
});
@@ -42,6 +42,7 @@ export const GROUPS: Group[] = [
actions: [ actions: [
...commands.media, ...commands.media,
...commands.table, ...commands.table,
...commands.math,
{ {
icon: Minus, icon: Minus,
name: 'horizontalRule', name: 'horizontalRule',
@@ -10,7 +10,10 @@
import Duplicate from '@lucide/svelte/icons/copy'; import Duplicate from '@lucide/svelte/icons/copy';
import Clipboard from '@lucide/svelte/icons/clipboard'; import Clipboard from '@lucide/svelte/icons/clipboard';
import Delete from '@lucide/svelte/icons/trash-2'; import Delete from '@lucide/svelte/icons/trash-2';
import Repeat2 from '@lucide/svelte/icons/repeat-2';
import Plus from '@lucide/svelte/icons/plus';
import { NodeSelection } from '@tiptap/pm/state'; import { NodeSelection } from '@tiptap/pm/state';
import commands from '../commands/toolbar-commands';
interface Props { interface Props {
editor: Editor; editor: Editor;
@@ -21,6 +24,9 @@
let currentNode: Node | null = $state(null); let currentNode: Node | null = $state(null);
let currentNodePos: number = $state(-1); let currentNodePos: number = $state(-1);
let open = $state(false); let open = $state(false);
const turnIntoCommand = Object.values(commands)
.flat()
.filter((c) => c.turnInto !== undefined);
const pluginKey = 'globalDragHandle'; const pluginKey = 'globalDragHandle';
@@ -71,10 +77,39 @@
const handleDelete = () => { const handleDelete = () => {
editor.chain().setMeta('hideDragHandle', true).setNodeSelection(currentNodePos).deleteSelection().run(); editor.chain().setMeta('hideDragHandle', true).setNodeSelection(currentNodePos).deleteSelection().run();
}; };
const handleAddNodeNext = () => {
if (currentNodePos === -1) return;
const currentNodeSize = currentNode?.nodeSize || 0;
const insertPos = currentNodePos + currentNodeSize;
const currentNodeIsEmptyParagraph =
currentNode?.type.name === 'paragraph' && currentNode?.content?.size === 0;
const focusPos = currentNodeIsEmptyParagraph ? currentNodePos + 2 : insertPos + 2;
editor
.chain()
.command(({ dispatch, tr, state }) => {
if (dispatch) {
if (currentNodeIsEmptyParagraph) {
tr.insertText('/', currentNodePos, currentNodePos + 1);
} else {
tr.insert(
insertPos,
state.schema.nodes.paragraph.create(null, [state.schema.text('/')])
);
}
return dispatch(tr);
}
return true;
})
.focus(focusPos)
.run();
};
</script> </script>
<div class="drag-handle"> <div class="drag-handle">
<Button variant="ghost" class="ui:rounded-sm ui:p-0 !size-6" onclick={() => (open = true)}> <Button variant="ghost" class="!ui:size-6 ui:rounded-sm ui:p-0" onclick={() => (open = true)}>
<GripVertical /> <GripVertical />
</Button> </Button>
<DropdownMenu.Root bind:open> <DropdownMenu.Root bind:open>
@@ -82,10 +117,43 @@
<span>Drag Handle</span> <span>Drag Handle</span>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content> <DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.GroupHeading class="ui:text-muted-foreground ui:capitalize">
{currentNode?.type.name}
</DropdownMenu.GroupHeading>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger openDelay={300}>
<Repeat2 />
Turn Into
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="ui:max-h-96 ui:overflow-auto ui:duration-300">
{#each turnIntoCommand as command (command)}
{@const Icon = command.icon}
<DropdownMenu.Item
onclick={() => {
if (currentNode && currentNodePos) command.turnInto?.(editor, currentNodePos);
}}
>
<Icon />
<span>{command.tooltip}</span>
<DropdownMenu.Shortcut class="ui:bg-background ui:rounded ui:border ui:p-0.5"
>{command.shortCut}</DropdownMenu.Shortcut
>
</DropdownMenu.Item>
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleAddNodeNext}>
<Plus />
Add Node
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleRemoveFormatting}> <DropdownMenu.Item onclick={handleRemoveFormatting}>
<RemoveFormatting /> <RemoveFormatting />
Remove Formatting Remove Formatting
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDuplicate}> <DropdownMenu.Item onclick={handleDuplicate}>
<Duplicate /> <Duplicate />
Duplicate Node Duplicate Node
@@ -94,6 +162,7 @@
<Clipboard /> <Clipboard />
Copy to clipboard Copy to clipboard
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}> <DropdownMenu.Item onclick={handleDelete}>
<Delete class="ui:text-destructive" /> <Delete class="ui:text-destructive" />
Delete Node Delete Node
@@ -106,7 +106,11 @@
{:else} {:else}
{@const commandGroup = commands[cmd]} {@const commandGroup = commands[cmd]}
{#each commandGroup as command (command)} {#each commandGroup as command (command)}
<ToolBarIcon {editor} {command} /> {#if command.name === 'paragraph'}
<span></span>
{:else}
<ToolBarIcon {editor} {command} />
{/if}
{/each} {/each}
{/if} {/if}
{/each} {/each}