Headless rich text editor built on ProseMirror with extensible architecture for building custom editors
94
@tiptap/core provides a comprehensive set of helper functions for document manipulation, content generation, querying, and state inspection. These functions enable advanced document processing and analysis.
Functions for converting content between different formats and creating documents.
/**
* Generate HTML string from JSON document content
* @param doc - JSON document content
* @param extensions - Array of extensions for schema generation
* @returns HTML string representation
*/
function generateHTML(doc: JSONContent, extensions: Extensions): string;
/**
* Generate JSON document from HTML string
* @param content - HTML string to parse
* @param extensions - Array of extensions for schema generation
* @returns JSONContent representation
*/
function generateJSON(content: string, extensions: Extensions): JSONContent;
/**
* Generate plain text from JSON document content
* @param doc - JSON document or ProseMirror Node
* @param options - Text generation options
* @returns Plain text string
*/
function generateText(
doc: JSONContent | ProseMirrorNode,
options?: GenerateTextOptions
): string;
interface GenerateTextOptions {
/** Separator between block elements */
blockSeparator?: string;
/** Include text from specific range */
from?: number;
to?: number;
}Usage Examples:
import { generateHTML, generateJSON, generateText } from '@tiptap/core';
// Convert JSON to HTML
const jsonDoc = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Hello ' },
{ type: 'text', text: 'World', marks: [{ type: 'bold' }] }
]
}
]
};
const html = generateHTML(jsonDoc, extensions);
// '<p>Hello <strong>World</strong></p>'
// Convert HTML to JSON
const htmlContent = '<h1>Title</h1><p>Paragraph with <em>emphasis</em></p>';
const json = generateJSON(htmlContent, extensions);
// Extract plain text
const plainText = generateText(jsonDoc);
// 'Hello World'
const textWithSeparator = generateText(jsonDoc, {
blockSeparator: ' | '
});
// 'Hello World |'
// From ProseMirror node
const text = generateText(editor.state.doc, {
from: 0,
to: 100
});Functions for creating ProseMirror documents and nodes from various content sources.
/**
* Create a ProseMirror document from content
* @param content - Content to convert to document
* @param schema - ProseMirror schema to use
* @param parseOptions - Options for parsing content
* @returns ProseMirror document node
*/
function createDocument(
content: Content,
schema: Schema,
parseOptions?: ParseOptions
): ProseMirrorNode;
/**
* Create ProseMirror node(s) from content
* @param content - Content to convert
* @param schema - ProseMirror schema to use
* @param options - Creation options
* @returns Single node or array of nodes
*/
function createNodeFromContent(
content: Content,
schema: Schema,
options?: CreateNodeFromContentOptions
): ProseMirrorNode | ProseMirrorNode[];
interface ParseOptions {
/** Preserve whitespace */
preserveWhitespace?: boolean | 'full';
/** Parse context */
context?: ResolvedPos;
/** Parse rules to use */
ruleFromNode?: (node: ProseMirrorNode) => ParseRule | null;
/** Top node name */
topNode?: string;
}
interface CreateNodeFromContentOptions extends ParseOptions {
/** Slice content to fragment */
slice?: boolean;
/** Parse as single node */
parseOptions?: ParseOptions;
}
type Content =
| string
| JSONContent
| JSONContent[]
| ProseMirrorNode
| ProseMirrorNode[]
| ProseMirrorFragment;Usage Examples:
import { createDocument, createNodeFromContent } from '@tiptap/core';
// Create document from HTML
const doc = createDocument(
'<h1>Title</h1><p>Content</p>',
schema
);
// Create document from JSON
const jsonContent = {
type: 'doc',
content: [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Title' }] }
]
};
const docFromJson = createDocument(jsonContent, schema);
// Create nodes from content
const nodes = createNodeFromContent(
'<p>Paragraph 1</p><p>Paragraph 2</p>',
schema
);
// Create with options
const nodeWithOptions = createNodeFromContent(
'<pre> Code with spaces </pre>',
schema,
{ parseOptions: { preserveWhitespace: 'full' } }
);
// Create fragment
const fragment = createNodeFromContent(
[
{ type: 'paragraph', content: [{ type: 'text', text: 'First' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'Second' }] }
],
schema,
{ slice: true }
);Functions for finding and extracting content from documents.
/**
* Find child nodes matching a predicate
* @param node - Parent node to search in
* @param predicate - Function to test each child
* @param descend - Whether to search recursively
* @returns Array of nodes with their positions
*/
function findChildren(
node: ProseMirrorNode,
predicate: (child: ProseMirrorNode) => boolean,
descend?: boolean
): NodeWithPos[];
/**
* Find child nodes in a specific range
* @param node - Parent node to search in
* @param range - Range to search within
* @param predicate - Function to test each child
* @param descend - Whether to search recursively
* @returns Array of nodes with their positions
*/
function findChildrenInRange(
node: ProseMirrorNode,
range: { from: number; to: number },
predicate: (child: ProseMirrorNode) => boolean,
descend?: boolean
): NodeWithPos[];
/**
* Find parent node matching predicate
* @param predicate - Function to test parent nodes
* @returns Function that takes selection and returns parent info
*/
function findParentNode(
predicate: (node: ProseMirrorNode) => boolean
): (selection: Selection) => NodeWithPos | null;
/**
* Find parent node closest to a position
* @param $pos - Resolved position
* @param predicate - Function to test parent nodes
* @returns Parent node info if found
*/
function findParentNodeClosestToPos(
$pos: ResolvedPos,
predicate: (node: ProseMirrorNode) => boolean
): NodeWithPos | null;
/**
* Get node at specific position
* @param state - Editor state
* @param pos - Document position
* @returns Node information at position
*/
function getNodeAtPosition(
state: EditorState,
pos: number
): { node: ProseMirrorNode; pos: number; depth: number };
/**
* Get text content between positions
* @param node - Node to extract text from
* @param from - Start position
* @param to - End position
* @param options - Extraction options
* @returns Text content
*/
function getTextBetween(
node: ProseMirrorNode,
from: number,
to: number,
options?: GetTextBetweenOptions
): string;
interface NodeWithPos {
node: ProseMirrorNode;
pos: number;
}
interface GetTextBetweenOptions {
/** Block separator string */
blockSeparator?: string;
/** Text serializers for custom nodes */
textSerializers?: Record<string, (props: { node: ProseMirrorNode }) => string>;
}Usage Examples:
import {
findChildren,
findChildrenInRange,
findParentNode,
getTextBetween
} from '@tiptap/core';
// Find all headings in document
const headings = findChildren(
editor.state.doc,
node => node.type.name === 'heading'
);
headings.forEach(({ node, pos }) => {
console.log(`H${node.attrs.level}: ${node.textContent} at ${pos}`);
});
// Find images in a specific range
const images = findChildrenInRange(
editor.state.doc,
{ from: 100, to: 200 },
node => node.type.name === 'image'
);
// Find all paragraphs recursively
const allParagraphs = findChildren(
editor.state.doc,
node => node.type.name === 'paragraph',
true // descend into nested structures
);
// Find parent list item
const findListItem = findParentNode(node => node.type.name === 'listItem');
const listItemInfo = findListItem(editor.state.selection);
if (listItemInfo) {
console.log('Inside list item at position:', listItemInfo.pos);
}
// Get text between positions
const textContent = getTextBetween(
editor.state.doc,
0,
editor.state.doc.content.size
);
// Custom text extraction
const textWithCustomSeparators = getTextBetween(
editor.state.doc,
0,
100,
{
blockSeparator: '\n\n',
textSerializers: {
image: ({ node }) => `[Image: ${node.attrs.alt || 'Untitled'}]`,
mention: ({ node }) => `@${node.attrs.label}`
}
}
);Functions for analyzing editor state and selection properties.
/**
* Check if node or mark is active
* @param state - Editor state
* @param name - Node/mark name or type
* @param attributes - Optional attributes to match
* @returns Whether the node/mark is active
*/
function isActive(
state: EditorState,
name?: string | NodeType | MarkType,
attributes?: Record<string, any>
): boolean;
/**
* Check if a mark is active
* @param state - Editor state
* @param type - Mark type or name
* @param attributes - Optional attributes to match
* @returns Whether the mark is active
*/
function isMarkActive(
state: EditorState,
type: MarkType | string,
attributes?: Record<string, any>
): boolean;
/**
* Check if a node is active
* @param state - Editor state
* @param type - Node type or name
* @param attributes - Optional attributes to match
* @returns Whether the node is active
*/
function isNodeActive(
state: EditorState,
type: NodeType | string,
attributes?: Record<string, any>
): boolean;
/**
* Check if node is empty
* @param node - Node to check
* @param options - Check options
* @returns Whether the node is considered empty
*/
function isNodeEmpty(
node: ProseMirrorNode,
options?: { ignoreWhitespace?: boolean; checkChildren?: boolean }
): boolean;
/**
* Check if selection is text selection
* @param selection - Selection to check
* @returns Whether selection is TextSelection
*/
function isTextSelection(selection: Selection): selection is TextSelection;
/**
* Check if selection is node selection
* @param selection - Selection to check
* @returns Whether selection is NodeSelection
*/
function isNodeSelection(selection: Selection): selection is NodeSelection;
/**
* Check if cursor is at start of node
* @param state - Editor state
* @param types - Optional node types to check
* @returns Whether cursor is at start of specified node types
*/
function isAtStartOfNode(
state: EditorState,
types?: string[] | string
): boolean;
/**
* Check if cursor is at end of node
* @param state - Editor state
* @param types - Optional node types to check
* @returns Whether cursor is at end of specified node types
*/
function isAtEndOfNode(
state: EditorState,
types?: string[] | string
): boolean;Usage Examples:
import {
isActive,
isMarkActive,
isNodeActive,
isNodeEmpty,
isTextSelection,
isAtStartOfNode
} from '@tiptap/core';
// Check active formatting
const isBold = isMarkActive(editor.state, 'bold');
const isItalic = isMarkActive(editor.state, 'italic');
const isLink = isMarkActive(editor.state, 'link');
// Check active nodes
const isHeading = isNodeActive(editor.state, 'heading');
const isH1 = isNodeActive(editor.state, 'heading', { level: 1 });
const isBlockquote = isNodeActive(editor.state, 'blockquote');
// Generic active check
const isHeadingActive = isActive(editor.state, 'heading');
const isSpecificHeading = isActive(editor.state, 'heading', { level: 2 });
// Check if nodes are empty
const isEmpty = isNodeEmpty(editor.state.doc);
const isEmptyIgnoreSpaces = isNodeEmpty(editor.state.doc, {
ignoreWhitespace: true
});
// Check selection type
if (isTextSelection(editor.state.selection)) {
console.log('Text is selected');
const { from, to } = editor.state.selection;
console.log(`Selection from ${from} to ${to}`);
}
// Check cursor position
const atStart = isAtStartOfNode(editor.state);
const atStartOfParagraph = isAtStartOfNode(editor.state, 'paragraph');
const atStartOfMultiple = isAtStartOfNode(editor.state, ['paragraph', 'heading']);
// UI state management
function getToolbarState() {
const state = editor.state;
return {
bold: isMarkActive(state, 'bold'),
italic: isMarkActive(state, 'italic'),
heading: isNodeActive(state, 'heading'),
canIndent: !isAtStartOfNode(state, 'listItem'),
canOutdent: isNodeActive(state, 'listItem')
};
}Functions for working with node and mark attributes and schema information.
/**
* Get attributes from current selection
* @param state - Editor state
* @param nameOrType - Node/mark name or type
* @returns Attributes object
*/
function getAttributes(
state: EditorState,
nameOrType: string | NodeType | MarkType
): Record<string, any>;
/**
* Get node attributes from selection
* @param state - Editor state
* @param typeOrName - Node type or name
* @returns Node attributes
*/
function getNodeAttributes(
state: EditorState,
typeOrName: NodeType | string
): Record<string, any>;
/**
* Get mark attributes from selection
* @param state - Editor state
* @param typeOrName - Mark type or name
* @returns Mark attributes
*/
function getMarkAttributes(
state: EditorState,
typeOrName: MarkType | string
): Record<string, any>;
/**
* Generate ProseMirror schema from extensions
* @param extensions - Array of extensions
* @returns ProseMirror schema
*/
function getSchema(extensions: Extensions): Schema;
/**
* Get schema from resolved extensions
* @param extensions - Resolved extensions array
* @returns ProseMirror schema
*/
function getSchemaByResolvedExtensions(
extensions: AnyExtension[]
): Schema;
/**
* Split attributes by extension type
* @param extensionAttributes - Extension attributes
* @param typeName - Type name to match
* @param attributes - Attributes to split
* @returns Object with extension and node/mark attributes
*/
function getSplittedAttributes(
extensionAttributes: ExtensionAttribute[],
typeName: string,
attributes: Record<string, any>
): {
extensionAttributes: Record<string, any>;
nodeAttributes: Record<string, any>;
};Usage Examples:
import {
getAttributes,
getNodeAttributes,
getMarkAttributes,
getSchema
} from '@tiptap/core';
// Get current attributes
const headingAttrs = getNodeAttributes(editor.state, 'heading');
// { level: 1, id: 'intro' }
const linkAttrs = getMarkAttributes(editor.state, 'link');
// { href: 'https://example.com', target: '_blank' }
// Generic attribute getting
const attrs = getAttributes(editor.state, 'image');
// { src: 'image.jpg', alt: 'Description', width: 500 }
// Build schema from extensions
const customSchema = getSchema([
// your extensions
]);
// Use attributes in UI
function LinkDialog() {
const linkAttrs = getMarkAttributes(editor.state, 'link');
const [url, setUrl] = useState(linkAttrs.href || '');
const [target, setTarget] = useState(linkAttrs.target || '');
const applyLink = () => {
editor.commands.setMark('link', { href: url, target });
};
return (
<div>
<input value={url} onChange={e => setUrl(e.target.value)} />
<select value={target} onChange={e => setTarget(e.target.value)}>
<option value="">Same window</option>
<option value="_blank">New window</option>
</select>
<button onClick={applyLink}>Apply</button>
</div>
);
}
// Dynamic attribute handling
function updateNodeAttributes(nodeType: string, newAttributes: Record<string, any>) {
const currentAttrs = getNodeAttributes(editor.state, nodeType);
const mergedAttrs = { ...currentAttrs, ...newAttributes };
editor.commands.updateAttributes(nodeType, mergedAttrs);
}Additional helper functions for document processing and analysis.
/**
* Combine transaction steps from multiple transactions
* @param oldTr - Original transaction
* @param newTr - New transaction to combine
* @returns Combined transaction
*/
function combineTransactionSteps(
oldTr: Transaction,
newTr: Transaction
): Transaction;
/**
* Create chainable state for command chaining
* @param options - State and transaction
* @returns Chainable state object
*/
function createChainableState(options: {
state: EditorState;
transaction: Transaction;
}): EditorState;
/**
* Get ranges that were changed by a transaction
* @param tr - Transaction to analyze
* @returns Array of changed ranges
*/
function getChangedRanges(tr: Transaction): {
from: number;
to: number;
newFrom: number;
newTo: number;
}[];
/**
* Get debug representation of document
* @param doc - Document to debug
* @param schema - Schema for type information
* @returns Debug object with structure info
*/
function getDebugJSON(
doc: ProseMirrorNode,
schema: Schema
): Record<string, any>;
/**
* Convert document fragment to HTML
* @param fragment - ProseMirror fragment
* @param schema - Schema for serialization
* @returns HTML string
*/
function getHTMLFromFragment(
fragment: ProseMirrorFragment,
schema: Schema
): string;
/**
* Get DOM rectangle for position range
* @param view - Editor view
* @param from - Start position
* @param to - End position
* @returns DOM rectangle
*/
function posToDOMRect(
view: EditorView,
from: number,
to: number
): DOMRect;Usage Examples:
import {
getChangedRanges,
getDebugJSON,
posToDOMRect
} from '@tiptap/core';
// Track changes in transactions
editor.on('transaction', ({ transaction }) => {
const changes = getChangedRanges(transaction);
changes.forEach(range => {
console.log(`Changed: ${range.from}-${range.to} → ${range.newFrom}-${range.newTo}`);
});
});
// Debug document structure
const debugInfo = getDebugJSON(editor.state.doc, editor.schema);
console.log('Document structure:', debugInfo);
// Get position of selection for UI positioning
function positionTooltip() {
const { from, to } = editor.state.selection;
const rect = posToDOMRect(editor.view, from, to);
const tooltip = document.getElementById('tooltip');
if (tooltip && rect) {
tooltip.style.left = `${rect.left}px`;
tooltip.style.top = `${rect.bottom + 10}px`;
}
}
// Use in selection change handler
editor.on('selectionUpdate', () => {
if (!editor.state.selection.empty) {
positionTooltip();
}
});Install with Tessl CLI
npx tessl i tessl/npm-tiptap--coredocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10