Extend the Editor
Guide for adding custom TipTap extensions to the Inklings editor.
Prerequisites
- Familiarize yourself with the TipTap / Loro editor architecture in
apps/desktop/src-react/components/editor/for the editor component tree and LoroSync bridge - Read Working with Loro CRDT for CRDT binary pass-through rules
Overview
The editor uses TipTap (ProseMirror-based) with Loro CRDT for persistent undo/redo. Custom
extensions live in apps/desktop/src-react/components/editor/extensions/.
Existing custom extensions:
| Extension | Type | Purpose |
|---|---|---|
WikiLink | Inline node (atom) | [[Display|slug]] with optional #heading |
PropertyRef | Inline node (atom) | {{name:value}} property references |
ImageBlock | Block node | Image blocks with metadata |
ImageBlockDrop | Plugin | Drag-drop and paste handling for images |
AttachmentNode | Inline node | Generic attachment references |
AttachmentUpload | Plugin | File upload handling |
Step 1: Create the Extension File
Create a new file in apps/desktop/src-react/components/editor/extensions/:
import { Node } from "@tiptap/core";
export const MyExtension = Node.create({ name: "myExtension", group: "inline", // or "block" inline: true, // for inline nodes atom: true, // for non-editable inline nodes
addAttributes() { return { myAttr: { default: null }, }; },
parseHTML() { return [{ tag: "my-extension" }]; },
renderHTML({ HTMLAttributes }) { return ["my-extension", HTMLAttributes]; },});Step 2: Register in the Editor
Add your extension to the TipTap editor configuration in InklingsEditor.tsx:
import { MyExtension } from "./extensions/MyExtension";
const editor = useEditor({ extensions: [ // ... existing extensions MyExtension, ],});Step 3: Add a NodeView (Optional)
For extensions that need React rendering, create a NodeView component:
export function MyExtensionView({ node, editor }: NodeViewProps) { return <span className="my-extension">{node.attrs.myAttr}</span>;}Register it in the extension:
addNodeView() { return ({ node, editor, getPos }) => { const dom = document.createElement("span"); // Vanilla DOM NodeView — see TipTap docs for React NodeView alternative return { dom }; };},For NodeViews that depend on React hook data (e.g., resolved properties), use the extensionOptions mutation pattern.
Step 4: Markdown Round-Trip
If your extension needs to survive markdown serialization, add conversion logic in
apps/desktop/src-react/components/editor/markdownUtils.ts:
- To markdown: Handle your node type in the serializer
- From markdown: Add a regex or parser rule to reconstruct the node
Step 5: Grid Layout Compatibility
If the extension is used in grid layouts, ensure it works with per-cell LoroDoc isolation. Each grid cell has an independent editor instance — extensions must not assume shared state across cells.
Checklist
- Extension file created in
extensions/ - Registered in
InklingsEditor.tsx - NodeView added if needed (vanilla DOM preferred for performance)
- Markdown round-trip works (if applicable)
- Works in both single-editor and grid layout modes
-
pnpm typecheckpasses
Related Documentation
- Working with Loro CRDT — CRDT rules for content storage
- Loro CRDT System — System-level CRDT design
- TipTap Extension Option Mutation — Forcing NodeView re-renders from React data
Was this page helpful?
Thanks for your feedback!