Skip to content

Editor API

The Editor class is the central API for Domternal. It wraps ProseMirror’s EditorView and EditorState with a clean, type-safe interface for creating editors, executing commands, listening to events, and reading content.

import { Editor, Document, Paragraph, Text } from '@domternal/core';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [Document, Paragraph, Text],
content: '<p>Hello world</p>',
});

The constructor requires either extensions or a ProseMirror schema. If you pass extensions, the schema is built automatically. If neither is provided, the constructor throws.

All options are passed to the Editor constructor:

OptionTypeDefaultDescription
elementHTMLElement | nullnullDOM element to mount the editor. Creates a detached <div> if omitted (useful for testing or headless mode).
extensionsAnyExtension[][]Extensions to load. The schema, commands, plugins, and toolbar items are derived from these.
schemaSchema-A ProseMirror schema. Optional if extensions are provided.
contentContentnullInitial content as an HTML string, JSON object, or null for an empty document.
editablebooleantrueWhether the editor is editable.
autofocusFocusPositionfalseFocus the editor on mount. See FocusPosition for accepted values.
clipboardHTMLTransform(html: string) => string-Transform function applied to clipboard HTML on copy/cut. Useful with inlineStyles to auto-apply CSS on copy.
onBeforeCreate(props) => void-Called before the editor is created.
onCreate(props) => void-Called when the editor is ready.
onMount(props) => void-Called when the editor view is mounted to the DOM.
onUpdate(props) => void-Called when the document content changes.
onSelectionUpdate(props) => void-Called when the selection changes (without content change).
onTransaction(props) => void-Called on every transaction (content or selection).
onFocus(props) => void-Called when the editor receives focus.
onBlur(props) => void-Called when the editor loses focus.
onDestroy() => void-Called before the editor is destroyed.
onContentError(props) => void-Called when content does not match the schema.
onError(props) => void-Called when an extension throws an error.

The Content type accepts multiple formats:

type Content = string | JSONContent | JSONContent[] | null;
  • string - HTML string, parsed into ProseMirror nodes
  • JSONContent - A JSON document object (the format returned by editor.getJSON())
  • JSONContent[] - An array of node objects
  • null - Empty document

Controls where the cursor is placed when focusing:

ValueBehavior
true or 'end'Focus at the end of the document
'start'Focus at the beginning
'all'Select all content
numberFocus at a specific document position
false or nullDon’t autofocus

Apply inline styles automatically when users copy content:

import { Editor, inlineStyles } from '@domternal/core';
const editor = new Editor({
extensions: [/* ... */],
clipboardHTMLTransform: inlineStyles,
});

This ensures copied content retains its formatting when pasted into email clients, Google Docs, and other external tools.

PropertyTypeDescription
viewEditorViewThe ProseMirror EditorView instance. Direct access to the underlying view for advanced use cases.
stateEditorStateThe current ProseMirror EditorState (shorthand for editor.view.state).
schemaSchemaThe ProseMirror schema built from extensions.
isEditablebooleanWhether the editor is currently editable.
isEmptybooleanWhether the document is empty.
isFocusedbooleanWhether the editor has DOM focus.
isDestroyedbooleanWhether the editor has been destroyed.
storageRecord<string, unknown>Extension storage, keyed by extension name.
toolbarItemsToolbarItem[]Toolbar items registered by all extensions.

Extensions can store mutable state via addStorage(). Access it from the editor:

// Character count extension stores the count
editor.storage.characterCount.characters();
editor.storage.characterCount.words();
// Code block lowlight stores language list
editor.storage.codeBlockLowlight.listLanguages();

Domternal has three ways to execute commands: single, chained, and dry-run.

Execute a command immediately and return whether it succeeded:

editor.commands.toggleBold(); // true or false
editor.commands.setHeading({ level: 2 });
editor.commands.insertText('Hello');

Batch multiple commands into a single ProseMirror transaction. All changes are applied atomically when you call run():

editor.chain()
.focus()
.insertText('Hello ')
.toggleBold()
.insertText('world')
.toggleBold()
.run();

If any command in the chain fails, the entire chain is not dispatched and run() returns false. You can inspect the failure:

const chain = editor.chain().toggleBold().setHeading({ level: 7 });
if (!chain.run()) {
const failure = chain.getFailure();
// { command: 'setHeading', args: [{ level: 7 }], index: 1 }
}

Insert inline logic with the command() method:

