/** * Minimal ProseMirror schema for markdown editor. * * Only contains structural nodes - no semantic marks (bold, italic, etc). * Markdown syntax is preserved as plain text and rendered via decorations. * hard_break preserves the \n vs \n\n distinction within block content. */ import { type NodeSpec, Schema } from "prosemirror-model" type DOMNode = "doc" | "paragraph" | "hard_break" | "code_block" | "math_block" | "text" /** Node definitions */ const nodes: Record = { /** Root document container — one or more paragraphs per block */ doc: { content: "block+", }, /** Paragraph — one per line within a block */ paragraph: { group: "block", content: "inline*", parseDOM: [{ tag: "p" }], toDOM: () => ["p", 0], }, /** Hard line break — preserves single \n within a paragraph */ hard_break: { inline: true, group: "inline", selectable: false, parseDOM: [{ tag: "br" }], toDOM: () => ["br"], }, /** Fenced code block — rendered via CodeMirror 6 node view */ code_block: { group: "block", content: "text*", code: true, defining: true, attrs: { language: { default: "" } }, parseDOM: [ { tag: "pre", getAttrs(dom) { const code = dom.querySelector("code") const lang = code?.className?.match(/language-(\w+)/)?.[1] ?? "" return { language: lang } }, }, ], toDOM: (node) => [ "pre", { class: node.attrs.language ? `language-${node.attrs.language}` : "" }, ["code", 0], ], }, /** Display math block — rendered via KaTeX node view */ math_block: { group: "block", content: "text*", code: true, defining: true, toDOM: () => ["div", { "data-math-block": "" }, 0], }, /** Text content */ text: { group: "inline", }, } /** No marks - all formatting handled via decorations */ const marks = {} export const schema = new Schema({ nodes, marks })