benrbray / prosemirror-math

Schema and plugins for "first-class" math support in ProseMirror!
https://benrbray.com/prosemirror-math/
MIT License
245 stars 35 forks source link

Support Tiptap #27

Open benrbray opened 2 years ago

benrbray commented 2 years ago

TipTap is a popular wysiwyg editor built on top of ProseMirror. I wonder if it would be possible to write an extension to TipTap based on prosemirror-math.

xzackli commented 2 years ago

I gave this a try, and found that a naive wrap of the inline math mostly works except for the arrow-key escaping, which always ends up on the side it entered. Not sure why it's remembering that?

https://codesandbox.io/s/tiptap-math-04o0s?file=/src/components/HelloWorld.vue

import { Node } from '@tiptap/core'

import {
    makeBlockMathInputRule, makeInlineMathInputRule,
    REGEX_INLINE_MATH_DOLLARS, REGEX_BLOCK_MATH_DOLLARS
} from "@benrbray/prosemirror-math";
import { mathPlugin, mathBackspaceCmd, insertMathCmd, mathSerializer } from "@benrbray/prosemirror-math";

export default Node.create({
  name: 'math_inline',
  group: "inline math",
  content: "text*",        // important!
  inline: true,            // important!
  atom: true,              // important!

  parseHTML () {
    return [{
      tag: "math-inline"   // important!
    }]
  },

  renderHTML ({ HTMLAttributes }) {
    return ["math-inline", { class: "math-node" }, 0]
  },

  addProseMirrorPlugins () {
    return [
      mathPlugin
    ]
  },

  addInputRules () {
    return [
      makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)
    ]
  }
})

I love the package, by the way! I've hacked together something like this for myself, and it's not nearly as polished as this.

benrbray commented 2 years ago

Hey thanks! This is good to know, I wasn't expecting it to be so simple. It's pretty strange that it returns you to the same side you entered from -- I'm wondering if this is a quirk of how Tiptap manages NodeViews. If I'm remembering correctly, prosemirror-math only needed special handling when the cursor moves from outside -> inside a NodeView, but ProseMirror's default behavior was fine when moving inside -> outside.

One way to test this hypothesis might be to see whether hooking up a much simpler NodeView to Tiptap (such as the footnote example) fails in the same way.

xzackli commented 2 years ago

Maybe this is a projection of a larger problem -- it seems that inserting text into an existing inline prosemirror-math NodeView generates a prosemirror error (despite successfully rendering the math for it afterward). This might take more work than my naive wrap sadly, and might resemble what one would do for the footnote you mentioned -- rewriting the NodeView logic on the Tiptap extension side. :(

index.es.js?576a:3280 Uncaught TypeError: Cannot read property 'dirty' of null
    at DOMObserver.flush (index.es.js?576a:3280)
    at MutationObserver.DOMObserver.observer (index.es.js?576a:3146)
arunsah commented 2 years ago

I gave this a try, and found that a naive wrap of the inline math mostly works except for the arrow-key escaping, which always ends up on the side it entered. Not sure why it's remembering that?

https://codesandbox.io/s/tiptap-math-04o0s?file=/src/components/HelloWorld.vue

import { Node } from '@tiptap/core'

import {
  makeBlockMathInputRule, makeInlineMathInputRule,
  REGEX_INLINE_MATH_DOLLARS, REGEX_BLOCK_MATH_DOLLARS
} from "@benrbray/prosemirror-math";
import { mathPlugin, mathBackspaceCmd, insertMathCmd, mathSerializer } from "@benrbray/prosemirror-math";

export default Node.create({
  name: 'math_inline',
  group: "inline math",
  content: "text*",        // important!
  inline: true,            // important!
  atom: true,              // important!

  parseHTML () {
    return [{
      tag: "math-inline"   // important!
    }]
  },

  renderHTML ({ HTMLAttributes }) {
    return ["math-inline", { class: "math-node" }, 0]
  },

  addProseMirrorPlugins () {
    return [
      mathPlugin
    ]
  },

  addInputRules () {
    return [
      makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)
    ]
  }
})

I love the package, by the way! I've hacked together something like this for myself, and it's not nearly as polished as this.

Hi, One import is missing, I guess. In the code snippet and as well as in codesandbox Extension.js file.

import mergeAttributes from '@tiptap/core/src/utilities/mergeAttributes'

Thanks for sharing. I was looking for something like this for a while. Reached here by following Inline Math Integration for Tiptap1 | BrianHung/Math.css | gist.github.com.

rajeshtva commented 2 years ago