editor.chain()
.focus()
.command(({ tr }) => {
tr.insertText('custom logic');
return true;
})
.run();

Check if commands can execute without modifying the document:

if (editor.can().toggleBold()) {
// Bold can be applied at the current selection
}
// Chain checks
if (editor.can().chain().toggleBold().toggleItalic().run()) {
// Both commands can execute
}

Dry-run mode passes dispatch: undefined to the command. Commands check feasibility and return a boolean without side effects.

These commands are available on every editor, regardless of which extensions are loaded:

CommandArgumentsDescription
focusposition?: FocusPositionFocuses the editor. Optionally places the cursor at the given position.
blur-Removes focus from the editor.
selectAll-Selects the entire document.
selectNodeBackward-Selects the node before the cursor (when at start of a textblock).
deleteSelection-Deletes the current selection. Returns false if the selection is empty.
CommandArgumentsDescription
setContentcontent: Content, options?: SetContentOptionsReplaces the entire document. Options: emitUpdate (default true), parseOptions.
clearContentoptions?: ClearContentOptionsClears the document. Options: emitUpdate (default true).
insertTexttext: stringInserts text at the current selection. Returns false if the parent does not allow inline content.
insertContentcontent: ContentInserts content (HTML string, JSON, or array of JSON nodes) at the current selection.
CommandArgumentsDescription
toggleMarkmarkName: string, attributes?: AttrsToggles a mark. In cursor mode, toggles stored marks. In range mode, applies or removes across the selection.
setMarkmarkName: string, attributes?: AttrsAdds a mark. Merges attributes with existing marks to preserve sibling attributes (e.g., setting fontSize preserves fontFamily on textStyle).
unsetMarkmarkName: stringRemoves a mark from the selection.
unsetAllMarks-Removes all formatting marks. Marks with isFormatting: false (like Link) are preserved. Returns false for empty selections.
CommandArgumentsDescription
setBlockTypenodeName: string, attributes?: AttrsChanges the block type of selected textblocks. Preserves global attributes (textAlign, lineHeight) by merging.
toggleBlockTypenodeName: string, defaultNodeName: string, attributes?: AttrsToggles between a block type and a default type (usually 'paragraph').
wrapInnodeName: string, attributes?: AttrsWraps the selection in a node type (e.g., blockquote).
toggleWrapnodeName: string, attributes?: AttrsToggles wrapping. If already wrapped, lifts out. Supports CellSelection.
lift-Lifts the current block out of its parent wrapper.
CommandArgumentsDescription
toggleListlistNodeName: string, listItemNodeName: string, attributes?: AttrsToggles a list. If not in a list, wraps. If in the same type, lifts out. If in a different type, converts in-place. Handles mixed selections and CellSelection.
CommandArgumentsDescription
updateAttributestypeOrName: string, attributes: Record<string, unknown>Updates attributes on all matching nodes or marks in the selection. Merges with existing attributes.
resetAttributestypeOrName: string, attributeName: stringResets an attribute to its schema default value on all matching nodes or marks.

Extensions add their own commands. Each extension’s documentation lists its commands. For example:

// From Bold extension
editor.commands.toggleBold();
editor.commands.setBold();
editor.commands.unsetBold();
// From Heading extension
editor.commands.setHeading({ level: 2 });
editor.commands.toggleHeading({ level: 3 });
// From Table extension
editor.commands.insertTable({ rows: 3, cols: 3 });
editor.commands.addRowAfter();
editor.commands.deleteTable();

All extension commands are type-safe. TypeScript provides autocomplete for editor.commands.*, editor.chain().*, and editor.can().*.

Commands use a two-layer curried pattern:

// Layer 1: Capture arguments
const setHeading = (attributes?: { level?: number }) =>
// Layer 2: Receive execution context
({ state, tr, dispatch }) => {
const nodeType = state.schema.nodes['heading'];
if (!nodeType) return false;
if (!dispatch) return true; // Dry-run: just check feasibility
const { from, to } = tr.selection;
tr.setBlockType(from, to, nodeType, attributes);
dispatch(tr);
return true;
};

The CommandProps passed to every command:

PropertyTypeDescription
editorEditorThe editor instance.
stateEditorStateCurrent ProseMirror state.
trTransactionThe current transaction. Shared across commands in a chain.
dispatch((tr) => void) | undefinedDispatch function. undefined in dry-run mode.
chain() => ChainedCommandsStart a new command chain from within a command.
can() => CanCommandsCheck command feasibility from within a command.
commandsSingleCommandsAccess other commands from within a command.

