Skip to main content

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:
1

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
2

Add to schema

Use addToEnd to add the mark to the marks collection:
marks: basicSchema.spec.marks.addToEnd("strike", strike)
3

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:
1

Extend the base node

Start with the default paragraph specification:
const paragraph: NodeSpec = {
  ...schema.spec.nodes.get('paragraph'),
  // Add customizations below
}
2

Add custom attributes

Define new attributes for the node:
attrs: {
  textAlign: { default: 'left' }
}
3

Define parsing rules

Convert HTML to ProseMirror:
parseDOM: [
  {
    tag: "p",
    getAttrs: (dom: HTMLElement) => ({
      textAlign: dom.style.textAlign || "left"
    })
  }
]
4

Define rendering rules

Convert ProseMirror to HTML:
toDOM(node: Node) {
  return [
    "p",
    { style: `text-align:${node.attrs['textAlign']}` },
    0  // Content placeholder
  ]
}
5

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

1

Start with existing nodes/marks

Extend the basic schema rather than starting from scratch:
const paragraph: NodeSpec = {
  ...schema.spec.nodes.get('paragraph'),
  // Your customizations
}
2

Define both parseDOM and toDOM

Always provide bidirectional conversion:
parseDOM: [{ tag: "s" }],  // HTML → ProseMirror
toDOM() { return ["s", 0]; }  // ProseMirror → HTML
3

Use meaningful defaults

Provide sensible default values for attributes:
attrs: {
  textAlign: { default: 'left' },
  level: { default: 1 }
}
4

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