Maybe this is a projection of a larger problem -- it seems that inserting text into an existing inline prosemirror-math NodeView generates a prosemirror error (despite successfully rendering the math for it afterward). This might take more work than my naive wrap sadly, and might resemble what one would do for the footnote you mentioned -- rewriting the NodeView logic on the Tiptap extension side. :(

index.es.js?576a:3280 Uncaught TypeError: Cannot read property 'dirty' of null
    at DOMObserver.flush (index.es.js?576a:3280)
    at MutationObserver.DOMObserver.observer (index.es.js?576a:3146)

@xzackli would you please share your implementation/hack? i am in a need to implement that thing too. plus i want to implement other things as well...

mraghuram commented 2 years ago

I finally got this to work with a few changes...

import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core";

import { mathPlugin } from "@benrbray/prosemirror-math";

export const regex = /(?:^|\s)((?:\$)((?:[^*]+))(?:\$))$/;

export const MathInline =  Node.create({
  name: "math_inline",
  group: "inline math",
  content: "text*", // important!
  inline: true, // important!
  atom: true, // important!
  code: true,

  parseHTML() {
    return [
      {
        tag: "math-inline" // important!
      }
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return ["math-inline", mergeAttributes({ class: "math-node" }, HTMLAttributes),0];
  },

  addProseMirrorPlugins() {
    return [mathPlugin];
  },

  addInputRules() {
    return [
      nodeInputRule({find:regex, type:this.type})];
  }
});

While this works, it does something strange. When I use $a=b$ it creates $empty$.. where empty is also with hidden $$. If I type between them then it recognizes the content. I am not sure why it deletes the content in the first place. Any thoughts / suggestion anyone?

loveklmn commented 2 years ago

I gave this a try, and found that a naive wrap of the inline math mostly works except for the arrow-key escaping, which always ends up on the side it entered. Not sure why it's remembering that?

https://codesandbox.io/s/tiptap-math-04o0s?file=/src/components/HelloWorld.vue

import { Node } from '@tiptap/core'

import {
  makeBlockMathInputRule, makeInlineMathInputRule,
  REGEX_INLINE_MATH_DOLLARS, REGEX_BLOCK_MATH_DOLLARS
} from "@benrbray/prosemirror-math";
import { mathPlugin, mathBackspaceCmd, insertMathCmd, mathSerializer } from "@benrbray/prosemirror-math";

export default Node.create({
  name: 'math_inline',
  group: "inline math",
  content: "text*",        // important!
  inline: true,            // important!
  atom: true,              // important!

  parseHTML () {
    return [{
      tag: "math-inline"   // important!
    }]
  },

  renderHTML ({ HTMLAttributes }) {
    return ["math-inline", { class: "math-node" }, 0]
  },

  addProseMirrorPlugins () {
    return [
      mathPlugin
    ]
  },

  addInputRules () {
    return [
      makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)
    ]
  }
})

I love the package, by the way! I've hacked together something like this for myself, and it's not nearly as polished as this.

It seems that tiptap.dev produced a new bug to cause error when typing. see https://codesandbox.io/s/tiptap-math-forked-2zfr9?file=/src/components/Extension.js type $123$ and get empty

cadars commented 2 years ago

Might be related to this change? https://github.com/ueberdosis/tiptap/pull/1997

mraghuram commented 2 years ago

@cadars, so is there a solution? and also, I'm struggling to insert math_inline using commands.

    addCommands() {
        return {
            setMath: () => ({commands}) => {
                // console.log("in math", x.can().setNode('math_inline'))
                return commands.setNode('math_inline')
            }
        }
    }

The above doesn't work. any thoughts?

wengtytt commented 2 years ago

I finally got this to work with a few changes...

import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core";

import { mathPlugin } from "@benrbray/prosemirror-math";

export const regex = /(?:^|\s)((?:\$)((?:[^*]+))(?:\$))$/;

export const MathInline =  Node.create({
  name: "math_inline",
  group: "inline math",
  content: "text*", // important!
  inline: true, // important!
  atom: true, // important!
  code: true,

  parseHTML() {
    return [
      {
        tag: "math-inline" // important!
      }
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return ["math-inline", mergeAttributes({ class: "math-node" }, HTMLAttributes),0];
  },

  addProseMirrorPlugins() {
    return [mathPlugin];
  },

  addInputRules() {
    return [
      nodeInputRule({find:regex, type:this.type})];
  }
});

While this works, it does something strange. When I use $a=b$ it creates $empty$.. where empty is also with hidden $$. If I type between them then it recognizes the content. I am not sure why it deletes the content in the first place. Any thoughts / suggestion anyone?

Any updates about this issue?

wengtytt commented 2 years ago

To make it compatible with the latest @tiptap input rules.

/* eslint-disable */
import { Node, mergeAttributes } from '@tiptap/core';

import { inputRules } from 'prosemirror-inputrules';

import {
    makeInlineMathInputRule,
    REGEX_INLINE_MATH_DOLLARS,
    mathPlugin,
} from '@benrbray/prosemirror-math';

import '@benrbray/prosemirror-math/style/math.css';
import 'katex/dist/katex.min.css';

export default Node.create({
    name: 'math_inline',
    group: 'inline math',
    content: 'text*', // important!
    inline: true, // important!
    atom: true, // important!
    code: true,

    parseHTML() {
        return [
            {
                tag: 'math-inline', // important!
            },
        ];
    },

    renderHTML({ HTMLAttributes }) {
        return ['math-inline', mergeAttributes({ class: 'math-node' }, HTMLAttributes), 0];
    },

    addProseMirrorPlugins() {
        const inputRulePlugin = inputRules({
            rules: [makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)],
        });

        return [mathPlugin, inputRulePlugin];
    },
});
wengtytt commented 2 years ago

Leave the math block here as well in case anybody needed. The mathPlugin only need to be inserted once if both math-inline and math-display are created.

/* eslint-disable */
import { Node, mergeAttributes } from '@tiptap/core';

import { inputRules } from 'prosemirror-inputrules';

import {
    mathPlugin,
    makeBlockMathInputRule,
    REGEX_BLOCK_MATH_DOLLARS,
} from '@benrbray/prosemirror-math';

export default Node.create({
    name: 'math_display',
    group: 'block math',
    content: 'text*', // important!
    atom: true, // important!
    code: true,

    parseHTML() {
        return [
            {
                tag: 'math-display', // important!
            },
        ];
    },

    renderHTML({ HTMLAttributes }) {
        return [
            'math-display',
            mergeAttributes({ class: 'math-node' }, HTMLAttributes),
            0,
        ];
    },

    addProseMirrorPlugins() {
        const inputRulePlugin = inputRules({
            rules: [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, this.type)],
        });

        return [mathPlugin, inputRulePlugin];
    },
});
swordream commented 2 years ago