In chains, all commands share the same tr. Each command sees changes made by prior commands in the chain. The accumulating dispatch copies steps and metadata (critical for undo/redo) from inner transactions to the shared transaction.

The editor emits typed events. You can listen using either constructor callbacks or the .on() API.

// Register a listener
editor.on('update', ({ editor, transaction }) => {
console.log('Content changed');
});
// Register a one-time listener
editor.once('create', ({ editor }) => {
console.log('Editor ready');
});
// Remove a specific listener
editor.off('update', myHandler);
// Remove all listeners for an event
editor.off('update');
// Remove all listeners
editor.removeAllListeners();
// Check listener count
editor.listenerCount('update'); // number
// Get event names with active listeners
editor.eventNames(); // ('update' | 'create' | ...)[]
const editor = new Editor({
extensions: [/* ... */],
onUpdate({ editor, transaction }) {
console.log('Content changed');
},
onFocus({ editor, event }) {
console.log('Focused');
},
});

Both approaches work simultaneously. Constructor callbacks fire alongside .on() listeners.

EventPayloadWhen it fires
beforeCreate{ editor }Before extensions are initialized. The listener receives a partially initialized editor.
create{ editor }After the editor is fully initialized and ready.
mount{ editor, view }After the ProseMirror view is attached to the DOM element.
unmount{ editor, view }When the editor view is unmounted from the DOM (during destroy).
update{ editor, transaction }When the document content changes. Does not fire if the transaction sets skipUpdate metadata.
selectionUpdate{ editor, transaction }When the selection changes without a content change.
transaction{ editor, transaction }On every transaction, regardless of whether content or selection changed.
focus{ editor, event }When the editor receives DOM focus. event is the native FocusEvent.
blur{ editor, event }When the editor loses DOM focus. event is the native FocusEvent.
paste{ editor, event, slice }When content is pasted. event is the native ClipboardEvent, slice is the ProseMirror Slice.
drop{ editor, event, slice, moved }When content is dropped. event is the native DragEvent, moved is true if the content was moved (not copied).
delete{ editor, from, to }When content is deleted. from and to are the document positions of the deleted range.
destroy-Before the editor is destroyed. No payload.
contentError{ editor, error, content }When initial content does not match the schema. The editor falls back to an empty document.
error{ editor, error, context }When an extension throws an error. context describes where (e.g., 'Bold.onUpdate').
linkEdit{ anchorElement? }When the link editing UI should open (from toolbar link button or Mod-K).
editor.on('update', ({ editor }) => {
const json = editor.getJSON();
saveToDatabase(json);
});
editor.on('update', ({ editor }) => {
const count = editor.storage.characterCount.characters();
document.getElementById('count')!.textContent = `${count} characters`;
});
editor.on('transaction', ({ editor, transaction }) => {
// Runs on every change (content, selection, or both)
updateToolbarState(editor);
});

Checks if a node or mark is active at the current selection:

editor.isActive('bold'); // Mark active?
editor.isActive('heading', { level: 2 }); // In h2?
editor.isActive('textStyle', { color: '#e03131' }); // Specific text color?
// Object form (used internally by toolbar items)
editor.isActive({ name: 'textAlign', attributes: { textAlign: 'center' } });

How it works for marks:

  • Empty selection: checks stored marks or marks at cursor position
  • Range selection: returns true only if ALL applicable text in the selection has the mark. Text inside blocks that don’t allow the mark (like code blocks) or text carrying an excluding mark (like inline code) is skipped.

How it works for nodes:

  • NodeSelection: checks the selected node directly
  • Range selection: the node must be an ancestor of both ends of the selection
  • List nodes: only the innermost list ancestor is considered active, preventing both bulletList and orderedList from showing active when nested

Gets the attributes of the active node or mark:

editor.getAttributes('heading'); // { level: 2 }
editor.getAttributes('link'); // { href: '...', target: '_blank' }
editor.getAttributes('textStyle'); // { color: '#e03131', fontSize: '18px' }

Returns an empty object if the node/mark is not found. For range selections, returns attributes from the start of the selection.

Returns the document as a JSON object:

const json = editor.getJSON();
// {
// type: 'doc',
// content: [
// { type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }
// ]
// }

Returns the document as an HTML string:

const html = editor.getHTML();
// '<p>Hello</p>'

