Documentation Index
Fetch the complete documentation index at: https://mintlify.com/euclidesseg/euclides-workspace/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The schema is the “dictionary” of the editor - it defines what nodes (blocks) and marks (inline styles) exist in your editor. Euclides Rich Editor is built on ProseMirror and extends the basic schema with custom functionality.
Understanding the Schema
The schema consists of two main parts:
Nodes (Blocks)
Nodes are structural elements that create blocks or containers.
Examples: <p>, <h1>, <ul>, <li>, <blockquote>, <pre>
Marks (Inline Styles)
Marks are formatting that applies to text within a block.
Examples: <strong>, <em>, <a>, <code>, <s>
Rule of thumb: If it modifies individual letters or words, use a mark. If it modifies the entire block structure, use a node.
Euclides Schema Implementation
import { NodeSpec, Schema } from 'prosemirror-model';
import { schema as basicSchema } from 'prosemirror-schema-basic';
import { addListNodes } from 'prosemirror-schema-list';
import type { MarkSpec, Node } from 'prosemirror-model'
const paragraph: NodeSpec = {
...schema.spec.nodes.get('paragraph'),
attrs: {
textAlign: { default: 'left' }
},
parseDOM: [
{
tag: "p",
getAttrs: (dom: HTMLElement) => ({
textAlign: dom.style.textAlign || "left"
})
}
],
toDOM(node: Node) {
return [
"p",
{ style: `text-align:${node.attrs['textAlign']}` },
0
]
}
}
const strike: MarkSpec = {
parseDOM: [
{ tag: "s" },
{ tag: "del" },
{ style: "text-decoration=line-through" }
],
toDOM() {
return ["s", 0];
}
};
const nodes = basicSchema.spec.nodes.update("paragraph", paragraph);
export const EuclidesEditorSchema = new Schema({
nodes: addListNodes(nodes, "paragraph block*", "block"),
marks: basicSchema.spec.marks.addToEnd("strike", strike),
});
Source: ~/workspace/source/projects/euclides-rich-editor/src/lib/engine/schema/euclides-schema.ts
What Comes from prosemirror-schema-basic
Built-in Nodes
- ✅
doc - Document root
- ✅
paragraph - Text paragraphs
- ✅
heading - Headings (h1-h6)
- ✅
blockquote - Block quotes
- ✅
horizontal_rule - Horizontal lines
- ✅
code_block - Code blocks
- ✅
image - Images
- ✅
hard_break - Line breaks
- ✅
text - Text content
Built-in Marks
- ✅
strong - Bold text
- ✅
em - Italic text
- ✅
link - Hyperlinks
- ✅
code - Inline code
Adding Custom Marks
Marks modify text within a block without changing the structure.
Example: Strike Mark
Euclides Editor adds strikethrough as a custom mark:
Define the mark specification
Create a MarkSpec that defines how to parse and render the mark:const strike: MarkSpec = {
parseDOM: [
{ tag: "s" },
{ tag: "del" },
{ style: "text-decoration=line-through" }
],
toDOM() {
return ["s", 0];
}
};
parseDOM: Rules for converting HTML to ProseMirror
toDOM: How to render the mark in HTML
0: Placeholder for content
Add to schema
Use addToEnd to add the mark to the marks collection:marks: basicSchema.spec.marks.addToEnd("strike", strike)
Use in commands
Access the mark through the schema:toggleStrike(view: EditorView): boolean {
const command: Command = toggleMark(EuclidesEditorSchema.marks['strike'])
return command(view.state, view.dispatch)
}
Example: Underline Mark
Add underline support to your editor:
const underline: MarkSpec = {
parseDOM: [
{ tag: "u" },
{ style: "text-decoration=underline" }
],
toDOM() {
return ["u", 0];
}
};
// Add to schema
const marks = basicSchema.spec.marks
.addToEnd("strike", strike)
.addToEnd("underline", underline);
export const EuclidesEditorSchema = new Schema({
nodes: addListNodes(nodes, "paragraph block*", "block"),
marks: marks,
});
Example: Text Color Mark
Add color support with attributes:
const textColor: MarkSpec = {
attrs: {
color: { default: "#000000" }
},
parseDOM: [
{
tag: "span[style*=color]",
getAttrs: (dom: HTMLElement) => ({
color: dom.style.color
})
}
],
toDOM(mark) {
return [
"span",
{ style: `color: ${mark.attrs.color}` },
0
];
}
};
Use with a command:
setTextColor(color: string, view: EditorView): boolean {
const { state, dispatch } = view;
const { from, to } = state.selection;
const mark = state.schema.marks.textColor.create({ color });
dispatch(state.tr.addMark(from, to, mark));
return true;
}
Adding Custom Nodes
Nodes create block-level structures or new content types.
Example: Custom Paragraph with Text Align
Euclides Editor extends the paragraph node:
Extend the base node
Start with the default paragraph specification:const paragraph: NodeSpec = {
...schema.spec.nodes.get('paragraph'),
// Add customizations below
}
Add custom attributes
Define new attributes for the node:attrs: {
textAlign: { default: 'left' }
}
Define parsing rules
Convert HTML to ProseMirror:parseDOM: [
{
tag: "p",
getAttrs: (dom: HTMLElement) => ({
textAlign: dom.style.textAlign || "left"
})
}
]
Define rendering rules
Convert ProseMirror to HTML:toDOM(node: Node) {
return [
"p",
{ style: `text-align:${node.attrs['textAlign']}` },
0 // Content placeholder
]
}
Update the schema
Replace the default paragraph node:const nodes = basicSchema.spec.nodes.update("paragraph", paragraph);
Example: Warning Block
Create a custom warning block node:
const warningBlock: NodeSpec = {
group: "block",
content: "inline*",
attrs: {
type: { default: "warning" }
},
parseDOM: [
{
tag: "div.warning",
getAttrs: (dom: HTMLElement) => ({
type: dom.getAttribute("data-type") || "warning"
})
}
],
toDOM(node) {
return [
"div",
{
class: "warning",
"data-type": node.attrs.type
},
0
];
}
};
// Add to nodes
const nodes = basicSchema.spec.nodes
.update("paragraph", paragraph)
.addToEnd("warning_block", warningBlock);
export const EuclidesEditorSchema = new Schema({
nodes: addListNodes(nodes, "paragraph block*", "block"),
marks: basicSchema.spec.marks.addToEnd("strike", strike),
});
Create a command to use it:
toggleWarning(view: EditorView): boolean {
return setBlockType(
EuclidesEditorSchema.nodes['warning_block']
)(view.state, view.dispatch);
}
Integrating List Nodes
Euclides uses addListNodes from prosemirror-schema-list:
import { addListNodes } from 'prosemirror-schema-list';
const nodes = basicSchema.spec.nodes.update("paragraph", paragraph);
export const EuclidesEditorSchema = new Schema({
nodes: addListNodes(nodes, "paragraph block*", "block"),
marks: basicSchema.spec.marks.addToEnd("strike", strike),
});
What this does:
- Adds
bullet_list node
- Adds
ordered_list node
- Adds
list_item node
Parameters:
addListNodes(
nodes, // Existing nodes OrderedMap
"paragraph block*", // List items can contain paragraph + blocks
"block" // Lists are block-level elements
)
Schema Patterns
Node Content Expressions
Define what can be inside a node:
// Only inline content (text and marks)
content: "inline*"
// Only block content
content: "block+"
// Paragraph followed by any blocks
content: "paragraph block*"
// Specific node types
content: "heading paragraph+"
// Empty node
content: ""
Node Groups
Organize nodes into groups:
// Block-level node
group: "block"
// Inline node
group: "inline"
// Multiple groups
group: "block special"
Mark Properties
const customMark: MarkSpec = {
// Mark attributes
attrs: {
level: { default: 1 }
},
// Can this mark span multiple blocks?
spanning: false,
// Can multiple instances exist?
inclusive: true,
// Exclude other marks
excludes: "_", // All marks
excludes: "link", // Specific mark
// Parse from HTML
parseDOM: [
{ tag: "span.custom" }
],
// Render to HTML
toDOM(mark) {
return ["span", { class: "custom" }, 0];
}
};
Best Practices
Start with existing nodes/marks
Extend the basic schema rather than starting from scratch:const paragraph: NodeSpec = {
...schema.spec.nodes.get('paragraph'),
// Your customizations
}
Define both parseDOM and toDOM
Always provide bidirectional conversion:parseDOM: [{ tag: "s" }], // HTML → ProseMirror
toDOM() { return ["s", 0]; } // ProseMirror → HTML
Use meaningful defaults
Provide sensible default values for attributes:attrs: {
textAlign: { default: 'left' },
level: { default: 1 }
}
Test HTML round-tripping
Ensure content survives HTML → ProseMirror → HTML conversion:const html = '<p style="text-align:center">Test</p>';
const doc = DOMParser.fromSchema(schema).parse(html);
const output = DOMSerializer.fromSchema(schema).serializeNode(doc);
// output should match input
Common Customizations
Things NOT in Basic Schema
Marks to add:
- ❌ Strike / strikethrough
- ❌ Underline
- ❌ Text color
- ❌ Background / highlight
- ❌ Font size
- ❌ Font family
Nodes to add:
- ❌ Text alignment (added in Euclides)
- ❌ Tables
- ❌ Video embeds
- ❌ Mentions (@user)
- ❌ Custom blocks (warning, info, success)
- ❌ Cards / previews
- ❌ Columns / layouts
- ❌ Task lists (defined but not implemented)
Advanced: Complete Custom Schema
import { Schema } from 'prosemirror-model';
import { schema as basicSchema } from 'prosemirror-schema-basic';
import { addListNodes } from 'prosemirror-schema-list';
// Custom paragraph with alignment
const paragraph = {
...basicSchema.spec.nodes.get('paragraph'),
attrs: { textAlign: { default: 'left' } },
parseDOM: [{
tag: "p",
getAttrs: (dom: HTMLElement) => ({
textAlign: dom.style.textAlign || "left"
})
}],
toDOM(node: Node) {
return ["p", { style: `text-align:${node.attrs.textAlign}` }, 0]
}
};
// Custom marks
const strike = {
parseDOM: [{ tag: "s" }, { tag: "del" }],
toDOM() { return ["s", 0]; }
};
const underline = {
parseDOM: [{ tag: "u" }],
toDOM() { return ["u", 0]; }
};
const highlight = {
attrs: { color: { default: "yellow" } },
parseDOM: [{
tag: "mark",
getAttrs: (dom: HTMLElement) => ({
color: dom.style.backgroundColor || "yellow"
})
}],
toDOM(mark) {
return ["mark", { style: `background-color: ${mark.attrs.color}` }, 0];
}
};
// Build schema
const nodes = basicSchema.spec.nodes.update("paragraph", paragraph);
const marks = basicSchema.spec.marks
.addToEnd("strike", strike)
.addToEnd("underline", underline)
.addToEnd("highlight", highlight);
export const CustomSchema = new Schema({
nodes: addListNodes(nodes, "paragraph block*", "block"),
marks: marks,
});
Next Steps