diff --git a/packages/core/package.json b/packages/core/package.json index 5de7926048..9ed7fe8039 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -97,6 +97,7 @@ "emoji-mart": "^5.6.0", "fast-deep-equal": "^3", "hast-util-from-dom": "^5.0.1", + "katex": "^0.16.22", "prosemirror-dropcursor": "^1.8.2", "prosemirror-highlight": "^0.13.0", "prosemirror-model": "^1.25.3", @@ -121,6 +122,7 @@ "devDependencies": { "@types/emoji-mart": "^3.0.14", "@types/hast": "^3.0.4", + "@types/katex": "^0.16.7", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "jsdom": "^25.0.1", diff --git a/packages/core/src/blocks/Equation/block.ts b/packages/core/src/blocks/Equation/block.ts new file mode 100644 index 0000000000..c1e1636d09 --- /dev/null +++ b/packages/core/src/blocks/Equation/block.ts @@ -0,0 +1,128 @@ +import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js"; +import { createBlockConfig, createBlockSpec } from "../../schema/index.js"; +import katex from "katex"; + +export const equationBlockConfig = createBlockConfig( + // No options for equation block + () => ({ + type: "equation" as const, + propSchema: { + latex: { + default: "", + }, + }, + content: "none" as const, + }) +); + +export const createEquationBlockSpec = createBlockSpec( + equationBlockConfig(), + () => ({ + meta: { + isolating: true, + }, + parse: (element) => { + if (element.tagName !== "DIV" || !element.classList.contains("equation")) { + return undefined; + } + const latex = element.getAttribute("data-latex") || ""; + return { latex }; + }, + render: (block, editor) => { + const wrapper = document.createElement("div"); + wrapper.className = "equation-wrapper"; + + // Editable input for LaTeX + const input = document.createElement("div"); + input.className = "equation-input"; + input.contentEditable = "true"; + input.textContent = block.props.latex || ""; + input.style.fontFamily = "monospace"; + input.style.padding = "8px"; + input.style.border = "1px solid #ccc"; + input.style.borderRadius = "4px"; + input.style.marginBottom = "8px"; + + // Function to update preview + const updatePreview = () => { + const latex = input.textContent || ""; + const preview = wrapper.querySelector(".equation-preview"); + if (preview) { + try { + const html = katex.renderToString(latex, { + throwOnError: false, + displayMode: true, + }); + preview.innerHTML = html; + } catch (error) { + preview.innerHTML = "Invalid LaTeX"; + } + } + // Update block props + if (latex !== block.props.latex) { + editor.updateBlock(block, { props: { latex } }); + } + }; + + // Initial preview + const preview = document.createElement("div"); + preview.className = "equation-preview"; + preview.style.padding = "8px"; + preview.style.border = "1px solid #eee"; + preview.style.borderRadius = "4px"; + preview.style.minHeight = "1em"; + updatePreview(); + + wrapper.appendChild(input); + wrapper.appendChild(preview); + + // Listen for changes + input.addEventListener("input", updatePreview); + + return { + dom: wrapper, + contentDOM: undefined, + destroy: () => { + input.removeEventListener("input", updatePreview); + }, + }; + }, + toExternalHTML: (block) => { + const div = document.createElement("div"); + div.className = "equation"; + div.setAttribute("data-latex", block.props.latex || ""); + try { + const html = katex.renderToString(block.props.latex || "", { + throwOnError: false, + displayMode: true, + }); + div.innerHTML = html; + } catch (error) { + div.textContent = block.props.latex || ""; + } + return { + dom: div, + contentDOM: undefined, + }; + }, + runsBefore: ["default"], + + }), + [ + createBlockNoteExtension({ + key: "equation-shortcuts", + keyboardShortcuts: { + "Mod-Alt-E": ({ editor }) => { + const cursorPosition = editor.getTextCursorPosition(); + const newBlock = editor.insertBlocks( + [{ type: "equation"}], + cursorPosition.block, + "before" + )[0]; + editor.setTextCursorPosition(newBlock, "end"); + return true; + }, + }, + }), + ] +); diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index a2d01b92df..683cbfe782 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -18,6 +18,7 @@ import { createVideoBlockSpec, defaultProps, } from "./index.js"; +import { createEquationBlockSpec } from "./Equation/block.js"; import { BlockNoDefaults, BlockSchema, @@ -39,6 +40,7 @@ export const defaultBlockSpecs = { bulletListItem: createBulletListItemBlockSpec(), checkListItem: createCheckListItemBlockSpec(), codeBlock: createCodeBlockSpec(), + equation: createEquationBlockSpec(), file: createFileBlockSpec(), heading: createHeadingBlockSpec(), image: createImageBlockSpec(), diff --git a/packages/core/src/blocks/index.ts b/packages/core/src/blocks/index.ts index 7d8a9fc3c4..3beb6963db 100644 --- a/packages/core/src/blocks/index.ts +++ b/packages/core/src/blocks/index.ts @@ -14,6 +14,8 @@ export * from "./Quote/block.js"; export * from "./Table/block.js"; export * from "./Video/block.js"; +export * from "./Equation/block.js"; + export { EMPTY_CELL_HEIGHT, EMPTY_CELL_WIDTH } from "./Table/TableExtension.js"; export * from "./ToggleWrapper/createToggleWrapper.js"; export * from "./File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91eb8f21b1..48ffb77692 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3612,6 +3612,9 @@ importers: hast-util-from-dom: specifier: ^5.0.1 version: 5.0.1 + katex: + specifier: ^0.16.22 + version: 0.16.22 prosemirror-dropcursor: specifier: ^1.8.2 version: 1.8.2 @@ -3679,6 +3682,9 @@ importers: '@types/hast': specifier: ^3.0.4 version: 3.0.4 + '@types/katex': + specifier: ^0.16.7 + version: 0.16.7 '@types/uuid': specifier: ^8.3.4 version: 8.3.4 @@ -9415,6 +9421,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} @@ -10485,6 +10494,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -12149,6 +12162,10 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + katex@0.16.22: + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -20534,6 +20551,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/katex@0.16.7': {} + '@types/linkify-it@5.0.0': {} '@types/lodash.foreach@4.5.9': @@ -21819,6 +21838,8 @@ snapshots: commander@4.1.1: {} + commander@8.3.0: {} + commondir@1.0.1: {} compressible@2.0.18: @@ -23902,6 +23923,10 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + katex@0.16.22: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1