"use client"; import katex from "katex"; import { Node, mergeAttributes } from "@tiptap/core"; import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react"; import type { NodeViewProps } from "@tiptap/react"; function renderMath(expression: string, displayMode: boolean): string { return katex.renderToString(expression, { displayMode, output: "htmlAndMathml", strict: "warn", throwOnError: false, }); } function InlineMathView({ node }: NodeViewProps) { const expression = String(node.attrs.expression ?? ""); return ( ); } function BlockMathView({ node }: NodeViewProps) { const expression = String(node.attrs.expression ?? ""); return (
); } export const InlineMathExtension = Node.create({ name: "inlineMath", group: "inline", inline: true, atom: true, selectable: true, addAttributes() { return { expression: { default: "", rendered: false, }, }; }, parseHTML() { return [ { tag: 'span[data-type="inline-math"]', getAttrs: (el) => ({ expression: (el as HTMLElement).getAttribute("data-expression") ?? "", }), }, ]; }, renderHTML({ node, HTMLAttributes }) { return [ "span", mergeAttributes(HTMLAttributes, { "data-type": "inline-math", "data-expression": node.attrs.expression, }), node.attrs.expression, ]; }, // Single-dollar inline math is intentionally not parsed from Markdown and has // no typing input rule. Dollar amounts like `$100~$120` must stay literal; // users should write explicit `$$...$$` blocks for math. renderMarkdown: (node: any) => { const expression = String(node.attrs?.expression ?? ""); return `$${expression}$`; }, addNodeView() { return ReactNodeViewRenderer(InlineMathView); }, }); export const BlockMathExtension = Node.create({ name: "blockMath", group: "block", atom: true, code: true, defining: true, isolating: true, selectable: true, addAttributes() { return { expression: { default: "", rendered: false, }, }; }, parseHTML() { return [ { tag: 'div[data-type="block-math"]', getAttrs: (el) => ({ expression: (el as HTMLElement).getAttribute("data-expression") ?? "", }), }, ]; }, renderHTML({ node, HTMLAttributes }) { return [ "div", mergeAttributes(HTMLAttributes, { "data-type": "block-math", "data-expression": node.attrs.expression, }), node.attrs.expression, ]; }, markdownTokenizer: { name: "blockMath", level: "block" as const, start(src: string) { return src.search(/^\$\$/m); }, tokenize(src: string) { if (!src.startsWith("$$")) return undefined; const match = src.match(/^\$\$[ \t]*\n?([\s\S]+?)\n?\$\$(?:\n|$)/); if (!match) return undefined; return { type: "blockMath", raw: match[0], attributes: { expression: match[1] ?? "" }, }; }, }, parseMarkdown: (token: any, helpers: any) => { return helpers.createNode("blockMath", token.attributes); }, renderMarkdown: (node: any) => { const expression = String(node.attrs?.expression ?? ""); return `$$\n${expression}\n$$`; }, addNodeView() { return ReactNodeViewRenderer(BlockMathView); }, });