Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Tiptap #27

Open
benrbray opened this issue Aug 12, 2021 · 17 comments
Open

Support Tiptap #27

benrbray opened this issue Aug 12, 2021 · 17 comments
Labels
enhancement New feature or request

Comments

@benrbray
Copy link
Owner

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.

@benrbray benrbray added the enhancement New feature or request label Aug 12, 2021
@xzackli
Copy link

xzackli commented Aug 25, 2021

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
Copy link
Owner Author

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
Copy link

xzackli commented Aug 26, 2021

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
Copy link

arunsah commented Sep 15, 2021

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
Copy link

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
Copy link

mraghuram commented Oct 26, 2021

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
Copy link

loveklmn commented Oct 29, 2021

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
Copy link

cadars commented Oct 29, 2021

Might be related to this change? ueberdosis/tiptap#1997

@mraghuram
Copy link

@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
Copy link

wengtytt commented Feb 2, 2022

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
Copy link

wengtytt commented Feb 3, 2022

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
Copy link

wengtytt commented Feb 3, 2022

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
Copy link

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
Copy link

raimondlume commented Apr 14, 2023

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
Copy link
Owner Author

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
Copy link

raimondlume commented Apr 17, 2023

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
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests