Extensions
Everything the editor can do comes from extensions. Three kinds, same as Tiptap:
Node— a block or inline node in the document (paragraph,heading,bulletList).Mark— inline formatting applied to text (bold,code,link).Extension— no schema of its own, just commands/shortcuts/rules (undoRedo,starterKit()).
starterKit()
starterKit() is an aggregate: it contributes no schema itself, it just returns the essential set from addExtensions().
import { starterKit } from "@domphy/editor"
import { createEditor } from "@domphy/editor/domphy"
const editor = createEditor({ extensions: [starterKit()] })| Extension | Commands | Shortcuts | Input rules |
|---|---|---|---|
Document | — | — | — |
Text | — | — | — |
Paragraph | setParagraph() | Mod-Alt-0 | — |
Heading | setHeading({ level }), toggleHeading({ level }) | Mod-Alt-1…Mod-Alt-6 | # … ###### |
Bold | setBold(), toggleBold(), unsetBold() | Mod-b | **text**, __text__ |
Italic | setItalic(), toggleItalic(), unsetItalic() | Mod-i | *text*, _text_ |
Strike | setStrike(), toggleStrike(), unsetStrike() | Mod-Shift-s | ~~text~~ |
Code | setCode(), toggleCode(), unsetCode() | Mod-e | `text` |
Blockquote | setBlockquote(), toggleBlockquote(), unsetBlockquote() | Mod-Shift-b | > |
BulletList | toggleBulletList() | Mod-Shift-8 | - , + , * |
OrderedList | toggleOrderedList() | Mod-Shift-7 | 1. |
ListItem | — | Enter splits, Tab sinks, Shift-Tab lifts | — |
HardBreak | setHardBreak() | Mod-Enter, Shift-Enter | — |
HorizontalRule | setHorizontalRule() | — | ---, *** |
CodeBlock | setCodeBlock({ language }), toggleCodeBlock({ language }) | Mod-Alt-c | ```lang, ~~~lang |
Link | setLink({ href }), toggleLink({ href }), unsetLink() | — | — |
UndoRedo | undo(), redo() | Mod-z, Shift-Mod-z, Mod-y | — |
Mod is Ctrl on Windows and Linux, Cmd on macOS.
Two names are worth knowing because they differ from what you would guess:
Document's schema name is"doc", not"document"— thestarterKit()option key isdocument, but anything addressing the node type (isActive, content expressions) uses"doc".UndoRedoregisters no commands of its own.undo()andredo()are generic engine commands that exist regardless; the extension only carriesdepth/newGroupDelayand the keymap, so dropping it withundoRedo: falseremoves the shortcuts, not the commands.
Configuring the kit
Every key of the starterKit() options object takes that sub-extension's options, or false to leave it out entirely:
starterKit({
heading: { levels: [1, 2, 3] }, // only H1-H3, so only Mod-Alt-1..3 bind
codeBlock: false, // drop code blocks (and their input rules)
undoRedo: { depth: 50, newGroupDelay: 300 },
link: { HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" } },
})Individual extensions are also exported by name, so you can skip the kit and assemble the set yourself. Document, Text and Paragraph are the minimum a working document needs:
import { Bold, Document, Italic, Paragraph, Text } from "@domphy/editor"
const editor = createEditor({ extensions: [Document, Paragraph, Text, Bold, Italic] })configure() vs extend()
configure(options) deep-merges over the result of addOptions() and returns a new instance — the config hooks are untouched. extend(config) replaces hooks; inside a replaced hook, this.parent is the same hook from the extended config, which is how you add to a hook instead of overwriting it:
const HeadingWithBigShortcut = Heading.extend({
addKeyboardShortcuts() {
return {
...this.parent?.(),
"Mod-Alt-9": ({ editor }) => editor.commands.toggleHeading({ level: 1 }),
}
},
})Every config hook is called with this bound to { name, options, storage, editor, parent }.
Writing an extension
A Mark needs a name, how it parses out of HTML, how it renders back, and the commands it contributes:
import { Mark, mergeAttributes } from "@domphy/editor"
const Highlight = Mark.create({
name: "highlight",
parseHTML() {
return [{ tag: "mark" }]
},
renderHTML({ HTMLAttributes }) {
return ["mark", mergeAttributes(HTMLAttributes), 0]
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) =>
commands.toggleMark(this.name),
}
},
addKeyboardShortcuts() {
return { "Mod-Shift-h": ({ editor }) => editor.commands.toggleHighlight() }
},
})Commands are curried: the outer function takes the caller's arguments, the inner one takes CommandProps and returns whether it applied. A command must check dispatch before mutating the transaction — can() runs commands with dispatch: undefined precisely so it can ask "would this work?" without changing anything:
addCommands() {
return {
clearHighlight:
() =>
({ tr, state, dispatch }) => {
const { from, to } = state.selection
if (from === to) return false
if (dispatch) tr.removeMark(from, to, this.name)
return true
},
}
}Node extensions add content, group, and addAttributes. Attributes declare a default and are omitted from JSON when every attribute is at its default:
import { Node, mergeAttributes } from "@domphy/editor"
const Callout = Node.create({
name: "callout",
group: "block",
content: "block+",
defining: true,
addAttributes() {
return { tone: { default: "info" } }
},
parseHTML() {
return [{ tag: "aside[data-callout]" }]
},
renderHTML({ HTMLAttributes }) {
return ["aside", mergeAttributes({ "data-callout": "" }, HTMLAttributes), 0]
},
})priority (default 100, higher runs first) decides ordering when two extensions register the same shortcut or parse rule — Paragraph and Link both use 1000.
Next steps
- API Reference — editor options, the generic command set, and the adapter exports
- Toolbar example — wiring these commands to buttons