Giter Club home page Giter Club logo

prosemirror-math's Introduction

prosemirror-math

license types

Please don't hesitate to report issues or make feature requests! Contributions welcome!

Overview

The prosemirror-math package provides schema and plugins for comfortably writing mathematics with ProseMirror. Written in TypeScript, with math rendering handled by KaTeX. You can install the npm package or use this repository as a starting point for your own plugin. The important files in this project are:

  • lib/math-schema.ts: A minimal ProseMirror schema supporting inline and display math nodes.
  • lib/math-nodeview.ts: A NodeView responsible for rendering and editing math nodes.
  • public/prosemirror-math.css: Contains all necessary styling for math nodes to display correctly. This file can easily be modified to achieve your desired appearance.

Basic Usage (try it yourself!)

Unlike other editors, this plugin treats math as part of the text itself, rather than as an "atom" that can only be edited through a dialog box. For example, inline math nodes can be edited directly by bringing the cursor inside of them:

edit inline math

Display math supports multiline editing, as shown below:

edit display math

To create a new math expression, simply enclose LaTeX math notation in dollar signs, like $x+y=5$. When you finish typing, a new math node will be automatically created:

create inline math

To start a display math block, create a blank line and type $$ followed by a space. A multiline editor will appear. To exit the block, press Ctrl-Enter or navigate away the mouse or arrow keys.

create display math

Math nodes behave like regular text when using the arrow keys or Backspace. You can select, copy, and paste math nodes just like regular text! From within a math node, press Ctrl-Backspace to delete the entire node.

TIP: You can define your own commands with \providecommand{\cmd}{...}!

See the KaTeX documentation for a list of supported LaTeX commands. In the future, prosemirror-math will also accept a custom callback that can be used to invoke alternative renderers like MathJax.

Installation & Setup

Note that prosemirror-math is built on top of ProseMirror, which itself has a steep learning curve. At the very least, you will need to understand Schema and Plugins to integrate prosemirror-math into your project. Start by installing the npm package:

npm install @benrbray/prosemirror-math

CSS

First, make sure you include the CSS files for prosemirror-math and katex on any pages that will need them. They can be found at the following paths:

node_modules/katex/dist/katex.min.css
node_modules/@benrbray/prosemirror-math/dist/prosemirror-math.css

If you are using a bundler like vite or webpack, you may be able to include the CSS files like this:

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

Schema

Add math_inline and math_display nodes to your prosemirror document schema. The names are important! If you modify the schema, be careful not to change any of the values marked important! below, or you might run into unexpected behavior!

import { Schema } from "prosemirror-model";

let schema = new Schema({
    nodes: {
        doc: {
            content: "block+"
        },
        paragraph: {
            content: "inline*",
            group: "block",
            parseDOM: [{ tag: "p" }],
            toDOM() { return ["p", 0]; }
        },
        math_inline: {               // important!
            group: "inline math",
            content: "text*",        // important!
            inline: true,            // important!
            atom: true,              // important!
            toDOM: () => ["math-inline", { class: "math-node" }, 0],
            parseDOM: [{
                tag: "math-inline"   // important!
            }]
        },
        math_display: {              // important!
            group: "block math",
            content: "text*",        // important!
            atom: true,              // important!
            code: true,              // important!
            toDOM: () => ["math-display", { class: "math-node" }, 0],
            parseDOM: [{
                tag: "math-display"  // important!
            }]
        },
        text: {
            group: "inline"
        }
    }
});

Input Rules

If you want the user to be able to easily add new math nodes by typing $...$ for inline math or $$ followed by a space for block math, you need to create InputRule instances. You can write your own, or use the helper functions provided by prosemirror-math.

CAUTION: Make sure the NodeTypes you provide to each input rule belong to the same schema instance that you pass to your ProseMirror EditorView instance. Otherwise, you'll see strange errors in the console!

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

// create input rules (using default regex)
let inlineMathInputRule = makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, editorSchema.nodes.math_inline);
let blockMathInputRule = makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, editorSchema.nodes.math_display);

Plugins

Choose which plugins you need from the following list, and pass them to your EditorState instance, along with the input rules you created.

  • mathPlugin (required) Provides the core functionality of prosemirror-math.
  • mathBackspaceCmd (recommended) When included in your keymap for the "Backspace" key, pressing backspace on the right boundary of a math node will place the cursor inside the math node, rather than deleting it.
  • insertMathCmd(nodeType: NodeType) (optional) Helper function for creating a command which can be used to insert a math node at the current document position.
  • mathSerializer (recommended) Attach to the clipboardTextSerializer prop of your EditorView. When pasting a selection from a prosemirror-math editor to a plain text editor, ensures that the pasted math expressions will be properly delimited by $...$ and $$...$$.

