analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 8.28 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/RichEditor/components/MathNode.tsx"],"sourcesContent":["import { Node, mergeAttributes } from '@tiptap/core';\nimport { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react';\nimport type { NodeViewProps } from '@tiptap/react';\nimport katex from 'katex';\nimport { useMemo } from 'react';\n\ninterface MathNodeViewProps {\n readonly node: NodeViewProps['node'];\n readonly editor: NodeViewProps['editor'];\n readonly getPos: NodeViewProps['getPos'];\n}\n\nfunction MathNodeView({ node, editor, getPos }: MathNodeViewProps) {\n // `node.attrs.latex` is not guaranteed to be a string: pasted/imported HTML\n // without a `data-latex` attribute makes parseHTML return `undefined`, which\n // bypasses the attribute's `default: ''`. Normalising here keeps both the\n // KaTeX render and the cursor math below working on a real string.\n const latex = typeof node.attrs.latex === 'string' ? node.attrs.latex : '';\n\n const renderedHtml = useMemo(() => {\n try {\n return katex.renderToString(latex, {\n throwOnError: false,\n displayMode: false,\n });\n } catch {\n return `<span class=\"text-error-600\">${latex}</span>`;\n }\n }, [latex]);\n\n // When clicked, convert back to editable text\n const handleClick = () => {\n const pos = getPos();\n // `typeof NaN === 'number'`, so an explicit finite check is required here —\n // a NaN position would flow into setTextSelection and make ProseMirror throw\n // \"Position NaN out of range\" (FRONTEND-BACKOFFICE-WEB-P).\n if (typeof pos !== 'number' || !Number.isFinite(pos)) return;\n\n editor\n .chain()\n .focus()\n .deleteRange({ from: pos, to: pos + node.nodeSize })\n .insertContent(`$${latex}$`)\n .run();\n\n // Move cursor to the end of the inserted text (before the last $).\n // Clamp to the document bounds: the position is derived from `pos`, which\n // predates the edits above, so it can land past the end of the resulting\n // document — another way ProseMirror would throw \"out of range\".\n const docSize = editor.state.doc.content.size;\n const newPos = Math.min(Math.max(pos + latex.length + 1, 0), docSize);\n editor.commands.setTextSelection(newPos);\n };\n\n return (\n <NodeViewWrapper\n as=\"span\"\n className=\"inline-math cursor-pointer hover:bg-primary-50 rounded px-0.5 transition-colors\"\n onClick={handleClick}\n title=\"Clique para editar\"\n >\n <span\n dangerouslySetInnerHTML={{ __html: renderedHtml }}\n className=\"inline\"\n />\n </NodeViewWrapper>\n );\n}\n\nexport const MathNode = Node.create({\n name: 'mathInline',\n group: 'inline',\n inline: true,\n atom: true,\n selectable: true,\n\n addAttributes() {\n return {\n latex: {\n default: '',\n },\n };\n },\n\n parseHTML() {\n return [\n {\n tag: 'span[data-type=\"math-inline\"]',\n getAttrs: (dom) => {\n // Fall back to '' instead of forwarding `undefined`: returning the\n // attribute explicitly overrides the `default: ''` declared above, so\n // HTML pasted without `data-latex` would otherwise produce a node\n // whose `latex` attr is undefined.\n return { latex: dom.dataset.latex ?? '' };\n },\n },\n ];\n },\n\n renderHTML({ node, HTMLAttributes }) {\n return [\n 'span',\n mergeAttributes(HTMLAttributes, {\n 'data-type': 'math-inline',\n 'data-latex': node.attrs.latex,\n }),\n ];\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(MathNodeView);\n },\n\n // Disable default input rules\n addInputRules() {\n return [];\n },\n\n addKeyboardShortcuts() {\n return {\n // When space is pressed after closing $, convert to math node\n Space: ({ editor }) => {\n const { state } = editor;\n const { selection } = state;\n const { $from } = selection;\n\n // Get text before cursor\n const textBefore = $from.parent.textBetween(\n Math.max(0, $from.parentOffset - 200),\n $from.parentOffset,\n '\\n'\n );\n\n // Check if there's a complete $...$ pattern right before the cursor\n const regex = /\\$([^$]+)\\$$/;\n const match = regex.exec(textBefore);\n if (match?.[1]?.trim()) {\n const latex = match[1];\n const matchStart = $from.pos - match[0].length;\n\n // Replace $latex$ with math node, then add a space\n editor\n .chain()\n .deleteRange({ from: matchStart, to: $from.pos })\n .insertContent([\n { type: 'mathInline', attrs: { latex } },\n { type: 'text', text: ' ' },\n ])\n .run();\n\n return true;\n }\n\n return false;\n },\n\n // When backspace is pressed on a math node, convert it back to text for editing\n Backspace: ({ editor }) => {\n const { selection } = editor.state;\n const { $from } = selection;\n\n // Check if we're right after a math node\n const nodeBefore = $from.nodeBefore;\n if (nodeBefore?.type.name === 'mathInline') {\n const pos = $from.pos - nodeBefore.nodeSize;\n const latex = nodeBefore.attrs.latex;\n\n // Convert back to text without the last $, so user can continue editing\n editor\n .chain()\n .deleteRange({ from: pos, to: $from.pos })\n .insertContent(`$${latex}`)\n .run();\n\n return true;\n }\n\n return false;\n },\n };\n },\n});\n"],"mappings":"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","names":[]}