mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
169 lines
3.9 KiB
TypeScript
169 lines
3.9 KiB
TypeScript
"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 (
|
|
<NodeViewWrapper
|
|
as="span"
|
|
className="math-node inline"
|
|
data-type="inline-math"
|
|
data-expression={expression}
|
|
contentEditable={false}
|
|
>
|
|
<span dangerouslySetInnerHTML={{ __html: renderMath(expression, false) }} />
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
function BlockMathView({ node }: NodeViewProps) {
|
|
const expression = String(node.attrs.expression ?? "");
|
|
return (
|
|
<NodeViewWrapper
|
|
as="div"
|
|
className="math-node block"
|
|
data-type="block-math"
|
|
data-expression={expression}
|
|
contentEditable={false}
|
|
>
|
|
<div dangerouslySetInnerHTML={{ __html: renderMath(expression, true) }} />
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
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);
|
|
},
|
|
});
|