Here is the recommended setup:

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

// prosemirror imports
import { EditorView } from "prosemirror-view";
import { EditorState, Plugin } from "prosemirror-state";
import { chainCommands, deleteSelection, selectNodeBackward, joinBackward, Command } from "prosemirror-commands";
import { keymap } from "prosemirror-keymap";
import { inputRules } from "prosemirror-inputrules";

// plugins (order matters)
let plugins:Plugin[] = [
    mathPlugin,
    keymap({
        "Mod-Space" : insertMathCmd(schema.nodes.math_inline),
        // modify the default keymap chain for backspace
        "Backspace": chainCommands(deleteSelection, mathBackspaceCmd, joinBackward, selectNodeBackward),
    }),
    inputRules({ rules: [ inlineMathInputRule, blockMathInputRule ] })
];

// create prosemirror state
let state = EditorState.create({
    schema: editorSchema,
    plugins: plugins,
    doc: /* ... */
})

// create prosemirror view
let view = new EditorView(editorElt, {
    state,
    clipboardTextSerializer: (slice) => { return mathSerializer.serializeSlice(slice) },
})

Development

Clone this repository and install the necessary dependencies:

git clone [email protected]:benrbray/prosemirror-math.git
cd prosemirror-math
npm install

From the root directory, you can run the scripts in package.json.

  • Use npm run build to build the prosemirror-math package
  • Use npm run build:site to generate the static demo website
  • Use npm run dev to start a local development server

Release

(this section is to help me remember the steps required to publish a new release)

To make a prerelease version for testing:

# begin a prerelease
npm version premajor --preid=rc # from 1.0.0 to 2.0.0-rc.0
npm version preminor --preid=rc # from 1.0.0 to 1.1.0-rc.0
npm version prepatch --preid=rc # from 1.0.0 to 1.0.1-rc.0

# increment the prerelease version number
npm version prerelease # from 2.0.0-rc.0 to 2.0.0-rc.1

# promote the prerelease version
npm version major # from 2.0.0-rc.1 to 2.0.0
npm version minor # from 1.1.0-beta.0 to 1.1.0
npm version patch # from 1.0.1-alpha.0 to 1.0.1

prosemirror-math's People

Contributors

benrbray avatar brianhung avatar saul-mirone avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

prosemirror-math's Issues

Problem with caret position of math_inline in RTL

When the editor direction is rtl, caret position acts wrong in inline mode and makes it unusable. Pressing left arrow key (or right) doesn't work correctly and gets stuck in a loop. It's easy to reproduce:
tuy5y5y5y

I tried to change updateCursorPos internally but didn't get anywhere. How can I change the cursorSide in tiptap2? I'm using the example from here.

Specialized Support for Arrays

One of the most annoying-times-frequency things among my latex experiences is making 2d grids by describing them using 1d text. Such grids include matrices, commutative diagrams, tables, multi-line derivations whose terms are aligned, and more. It'd be neat to provide a special "array mode" for parsing and editing text of the form

A & B & C & D & E \\  
A & B & C & D & E \\  
A & B & C & D & E \\  
A & B & C & D & E

This is the syntax for the latex body in each of all four examples above. The cells(' latex expressions) will typically all differ in length, creating a headache (do we spend time inserting white-space to align the columns, maintaining this alignment when we modify the cells? or do we ignore alignment issues in the source code and just count in our head when editing each row?)

It would be nice if when one's cursor enters a latex block of the above form, the cells in the latex block automatically align (wrt the ampersands and line-break symbols). If I correctly understand the spirit of prosemirror-math, one wants to avoid having popup dialog boxes or editors separate from the text. So perhaps the alignment could be as simple as inserting appropriate whitespace when the block is "active"!

Usage with TipTap: `Error: inner view should not exist!`

I've been trying to integrate TipTap with prosemirror-math. However, whenever I enter "editing mode" via mouse-click or arrow-keys, Error: inner view should not exist! popups. Here's the custom node I'm using (derived from #27):

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

import { inputRules } from "prosemirror-inputrules";

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

export const Math = 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, mathSelectPlugin];
  },
});

Expected Behavior

The extension to work :)

{
  "@tiptap/react": "^2.0.0-beta.114", 
  "@benrbray/prosemirror-math": "^0.2.2",
}

Current Behavior

Screen.Recording.2022-07-22.at.2.36.03.PM.mov

Test in Safari

ProseMirror (and by extension, prosemirror-math) relies on contenteditable to function. Since the implementation of contenteditable is not yet standardized between browsers, we must test manually in each browser to ensure acceptable behavior.

I do not own a Mac, so I cannot test prosemirror-math on Safari. Please report any bugs you find in Safari!