With inline styles for external rendering (email, CMS):

// Apply default light-theme styles
const styled = editor.getHTML({ styled: true });
// With custom overrides
const custom = editor.getHTML({
styled: {
linkColor: '#ff6600',
tableBorder: '2px solid #333',
},
});

The styled option applies structural CSS (borders, padding, fonts, colors) so the HTML renders correctly outside the editor. See inlineStyles for all override keys.

Returns the document as plain text:

const text = editor.getText();
// 'Hello\n\nWorld'
const singleLine = editor.getText({ blockSeparator: ' ' });
// 'Hello World'

Replaces the entire document:

// From HTML
editor.setContent('<p>New content</p>');
// From JSON
editor.setContent({
type: 'doc',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'New' }] }],
});
// Empty document
editor.setContent(null);
// Without emitting update event
editor.setContent('<p>Silent update</p>', false);

Returns false if the content is invalid.

Clears the document to an empty state:

editor.clearContent();
// Without emitting update event
editor.clearContent(false);

Toggles whether the editor is editable:

editor.setEditable(false); // Read-only
editor.setEditable(true); // Editable

Dispatches an empty transaction to trigger ProseMirror’s editability re-evaluation.

Focuses the editor:

editor.focus(); // Focus without changing selection
editor.focus('start'); // Focus at the beginning
editor.focus('end'); // Focus at the end
editor.focus('all'); // Select all
editor.focus(42); // Focus at position 42

Uses TextSelection.near() to find the nearest valid text position. Chainable.

Removes focus:

editor.blur();

Cleans up the editor and releases all resources:

editor.destroy();

Destroy sequence:

  1. Clear pending autofocus timer
  2. Emit destroy event
  3. Call onDestroy callback
  4. Destroy ProseMirror view
  5. Destroy extension manager
  6. Remove all event listeners
  7. Mark as destroyed

After destroy(), the editor instance should not be used. The isDestroyed property returns true.

Add or remove ProseMirror plugins at runtime:

import { Plugin, PluginKey } from '@domternal/pm/state';
const myPluginKey = new PluginKey('myPlugin');
const myPlugin = new Plugin({
key: myPluginKey,
// ...
});
// Register (prevents duplicates automatically)
editor.registerPlugin(myPlugin);
// Unregister
editor.unregisterPlugin(myPluginKey);

This is used internally by framework wrappers (e.g., the Angular BubbleMenu component) to add plugins after the editor is created.

Three functions convert content between formats without creating an editor instance. They work in both browser and Node.js (Node.js requires linkedom as a peer dependency).

Converts JSON content to an HTML string:

import { generateHTML, Document, Paragraph, Text } from '@domternal/core';
const html = generateHTML(
{ type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }] },
[Document, Paragraph, Text]
);
// '<p>Hello</p>'
ParameterTypeDescription
contentJSONContentThe JSON content to convert.
extensionsAnyExtension[]Extensions that define the schema (same ones you use in the editor).
options.documentDocumentCustom DOM implementation. Uses native document in browser, linkedom in Node.js.

Converts an HTML string to JSON content:

import { generateJSON, Document, Paragraph, Text } from '@domternal/core';
const json = generateJSON('<p>Hello</p>', [Document, Paragraph, Text]);
// { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }] }
ParameterTypeDescription
htmlstringThe HTML string to parse.
extensionsAnyExtension[]Extensions that define the schema.
options.documentDocumentCustom DOM implementation.

Extracts plain text from JSON content:

import { generateText, Document, Paragraph, Text } from '@domternal/core';
const text = generateText(jsonContent, [Document, Paragraph, Text]);
// 'Hello\n\nWorld'
const singleLine = generateText(jsonContent, extensions, { blockSeparator: ' ' });
// 'Hello World'
ParameterTypeDescription
contentJSONContentThe JSON content to extract text from.
extensionsAnyExtension[]Extensions that define the schema.
options.blockSeparatorstringString between block elements (default: '\n\n').

Applies inline CSS styles to HTML for external rendering. This is critical for email clients, CMS editors, and Google Docs, which strip <style> tags and class-based styling.

import { inlineStyles } from '@domternal/core';
// Apply default light-theme styles
const styled = inlineStyles(html);
// With custom overrides
const custom = inlineStyles(html, {
blockquoteBorder: '5px solid red',
linkColor: '#ff6600',
tableHeaderBg: '#e0e0e0',
});

