Skip to content

Commit

Permalink
feat: add mathematics extension
Browse files Browse the repository at this point in the history
  • Loading branch information
phyohtetarkar committed Jun 30, 2024
1 parent 008d159 commit 15b4428
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 0 deletions.
3 changes: 3 additions & 0 deletions apps/web/components/tailwind/advanced-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { defaultExtensions } from "./extensions";
import { ColorSelector } from "./selectors/color-selector";
import { LinkSelector } from "./selectors/link-selector";
import { NodeSelector } from "./selectors/node-selector";
import { MathSelector } from "./selectors/math-selector";
import { Separator } from "./ui/separator";

import { handleImageDrop, handleImagePaste } from "novel/plugins";
Expand Down Expand Up @@ -126,6 +127,8 @@ const TailwindAdvancedEditor = () => {

<LinkSelector open={openLink} onOpenChange={setOpenLink} />
<Separator orientation="vertical" />
<MathSelector />
<Separator orientation="vertical" />
<TextButtons />
<Separator orientation="vertical" />
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
Expand Down
11 changes: 11 additions & 0 deletions apps/web/components/tailwind/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Twitter,
UpdatedImage,
Youtube,
Mathematics,
} from "novel/extensions";
import { UploadImagesPlugin } from "novel/plugins";

Expand Down Expand Up @@ -130,6 +131,15 @@ const twitter = Twitter.configure({
inline: false,
});

const mathematics = Mathematics.configure({
HTMLAttributes: {
class: cx("text-foreground rounded p-1 hover:bg-accent cursor-pointer"),
},
katexOptions: {
throwOnError: false,
},
});

const characterCount = CharacterCount.configure();

export const defaultExtensions = [
Expand All @@ -145,6 +155,7 @@ export const defaultExtensions = [
codeBlockLowlight,
youtube,
twitter,
mathematics,
characterCount,
GlobalDragHandle,
];
35 changes: 35 additions & 0 deletions apps/web/components/tailwind/selectors/math-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Button } from "@/components/tailwind/ui/button";
import { cn } from "@/lib/utils";
import { SigmaIcon } from "lucide-react";
import { useEditor } from "novel";

export const MathSelector = () => {
const { editor } = useEditor();

if (!editor) return null;

return (
<Button
variant="ghost"
size="sm"
className="rounded-none w-12"
onClick={(evt) => {
if (editor.isActive("math")) {
editor.chain().focus().unsetLatex().run();
} else {
const { from, to } = editor.state.selection;
const latex = editor.state.doc.textBetween(from, to);

if (!latex) return;

editor.chain().focus().setLatex({ latex }).run();
}
}}
>
<SigmaIcon
className={cn("size-4", { "text-blue-500": editor.isActive("math") })}
strokeWidth={2.3}
/>
</Button>
);
};
2 changes: 2 additions & 0 deletions packages/headless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@
"react-markdown": "^8.0.7",
"react-moveable": "^0.56.0",
"react-tweet": "^3.2.1",
"katex": "^0.16.10",
"tippy.js": "^6.3.7",
"tiptap-extension-global-drag-handle": "^0.1.7",
"tiptap-markdown": "^0.8.9",
"tunnel-rat": "^0.1.2"
},
"devDependencies": {
"@biomejs/biome": "^1.7.2",
"@types/katex": "^0.16.7",
"@types/react": "^18.2.55",
"@types/react-dom": "18.2.19",
"tsconfig": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions packages/headless/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Markdown } from "tiptap-markdown";
import CustomKeymap from "./custom-keymap";
import { ImageResizer } from "./image-resizer";
import { Twitter } from "./twitter";
import { Mathematics } from "./mathematics";
import UpdatedImage from "./updated-image";

import CharacterCount from "@tiptap/extension-character-count";
Expand Down Expand Up @@ -82,6 +83,7 @@ export {
simpleExtensions,
Youtube,
Twitter,
Mathematics,
CharacterCount,
GlobalDragHandle,
};
179 changes: 179 additions & 0 deletions packages/headless/src/extensions/mathematics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import 'katex/dist/katex.min.css';

import { Node, mergeAttributes } from "@tiptap/core";
import { EditorState } from "@tiptap/pm/state";
import katex, { type KatexOptions } from "katex";

export interface MathematicsOptions {
/**
* By default LaTeX decorations can render when mathematical expressions are not inside a code block.
* @param state - EditorState
* @param pos - number
* @returns boolean
*/
shouldRender: (state: EditorState, pos: number) => boolean;

/**
* @see https://katex.org/docs/options.html
*/
katexOptions?: KatexOptions;

HTMLAttributes: Record<string, any>;
}

declare module "@tiptap/core" {
interface Commands<ReturnType> {
LatexCommand: {

/**
* Set selection to a LaTex symbol
*/
setLatex: ({ latex }: { latex: string }) => ReturnType;

/**
* Unset a LaTex symbol
*/
unsetLatex: () => ReturnType;

};
}
}

/**
* This extension adds support for mathematical symbols with LaTex expression.
*
* @see https://katex.org/
*/
export const Mathematics = Node.create<MathematicsOptions>({
name: "math",
inline: true,
group: "inline",
atom: true,
selectable: true,
marks: "",

addAttributes() {
return {
latex: "",
};
},

addOptions() {
return {
shouldRender: (state, pos) => {
const $pos = state.doc.resolve(pos);

if (!$pos.parent.isTextblock) {
return false;
}

return $pos.parent.type.name !== "codeBlock";
},
katexOptions: {
throwOnError: false,
},
HTMLAttributes: {},
};
},

addCommands() {
return {
setLatex:
({ latex }) =>
({ chain, state }) => {
if (!latex) {
return false;
}
const { from, to, $anchor } = state.selection;

if (!this.options.shouldRender(state, $anchor.pos)) {
return false;
}

return chain()
.insertContentAt(
{ from: from, to: to },
{
type: "math",
attrs: {
latex: latex,
},
}
)
.setTextSelection({ from: from, to: from + 1 })
.run();
},
unsetLatex:
() =>
({ editor, state, chain }) => {
const latex = editor.getAttributes(this.name).latex;
if (typeof latex !== "string") {
return false;
}

const { from, to } = state.selection;

return chain()
.command(({ tr }) => {
tr.insertText(latex, from, to);
return true;
})
.setTextSelection({
from: from,
to: from + latex.length,
})
.run();
},
};
},

parseHTML() {
return [{ tag: `span[data-type="${this.name}"]` }];
},

renderHTML({ node, HTMLAttributes }) {
const latex = node.attrs["latex"] ?? "";
return [
"span",
mergeAttributes(HTMLAttributes, {
"data-type": this.name,
}),
latex,
];
},

renderText({ node }) {
return node.attrs["latex"] ?? "";
},

addNodeView() {
return ({ node, HTMLAttributes, getPos, editor }) => {
const dom = document.createElement("span");
const latex: string = node.attrs["latex"] ?? "";

Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {
dom.setAttribute(key, value);
});

Object.entries(HTMLAttributes).forEach(([key, value]) => {
dom.setAttribute(key, value);
});

dom.addEventListener("click", (evt) => {
if (editor.isEditable && typeof getPos === "function") {
const pos = getPos();
const nodeSize = node.nodeSize;
editor.commands.setTextSelection({ from: pos, to: pos + nodeSize });
}
});

dom.contentEditable = "false";

dom.innerHTML = katex.renderToString(latex, this.options.katexOptions);

return {
dom: dom,
};
};
},
});
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 15b4428

Please sign in to comment.