Paste from External Sources (MathJax in the wild, Wikipedia, etc.)

It would be great to modify the default paste behavior to automatically detect math markup in HTML pasted from external sources. Unfortunately, the solution will be messy, as there is not yet a universally-accepted way to render math on the web.

See Robert Miner 2010, "MathType, Math Markup, and the Goal of Cut and Paste" for a brief summary of the challenges faced in this area. Here's an except from one of the slides:

Math on the Web formats in the Wild

  • Image with TeX code (alt tags, comments, urls)
  • Some content is in text (HTML math, TeX source, ASCII art)
  • Some is in the DOM (MathML, s and CSS)

The following tasks are relatively low-effort and high-reward:

  • support pasting MathML expressions when the source TeX code is included as an annotation (this seems to be a standard feature in some mathjax configurations)
  • support pasting inline math images from Wikipedia when there is an alt tag present

Some higher-effort tasks:

Things to be cautious of:

  • MathJax and KaTeX both include the same math expression multiple times in the same block, e.g. rendered as MathML and SVG simultaneously for compatibility reasons. We need to identify the common parent element and ensure that it is replaced by a single math expression, rather than two or three.
  • Pasting behavior between different browsers

Here are some places we might expect users to paste from:

  • Wikipedia: Extremely inconsistent -- pages have a mix of MathJax, HTML math, and pre-rendered images
  • StackExchange: Uses MathJax. The source code is evidently stored in a <script type="math/tex; mode=display"> tag within a .math-container-classed element.
  • ncatlab: Uses MathJax. Source is stored in a <annotation encoding="application/x-tex"> tag.
  • Planet Math: Uses MathJax, with some weird layouts. Display math is sometimes wrapped in a <table class="ltx_equation ltx_eqn_table"> element. The MathML node has an an alttext attribute containing the TeX source.
  • arXiv: (example) Uses MathJax with the source stored in a <script type="math/tex"> tag.
  • ProofWiki: uses MathJax with source in a <script type="math/tex"> tag
  • Google Docs: ???
  • Microsoft Word: ???

Plugin to detect math in pasted LaTeX-like text

Currently, consumers of prosemirror-math can set up custom paste behavior for their own configuration, but it would be helpful if we provided some tools to make it easier.

So, prosemirror-math should export an optional Plugin that detects dollar signs (or another user-configurable math delimiter) in pasted plain text and converts the encompassed text to an inline or block math node.

However, handling non-math dollar signs will be tricky, since it is unlikely that they will already be escaped in the pasted text. So, we will need to apply some common-sense criterion to determine whether a dollar sign corresponds to math or not. For example,

  • There are usually no spaces before a closing math dollar sign, so no math node should be detected in the example Billy has $4 and Sally has $3. When pasting, the dollar signs should be automatically escaped by prosemirror-math.

Preview panel support

Hi Benrbary:

Milkdown uses this awesome project and we have some feed back from users. It would be great if this library can support the preview panel feature like in typora:

image

I tried to just display the render block when math node is selected, but it seems that it won't update when user editing.
So I think maybe it should be supported in this library.

original issue:
Milkdown/milkdown#91

Support for async KaTeX

Hi benrbray,

Thank you for this library, I have integrated it into Starboard Notebook (you can try it here). Before the NPM package was released I vendored all the code (read: copy pasted), but now that it's out I would love to get rid of my own version.

I was only able to remove about half of the code: in Starboard Notebook I load as much stuff asynchronously as possible to keep the initial load and render time fast, KaTeX included.

Basically the only change I made is this in NodeView:

katexLoader.then((katex) => {
    if (!this._mathRenderElt) return;
    // render katex, but fail gracefully
    try {
        katex.render(texString, this._mathRenderElt, this._katexOptions);
        this._mathRenderElt.classList.remove("parse-error");
        this.dom.setAttribute("title", "");
    } catch (err) {
        if (err instanceof ParseError) {
            console.error(err);
            this._mathRenderElt.classList.add("parse-error");
            this.dom.setAttribute("title", err.toString());
        } else {
            throw err;
        }
    }
});

I don't know the best way to fit this into the library.

Perhaps instead of importing katex directly one could pass an object with this signature:

interface MathRenderer {
  render(value: string, element: HTMLElement, rendererOptions: any): Promise<{ok: true} | {ok: false, error: Error}>
}

I saw in another issue that you wanted to support Mathjax as well, this would be a step towards supporting that too.

Editor loses focus when deleting empty line following math block

Steps to reproduce:

  • Create a math block with some text in it.
  • Create an empty line after the math block.
  • Move the cursor to the beginning of the empty line and press backspace. The editor will lose focus.

Expected behavior:

  • The math node should expand with the cursor in the rightmost position