An applyInlineStyles(container, overrides?) function is also exported for applying styles directly to a DOM element (used internally by the clipboard serializer).

KeyDefaultDescription
blockquoteBorder'3px solid #6a6a6a'Blockquote left border.
blockquoteColor'#6a6a6a'Blockquote text color.
tableBorder'1px solid #e5e7eb'Table and cell border.
tableHeaderBg'#f8f9fa'Table header cell background.
codeBg'#f0f0f0'Inline code background.
codeFontmonospace stackInline code font.
codeBorder'1px solid #e5e7eb'Inline code border.
codeBlockBg'#f0f0f0'Code block background.
codeBlockFontmonospace stackCode block font.
hrBorder'2px solid #e5e7eb'Horizontal rule border.
linkColor'#2563eb'Link color.
detailsBorder'1px solid #e5e7eb'Details/accordion border.
detailsBg'#f8f9fa'Details/accordion background.
tableColumnWidths'percent'How table column widths are exported: 'percent', 'pixel', or 'none'.
codeHighlighter-Callback for syntax highlighting code blocks. See Code Block Lowlight.

inlineStyles applies CSS to:

  • Blockquotes - left border, color, padding, margin
  • Tables - border-collapse, cell borders and padding, header backgrounds, column widths
  • Code - inline (background, font, border, padding) and blocks (background, font, padding)
  • Links - color and text decoration
  • Headings - font-size, font-weight, margins
  • Lists - margin, padding, task list flex layout with checkboxes
  • Details/Accordion - border, background, cursor on summary
  • Horizontal rules - border style
  • Syntax highlighting - hljs-* classes mapped to GitHub light theme colors

A headless, framework-agnostic state machine for toolbar UI. It manages active states, disabled states, dropdown toggling, and keyboard navigation.

import { ToolbarController } from '@domternal/core';
const controller = new ToolbarController(editor, () => {
// Called whenever toolbar state changes - re-render your UI here
renderToolbar(controller);
});
controller.subscribe(); // Start listening to editor transactions
// Optional: pass a custom layout to reorder/group items
const custom = new ToolbarController(editor, onChange, [
'bold', 'italic', 'underline', '|', 'heading',
]);
PropertyTypeDescription
groupsToolbarGroup[]Toolbar items grouped by their group property.
activeMapReadonlyMap<string, boolean>Active state per button name.
disabledMapReadonlyMap<string, boolean>Disabled state per button name.
expandedMapReadonlyMap<string, boolean>Expanded state for emitEvent buttons (true when their panel is open).
openDropdownstring | nullName of the currently open dropdown.
focusedIndexnumberIndex of the focused button for keyboard navigation.
flatButtonCountnumberTotal number of top-level buttons.
MethodReturnsDescription
isActive(item)booleanCheck if a toolbar button is active.
isDisabled(item)booleanCheck if a toolbar button’s command can execute.
executeCommand(item)voidRun the button’s command.
toggleDropdown(name)voidOpen or close a dropdown.
closeDropdown()voidClose any open dropdown.
navigateNext()numberMove focus to the next button.
navigatePrev()numberMove focus to the previous button.
navigateFirst()numberMove focus to the first button.
navigateLast()numberMove focus to the last button.
setFocusedIndex(idx)voidSet focus to a specific index.
getFlatIndex(name)numberGet the flat index of an item by name.
subscribe()voidStart listening to editor transactions.
destroy()voidClean up and stop listening.
MethodReturnsDescription
ToolbarController.resolveActive(editor, item)booleanCheck if a single item is active.
ToolbarController.executeItem(editor, item)voidExecute a single item’s command.

The editor follows a precise initialization sequence:

  1. beforeCreate event fires
  2. ExtensionManager is created (flattens, deduplicates, sorts, validates extensions)
  3. Extension onBeforeCreate hooks fire
  4. Schema is validated (must have doc and text nodes)
  5. Document is created from content (falls back to empty doc on error, emitting contentError)
  6. Plugins are collected from extensions
  7. EditorState is created
  8. EditorView is created and mounted to the DOM element
  9. mount event fires
  10. CommandManager is created
  11. Autofocus is scheduled (via setTimeout for DOM readiness)
  12. Error handler is wired up
  13. create event fires
  14. Extension onCreate hooks fire

