Learn to create auto-formatting rules, handle paste events, and transform content as users type or paste. This lesson covers TipTap's powerful rule system that enables markdown-style shortcuts and intelligent content processing.
By the end of this lesson, you will be able to:
- Understand input rules for markdown-style shortcuts and auto-formatting
- Implement paste rules for content transformation and cleaning
- Create pattern-based text replacement rules
- Handle different paste content types (text, HTML, markdown)
- Build auto-linking functionality for URLs and other patterns
- Complete Lesson 01: Basic Editor Setup
- Complete Lesson 02: Content & Document Structure
- Complete Lesson 03: Commands & Node Positions
- Basic understanding of regular expressions
Implement input rules that trigger formatting based on typing patterns:
import { InputRule } from '@tiptap/core';
// TODO: Create custom input rules extension
const CustomInputRules = Extension.create({
name: 'customInputRules',
addInputRules() {
return [
// Heading rule: # text → heading
new InputRule({
find: /^# (.*)$/,
handler: ({ state, range, match }) => {
const [, text] = match;
const { tr } = state;
tr.delete(range.from, range.to);
tr.setBlockType(range.from, range.from, this.editor.schema.nodes.heading, { level: 1 });
tr.insertText(text);
},
}),
// Bold rule: **text** → bold
new InputRule({
find: /\*\*([^*]+)\*\*$/,
handler: ({ state, range, match }) => {
const [, text] = match;
const { tr } = state;
tr.delete(range.from, range.to);
tr.insertText(text);
tr.addMark(range.from, range.from + text.length, this.editor.schema.marks.bold.create());
},
}),
// TODO: Add more input rules
// - Italic: *text* → italic
// - Blockquote: > text → blockquote
// - Horizontal rule: --- → hr
// - Code: `code` → code mark
];
},
});Create paste rules that process content when pasted:
import { PasteRule } from '@tiptap/core';
const CustomPasteRules = Extension.create({
name: 'customPasteRules',
addPasteRules() {
return [
// Auto-link URLs
new PasteRule({
find: /(https?:\/\/[^\s]+)/g,
handler: ({ state, range, match }) => {
const [url] = match;
const { tr } = state;
// TODO: Implement URL linking
tr.addMark(range.from, range.to, this.editor.schema.marks.link.create({ href: url }));
},
}),
// Clean up pasted HTML
new PasteRule({
find: /<[^>]+>/g,
handler: ({ state, range, match }) => {
// TODO: Implement HTML cleaning logic
const { tr } = state;
tr.delete(range.from, range.to);
},
}),
// TODO: Add more paste rules
// - Email auto-linking
// - Markdown formatting conversion
// - Special character replacement
];
},
});Create more sophisticated input rules with complex patterns:
// TODO: Implement these advanced patterns
// List item rule
const listItemRule = new InputRule({
find: /^(\*|\-|\+) (.*)$/,
handler: ({ state, range, match }) => {
const [, bullet, text] = match;
const { tr } = state;
// Convert to bullet list
tr.delete(range.from, range.to);
tr.setBlockType(range.from, range.from, this.editor.schema.nodes.bulletList);
tr.setBlockType(range.from, range.from, this.editor.schema.nodes.listItem);
tr.insertText(text);
},
});
// Numbered list rule
const numberedListRule = new InputRule({
find: /^(\d+)\. (.*)$/,
handler: ({ state, range, match }) => {
const [, number, text] = match;
const { tr } = state;
// TODO: Implement numbered list creation
tr.delete(range.from, range.to);
tr.setBlockType(range.from, range.from, this.editor.schema.nodes.orderedList);
tr.setBlockType(range.from, range.from, this.editor.schema.nodes.listItem);
tr.insertText(text);
},
});
// Code block rule
const codeBlockRule = new InputRule({
find: /^```([a-z]*)\n(.*)$/s,
handler: ({ state, range, match }) => {
const [, language, code] = match;
const { tr } = state;
// TODO: Implement code block creation
tr.delete(range.from, range.to);
tr.setBlockType(range.from, range.from, this.editor.schema.nodes.codeBlock, { language });
tr.insertText(code);
},
});Implement advanced paste handling with content transformation:
const editor = useEditor({
extensions: [StarterKit, CustomInputRules, CustomPasteRules],
editorProps: {
handlePaste: (view, event, slice) => {
const pastedText = event.clipboardData?.getData('text/plain') || '';
const pastedHtml = event.clipboardData?.getData('text/html') || '';
// TODO: Implement custom paste logic
// Handle markdown content
if (pastedText.includes('**') || pastedText.includes('*') || pastedText.includes('#')) {
return handleMarkdownPaste(pastedText);
}
// Handle URL pasting
if (/^https?:\/\//.test(pastedText.trim())) {
return handleUrlPaste(pastedText);
}
// Handle structured data (JSON, CSV, etc.)
if (pastedText.startsWith('{') || pastedText.includes('\t')) {
return handleStructuredPaste(pastedText);
}
// Let default handling continue
return false;
},
// TODO: Add more paste customization
transformPastedHTML: (html) => {
// Clean up pasted HTML
return html
.replace(/<script[^>]*>.*?<\/script>/gi, '')
.replace(/<style[^>]*>.*?<\/style>/gi, '')
.replace(/style="[^"]*"/gi, '');
},
},
});
// Helper functions for paste handling
const handleMarkdownPaste = (text: string) => {
// TODO: Convert markdown to TipTap format
const converted = text
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
.replace(/^# (.*)$/gm, '<h1>$1</h1>')
.replace(/^## (.*)$/gm, '<h2>$1</h2>');
return editor?.commands.insertContent(converted);
};
const handleUrlPaste = (url: string) => {
// TODO: Create link from pasted URL
const domain = new URL(url).hostname;
return editor?.commands.insertContent(`<a href="${url}">${domain}</a>`);
};Implement tracking to see when rules are triggered:
const [ruleActivity, setRuleActivity] = useState<string[]>([]);
const editor = useEditor({
// ... other config
onTransaction: ({ transaction }) => {
// Check for input rule metadata
if (transaction.getMeta('inputRule')) {
const ruleInfo = transaction.getMeta('inputRule');
setRuleActivity(prev => [...prev.slice(-9), `Input rule: ${ruleInfo.type}`]);
}
// Check for paste rule metadata
if (transaction.getMeta('pasteRule')) {
const ruleInfo = transaction.getMeta('pasteRule');
setRuleActivity(prev => [...prev.slice(-9), `Paste rule: ${ruleInfo.type}`]);
}
},
});-
Input Rules: Pattern-based transformations triggered by typing specific sequences. They use regular expressions to detect patterns and apply formatting automatically.
-
Paste Rules: Content transformations applied when pasting text, HTML, or other content. They can clean, format, or enhance pasted content.
-
Auto-formatting: Automatic content conversion based on user input patterns, enabling markdown-style shortcuts and intelligent text processing.
-
Pattern Matching: Regular expressions that detect when rules should apply. Patterns can be simple (exact matches) or complex (with capture groups).
-
Transaction Metadata: Rules can add metadata to transactions to track their activity and provide debugging information.
-
Rule Priority: Rules are processed in order, and the first matching rule wins. Order matters when multiple rules could match the same pattern.
-
Greedy Patterns: Be careful with regex patterns that might match too much. Use specific patterns and test thoroughly.
-
Rule Conflicts: Multiple rules matching the same pattern can cause conflicts. Design patterns to be mutually exclusive.
-
Performance: Too many complex rules can impact typing performance. Keep rules efficient and consider debouncing.
-
Undo Behavior: Input rules create new undo steps. Users might need to undo twice to remove auto-formatted content.
-
Schema Violations: Rules must respect the editor schema. Invalid transformations will fail or cause errors.
Run the lesson tests to verify your implementation:
pnpm test lesson-04The tests verify that:
- Rule activity tracking displays work
- Paste event monitoring functions
- All UI controls render properly
- Quick reference guide is complete
Continue to Lesson 05: Events & Editor Lifecycle to learn about event handling, transaction hooks, and editor lifecycle management.