Confirmed in Chrome and Firefox.

InputRules should ignore escaped `$` symbols

I write many more LaTeX equations than dollar amounts, but I recognize that I am in the minority. For accessibility, prosemirror-math should have a standard (but configurable) way of escaping $ characters.

At the moment, the default inline input rule uses a negative-lookbehind regex to avoid inserting math nodes, but this has a few limitations:

  • Negative lookbehinds are currently only supported in new-ish versions of Chrome and FireFox
  • Sometimes, the input rules will accidentally latch on to a $ symbol in a distant node and assume the user wants to create a math block.

Solutions to consider:

  • Modify ProseMirror's InputRule implementation to support escape sequences before a matched string
  • By default, prosemirror-math should assume all $'s are math-related, unless the user explicitly indicates otherwise. For example, any of the following actions should create an escaped $
    • use a backslash \$ to escape math
    • or put a space after the dollar sign $ to escape math (automaticaly remove the space)

Pasting math into editor

Hi, here the parse rules are defined. However, they work only when parsing the document, not when pasting the code into editor. How to enable pasting based on these rules?

Publish to npm?

Really like the approach that you have taken here! I am thinking about integrating something like this to our project https://github.com/curvenote/editor which needs better math support. :)

I don't think that you have published this yet to npm? Do you have plans to?

I think there would also need to be a bit of a distinction between the current index, which is a demo if I am not mistaken, and exposing the relevant plugins to another prosemirror implementation.

Happy to help if you are going in this direction!

KaTeX Math Source should be ignored by ProseMirror inputRules

Given a ProseMirror instance with both prosemirror-math and an input rule for automatically detecting *italic* text, type the following:

The two functions $f(n)=(5+n)*2$ and $g(n)=(n*2)+10$ are...

Since math source is currently represented as a text node, ProseMirror will scan the math source processing inputRules. In this example, it will recognize the substring *2$ and $g(n)=(n* as italic text and erase one of the math blocks.

Possible solutions:

  • keep math source in an attr instead
  • re-implement the inputRules to somehow ignore custom nodeViews

static HTML for math node views

How to generate rendered math for static html? Currently math is nicely rendered in editing mode when it's not rendered when using DOMSerializer to get static HTML

Readonly mode

I think prosemirror's contenteditable can be set to false when we want to make the editor readonly, so prosemirror-math should also support this feature.

MathView throws unknown errors instead of handling them like ParseError

Hi benrbray,

Thank you for this great library, I have integrated it into the editor of Jade, my side-project to build a spatial note-taking tool.

An issue I notice recently is that MathView throws the KaTeX error if it's not a ParseError. It happens, for example, when I type this invalid LaTeX: \gdef\bar#1#{#1^2} \bar{y} + \bar{y}.

try {
	katex.render(texString, this._mathRenderElt, this._katexOptions);
	this._mathRenderElt.classList.remove("parse-error");
	this.dom.setAttribute("title", "");
} catch (err) {
	if (err instanceof ParseError) {
		console.error(err);
		this._mathRenderElt.classList.add("parse-error");
		this.dom.setAttribute("title", err.toString());
	} else {
		throw err; // <<
	}
}

Throwing the error means I need to handle it at higher level, or my app would crash.
But when I catch the error in the app, I cannot easily know which MathView threw it and I have no access to _mathRenderElt to show the error message in place.

I think a better solution is to handle it the same way as handling ParseError: show the error message in place to notify users to correct their LaTeX. You can take a look at the approach in my fork here.

Refresh All Math

Include a ProseMirror command to re-load all math in the current editor. This is useful e.g. if the user has re-defined a KaTeX command with \providecommand and wishes to see the result.

Math inline gets in the way of normal sentences.

eg. When the input is Jimmy has $5, Rachel has $6, how much do the kids have in total?

The result is Jimmy has 5, Rachel has 6, how much do the kids have in total?

There needs to be a way to exclude certain parses. Perhaps the delimiters can be $$...$$ for inline math, and $$$...$$$ for math block?

Copy / Paste to Plain Text should Include Dollar Signs `$`

At the moment, prosemirror-math relies on the default copy/paste behavior of ProseMirror, which simply calls textBetween on the current selection when copying. Since the $ symbols are just visual decorations in the editor, they don't appear as part of the text itself, and so will not appear when pasting from prosemirror-math into a plaintext editor.

Support Tiptap

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.

Undo

I'm using prosemirror-math as an in-browser text editor. For this it would be useful to have someway to undo changes --- e.g. "[esc]"-then-"u" or "[ctrl]"-and-"z". One way to do this is to maintain a stack for the history. Maintaining such a redundant history can in principle cost lots of space, but since these are human-generated strings of text, that might be a negligible event.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.