Leave the math block here as well in case anybody needed. The mathPlugin only need to be inserted once if both math-inline and math-display are created.

/* eslint-disable */
import { Node, mergeAttributes } from '@tiptap/core';

import { inputRules } from 'prosemirror-inputrules';

import {
    mathPlugin,
    makeBlockMathInputRule,
    REGEX_BLOCK_MATH_DOLLARS,
} from '@benrbray/prosemirror-math';

export default Node.create({
    name: 'math_display',
    group: 'block math',
    content: 'text*', // important!
    atom: true, // important!
    code: true,

    parseHTML() {
        return [
            {
                tag: 'math-display', // important!
            },
        ];
    },

    renderHTML({ HTMLAttributes }) {
        return [
            'math-display',
            mergeAttributes({ class: 'math-node' }, HTMLAttributes),
            0,
        ];
    },

    addProseMirrorPlugins() {
        const inputRulePlugin = inputRules({
            rules: [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, this.type)],
        });

        return [mathPlugin, inputRulePlugin];
    },
});

Does the keymap worked?

raimondlume commented 1 year ago

Thanks for everyone in this thread for the examples! Got it going in the editor without any problems with the latest tiptap version.

One hitch not yet working is the output of generateHtml, which only shows the raw TeX without being rendered by Katex.

Has anyone gotten this working by any chance? (@wengtytt - you seem to have gotten the furthest)

benrbray commented 1 year ago

I'm glad to see some progress integrating with TipTap. I don't use TipTap myself, but I would happily accept PRs (or suggested improvements) targeted at making TipTap integration easier.

raimondlume commented 1 year ago

Other than finding this thread, everything else was pretty straight-forward thanks to the examples by @wengtytt and others.

One thing that could be improved are the required ProseMirror peer dependencies. TipTap has their own wrapper @tiptap/pm which is pretty much required if you're doing anything more than basic, meaning that these could be reused for prosemirror-math as well, which atm pulls in the core ProseMirror packages as well.

The most convenient option imo would be to have the tiptap integration as a separate package which wraps prosemirror-math. I'd be open to contributing this - what would be the best option, a new repo or setting up a monorepo? @benrbray

FYI, TipTap has their own Mathematics extension as well, but it is behind a paywall and lacking in features / customisability compared to this package.

danlucraft commented 1 year ago

Thanks for your help @wengtytt and others. 🙏

It's all working great except one thing: clicking on the rendered equations does not open them for editing, as it does in this example. Has anyone seen that issue?