On every ProseMirror transaction:

  1. Transaction is applied to create a new state
  2. View is updated with the new state
  3. transaction event fires, onTransaction callback runs, extension onTransaction hooks fire
  4. If selection changed (no doc change): selectionUpdate event fires, extension onSelectionUpdate hooks fire
  5. If document changed (no skipUpdate meta): update event fires, extension onUpdate hooks fire

If initial content does not match the schema, the editor:

  1. Emits the contentError event with the error and original content
  2. Calls onContentError if provided
  3. Falls back to an empty document
const editor = new Editor({
extensions: [Document, Paragraph, Text],
content: '<table><tr><td>No table extension</td></tr></table>',
onContentError({ error, content }) {
console.warn('Invalid content:', error.message);
// The editor continues with an empty document
},
});

Extension lifecycle hooks are wrapped in error boundaries. If an extension throws, the editor continues working:

const editor = new Editor({
extensions: [/* ... */],
onError({ error, context }) {
// context: 'MyExtension.onUpdate', 'Bold.addProseMirrorPlugins', etc.
console.error(`Extension error in ${context}:`, error);
},
});

The JSON format used by getJSON() and setContent():

interface JSONContent {
type: string; // Node type name
attrs?: Record<string, JSONAttribute>; // Node attributes
content?: JSONContent[]; // Child nodes
marks?: JSONMark[]; // Marks applied to text
text?: string; // Text content (for text nodes)
}
interface JSONMark {
type: string; // Mark type name
attrs?: Record<string, JSONAttribute>; // Mark attributes
}

Example document:

{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 2 },
"content": [
{ "type": "text", "text": "Hello " },
{
"type": "text",
"text": "world",
"marks": [{ "type": "bold" }]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A link",
"marks": [{ "type": "link", "attrs": { "href": "https://domternal.dev" } }]
}
]
}
]
}

These functions are exported from @domternal/core for use in custom extensions, commands, and plugins.

FunctionSignatureDescription
findParentNode(predicate) => (selection) => FindParentNodeResult | undefinedCurried. Finds the closest parent node matching a predicate. Returns { node, pos, start, depth }.
findChildren(node, predicate) => FindChildResult[]Finds all direct children of a node matching a predicate. Returns { node, pos }[].
getMarkRange($pos, type) => MarkRange | undefinedGets the contiguous range { from, to } of a mark around a resolved position.
defaultBlockAt(match) => NodeType | nullFinds the first textblock type that can be created at a given ContentMatch position.
FunctionSignatureDescription
isNodeEmpty(node, options?) => booleanChecks if a ProseMirror node is empty. Options: checkChildren (recursive), ignoreHardBreaks.
isDocumentEmpty(doc) => booleanChecks if a document has no meaningful content (only empty paragraphs).
isValidUrl(url, options?) => booleanValidates a URL string. Options: protocols (default: ['http:', 'https:']). Prevents javascript: URLs.
import { findParentNode, getMarkRange, isNodeEmpty } from '@domternal/core';
// Find the nearest list item ancestor
const listItem = findParentNode((node) => node.type.name === 'listItem')(editor.state.selection);
if (listItem) {
console.log('Inside list item at position', listItem.pos);
}
// Get the range of a link mark at the cursor
const $pos = editor.state.selection.$from;
const linkType = editor.schema.marks['link'];
const range = getMarkRange($pos, linkType);
if (range) {
console.log('Link from', range.from, 'to', range.to);
}

All commands are fully typed through module augmentation. Each extension declares its commands:

declare module '@domternal/core' {
interface RawCommands {
toggleBold: CommandSpec;
setHeading: CommandSpec<[attributes?: { level?: number }]>;
}
}

This gives you autocomplete and type checking on:

  • editor.commands.* - returns boolean
  • editor.chain().* - returns ChainedCommands for chaining
  • editor.can().* - returns boolean
import type {
Editor,
EditorOptions,
EditorEvents,
Content,
JSONContent,
JSONMark,
FocusPosition,
Command,
CommandSpec,
CommandProps,
SingleCommands,
ChainedCommands,
CanCommands,
CanChainedCommands,
ChainFailure,
PasteEventProps,
DropEventProps,
DeleteEventProps,
AnyExtension,
ToolbarItem,
ToolbarButton,
ToolbarDropdown,
ToolbarSeparator,
ToolbarLayoutEntry,
ToolbarGroup,
InlineStyleOverrides,
FindParentNodeResult,
FindChildResult,
MarkRange,
IsNodeEmptyOptions,
IsValidUrlOptions,
} from '@domternal/core';