feat: add math support

This commit is contained in:
thecodrr
2022-06-30 17:27:56 +05:00
parent 73507e1771
commit dd66544ee8
70 changed files with 3180 additions and 25 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
/// <reference types="react" />
import { MenuItem } from "./types"; import { MenuItem } from "./types";
export declare function useFocus(items: MenuItem[], onAction: (event: KeyboardEvent) => void, onClose: (event: KeyboardEvent) => void): { export declare function useFocus(items: MenuItem[], onAction: (event: KeyboardEvent) => void, onClose: (event: KeyboardEvent) => void): {
focusIndex: number; focusIndex: number;

View File

@@ -0,0 +1,2 @@
import { Extension } from "@tiptap/core";
export declare const Codemark: Extension<any, any>;

View File

@@ -0,0 +1,9 @@
import { Extension } from "@tiptap/core";
import codemark from "prosemirror-codemark";
// import "prosemirror-codemark/dist/codemark.css";
export var Codemark = Extension.create({
name: "codemarkPlugin",
addProseMirrorPlugins: function () {
return codemark({ markType: this.editor.schema.marks.code });
},
});

View File

@@ -0,0 +1,2 @@
export * from "./code-mark";
export { Codemark as default } from "./code-mark";

View File

@@ -0,0 +1,2 @@
export * from "./code-mark";
export { Codemark as default } from "./code-mark";

View File

@@ -0,0 +1,2 @@
export { MathInline } from "./math-inline";
export { MathBlock } from "./math-block";

View File

@@ -0,0 +1,2 @@
export { MathInline } from "./math-inline";
export { MathBlock } from "./math-block";

View File

@@ -0,0 +1,2 @@
import { Node } from "@tiptap/core";
export declare const MathBlock: Node<any, any>;

View File

@@ -0,0 +1,31 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { inputRules } from "prosemirror-inputrules";
import { makeBlockMathInputRule, REGEX_BLOCK_MATH_DOLLARS, } from "./plugin";
export var MathBlock = Node.create({
name: "math_display",
group: "block math",
content: "text*",
atom: true,
code: true,
parseHTML: function () {
return [
{
tag: "div[class*='math-display']", // important!
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"div",
mergeAttributes({ class: "math-display math-node" }, HTMLAttributes),
0,
];
},
addProseMirrorPlugins: function () {
var inputRulePlugin = inputRules({
rules: [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, this.type)],
});
return [inputRulePlugin];
},
});

View File

@@ -0,0 +1,3 @@
import { Node } from "@tiptap/core";
import "katex/dist/katex.min.css";
export declare const MathInline: Node<any, any>;

View File

@@ -0,0 +1,33 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { inputRules } from "prosemirror-inputrules";
import { makeInlineMathInputRule, REGEX_INLINE_MATH_DOLLARS, mathPlugin, } from "./plugin";
import "katex/dist/katex.min.css";
export var MathInline = Node.create({
name: "math_inline",
group: "inline math",
content: "text*",
inline: true,
atom: true,
code: true,
parseHTML: function () {
return [
{
tag: "span[class*='math-inline']", // important!,
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"span",
mergeAttributes({ class: "math-inline math-node" }, HTMLAttributes),
0,
];
},
addProseMirrorPlugins: function () {
var inputRulePlugin = inputRules({
rules: [makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)],
});
return [mathPlugin, inputRulePlugin];
},
});

View File

@@ -0,0 +1,16 @@
import { Command } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
/**
* A ProseMirror command for determining whether to exit a math block, based on
* specific conditions. Normally called when the user has
*
* @param outerView The main ProseMirror EditorView containing this math node.
* @param dir Used to indicate desired cursor position upon closing a math node.
* When set to -1, cursor will be placed BEFORE the math node.
* When set to +1, cursor will be placed AFTER the math node.
* @param borderMode An exit condition based on cursor position and direction.
* @param requireEmptySelection When TRUE, only exit the math node when the
* (inner) selection is empty.
* @returns A new ProseMirror command based on the input configuration.
*/
export declare function collapseMathCmd(outerView: EditorView, dir: 1 | -1, requireOnBorder: boolean, requireEmptySelection?: boolean): Command;

View File

@@ -0,0 +1,51 @@
import { TextSelection } from "prosemirror-state";
/**
* A ProseMirror command for determining whether to exit a math block, based on
* specific conditions. Normally called when the user has
*
* @param outerView The main ProseMirror EditorView containing this math node.
* @param dir Used to indicate desired cursor position upon closing a math node.
* When set to -1, cursor will be placed BEFORE the math node.
* When set to +1, cursor will be placed AFTER the math node.
* @param borderMode An exit condition based on cursor position and direction.
* @param requireEmptySelection When TRUE, only exit the math node when the
* (inner) selection is empty.
* @returns A new ProseMirror command based on the input configuration.
*/
export function collapseMathCmd(outerView, dir, requireOnBorder, requireEmptySelection) {
if (requireEmptySelection === void 0) { requireEmptySelection = true; }
// create a new ProseMirror command based on the input conditions
return function (innerState, dispatch) {
// get selection info
var outerState = outerView.state;
var _a = outerState.selection, outerTo = _a.to, outerFrom = _a.from;
var _b = innerState.selection, innerTo = _b.to, innerFrom = _b.from;
// only exit math node when selection is empty
if (requireEmptySelection && innerTo !== innerFrom) {
return false;
}
var currentPos = dir > 0 ? innerTo : innerFrom;
// when requireOnBorder is TRUE, collapse only when cursor
// is about to leave the bounds of the math node
if (requireOnBorder) {
// (subtract two from nodeSize to account for start and end tokens)
var nodeSize = innerState.doc.nodeSize - 2;
// early return if exit conditions not met
if (dir > 0 && currentPos < nodeSize) {
return false;
}
if (dir < 0 && currentPos > 0) {
return false;
}
}
// all exit conditions met, so close the math node by moving the cursor outside
if (dispatch) {
// set outer selection to be outside of the nodeview
var targetPos = dir > 0 ? outerTo : outerFrom;
outerView.dispatch(outerState.tr.setSelection(TextSelection.create(outerState.doc, targetPos)));
// must return focus to the outer view, otherwise no cursor will appear
outerView.focus();
}
return true;
};
}

View File

@@ -0,0 +1,12 @@
import { Command } from "prosemirror-state";
import { NodeType } from "prosemirror-model";
/**
* Returns a new command that can be used to inserts a new math node at the
* user's current document position, provided that the document schema actually
* allows a math node to be placed there.
*
* @param mathNodeType An instance for either your math_inline or math_display
* NodeType. Must belong to the same schema that your EditorState uses!
* @param initialText (optional) The initial source content for the math editor.
*/
export declare function insertMathCmd(mathNodeType: NodeType, initialText?: string): Command;

View File

@@ -0,0 +1,27 @@
import { NodeSelection } from "prosemirror-state";
////////////////////////////////////////////////////////////////////////////////
/**
* Returns a new command that can be used to inserts a new math node at the
* user's current document position, provided that the document schema actually
* allows a math node to be placed there.
*
* @param mathNodeType An instance for either your math_inline or math_display
* NodeType. Must belong to the same schema that your EditorState uses!
* @param initialText (optional) The initial source content for the math editor.
*/
export function insertMathCmd(mathNodeType, initialText) {
if (initialText === void 0) { initialText = ""; }
return function (state, dispatch) {
var $from = state.selection.$from, index = $from.index();
if (!$from.parent.canReplaceWith(index, index, mathNodeType)) {
return false;
}
if (dispatch) {
var mathNode = mathNodeType.create({}, initialText ? state.schema.text(initialText) : null);
var tr = state.tr.replaceSelectionWith(mathNode);
tr = tr.setSelection(NodeSelection.create(tr.doc, $from.pos));
dispatch(tr);
}
return true;
};
}

View File

@@ -0,0 +1,10 @@
import { Command } from "prosemirror-state";
/**
* Some browsers (cough firefox cough) don't properly handle cursor movement on
* the edges of a NodeView, so we need to make the desired behavior explicit.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1252108
*/
export declare function nudgeCursorCmd(dir: -1 | 0 | 1): Command;
export declare const nudgeCursorForwardCmd: Command;
export declare const nudgeCursorBackCmd: Command;

View File

@@ -0,0 +1,25 @@
import { TextSelection } from "prosemirror-state";
////////////////////////////////////////////////////////////////////////////////
/**
* Some browsers (cough firefox cough) don't properly handle cursor movement on
* the edges of a NodeView, so we need to make the desired behavior explicit.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1252108
*/
export function nudgeCursorCmd(dir) {
return function (innerState, dispatch) {
var _a = innerState.selection, to = _a.to, from = _a.from;
// compute target position
var emptySelection = to === from;
var currentPos = dir < 0 ? from : to;
var increment = emptySelection ? dir : 0;
var nodeSize = innerState.doc.nodeSize;
var targetPos = Math.max(0, Math.min(nodeSize, currentPos + increment));
if (dispatch) {
dispatch(innerState.tr.setSelection(TextSelection.create(innerState.doc, targetPos)));
}
return true;
};
}
export var nudgeCursorForwardCmd = nudgeCursorCmd(+1);
export var nudgeCursorBackCmd = nudgeCursorCmd(-1);

View File

@@ -0,0 +1,9 @@
export { MathView, type ICursorPosObserver } from "./math-node-view";
export { mathPlugin, createMathView, type IMathPluginState, } from "./math-plugin";
export { mathSchemaSpec, createMathSchema } from "./math-schema";
export { mathBackspaceCmd } from "./plugins/math-backspace";
export { makeBlockMathInputRule, makeInlineMathInputRule, REGEX_BLOCK_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS_ESCAPED, } from "./plugins/math-input-rules";
export { mathSelectPlugin } from "./plugins/math-select";
export { insertMathCmd } from "./commands/insert-math-cmd";
export { mathSerializer } from "./utils/text-serializer";
export * from "./utils/types";

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// core functionality
export { MathView } from "./math-node-view";
export { mathPlugin, createMathView, } from "./math-plugin";
export { mathSchemaSpec, createMathSchema } from "./math-schema";
// recommended plugins
export { mathBackspaceCmd } from "./plugins/math-backspace";
export { makeBlockMathInputRule, makeInlineMathInputRule, REGEX_BLOCK_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS_ESCAPED, } from "./plugins/math-input-rules";
// optional / experimental plugins
export { mathSelectPlugin } from "./plugins/math-select";
// commands
export { insertMathCmd } from "./commands/insert-math-cmd";
// utilities
export { mathSerializer } from "./utils/text-serializer";
export * from "./utils/types";

View File

@@ -0,0 +1,66 @@
import { Node as ProseNode } from "prosemirror-model";
import { EditorState, Transaction, PluginKey } from "prosemirror-state";
import { NodeView, EditorView, Decoration, DecorationSource } from "prosemirror-view";
import { IMathPluginState } from "./math-plugin";
import { MathRenderFn } from "./renderers/types";
export interface ICursorPosObserver {
/** indicates on which side cursor should appear when this node is selected */
cursorSide: "start" | "end";
/** */
updateCursorPos(state: EditorState): void;
}
interface IMathViewOptions {
/** Dom element name to use for this NodeView */
tagName?: string;
/** Used to render the Tex input */
renderer: MathRenderFn;
/** Should be true if node is inline */
inline?: boolean;
}
export declare class MathView implements NodeView, ICursorPosObserver {
private _node;
private _outerView;
private _getPos;
dom: HTMLElement;
private _mathRenderElt;
private _mathSrcElt;
private _innerView;
cursorSide: "start" | "end";
private _tagName;
private _isEditing;
private _mathPluginKey;
private options;
/**
* @param onDestroy Callback for when this NodeView is destroyed.
* This NodeView should unregister itself from the list of ICursorPosObservers.
*
* Math Views support the following options:
* @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
* @option tagName HTML tag name to use for this NodeView. If none is provided,
* will use the node name with underscores converted to hyphens.
*/
constructor(node: ProseNode, view: EditorView, getPos: () => number, options: IMathViewOptions, mathPluginKey: PluginKey<IMathPluginState>, onDestroy?: () => void);
destroy(): void;
/**
* Ensure focus on the inner editor whenever this node has focus.
* This helps to prevent accidental deletions of math blocks.
*/
ensureFocus(): void;
update(node: ProseNode, _decorations: readonly Decoration[], _innerDecorations: DecorationSource): boolean;
updateCursorPos(state: EditorState): void;
selectNode(): void;
deselectNode(): void;
stopEvent(event: Event): boolean;
ignoreMutation(): boolean;
renderMath(): void;
dispatchInner(tr: Transaction): void;
openEditor(): void;
/**
* Called when the inner ProseMirror editor should close.
*
* @param render Optionally update the rendered math after closing. (which
* is generally what we want to do, since the user is done editing!)
*/
closeEditor(render?: boolean): void;
}
export {};

View File

@@ -0,0 +1,282 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
import { EditorState, TextSelection, } from "prosemirror-state";
import { EditorView, } from "prosemirror-view";
import { StepMap } from "prosemirror-transform";
import { keymap } from "prosemirror-keymap";
import { newlineInCode, chainCommands, deleteSelection, } from "prosemirror-commands";
import { collapseMathCmd } from "./commands/collapse-math-cmd";
var MathView = /** @class */ (function () {
// == Lifecycle ===================================== //
/**
* @param onDestroy Callback for when this NodeView is destroyed.
* This NodeView should unregister itself from the list of ICursorPosObservers.
*
* Math Views support the following options:
* @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
* @option tagName HTML tag name to use for this NodeView. If none is provided,
* will use the node name with underscores converted to hyphens.
*/
function MathView(node, view, getPos, options, mathPluginKey, onDestroy) {
var _this = this;
// store arguments
this.options = options;
this._node = node;
this._outerView = view;
this._getPos = getPos;
this._mathPluginKey = mathPluginKey;
// editing state
this.cursorSide = "start";
this._isEditing = false;
// options
this._tagName = options.tagName || this._node.type.name.replace("_", "-");
// create dom representation of nodeview
this.dom = document.createElement(this._tagName);
if (options.inline)
this.dom.classList.add("math-inline");
else
this.dom.classList.add("math-display");
this.dom.classList.add("math-node");
this._mathRenderElt = document.createElement("span");
this._mathRenderElt.textContent = "";
this._mathRenderElt.classList.add("math-render");
this.dom.appendChild(this._mathRenderElt);
this._mathSrcElt = document.createElement("span");
this._mathSrcElt.classList.add("math-src");
this.dom.appendChild(this._mathSrcElt);
// ensure
this.dom.addEventListener("click", function () { return _this.ensureFocus(); });
// render initial content
this.renderMath();
}
MathView.prototype.destroy = function () {
// close the inner editor without rendering
this.closeEditor(false);
// clean up dom elements
if (this._mathRenderElt) {
this._mathRenderElt.remove();
delete this._mathRenderElt;
}
if (this._mathSrcElt) {
this._mathSrcElt.remove();
delete this._mathSrcElt;
}
this.dom.remove();
};
/**
* Ensure focus on the inner editor whenever this node has focus.
* This helps to prevent accidental deletions of math blocks.
*/
MathView.prototype.ensureFocus = function () {
if (this._innerView && this._outerView.hasFocus()) {
this._innerView.focus();
}
};
// == Updates ======================================= //
MathView.prototype.update = function (node, _decorations, _innerDecorations) {
if (!node.sameMarkup(this._node))
return false;
this._node = node;
if (this._innerView) {
var state = this._innerView.state;
var start = node.content.findDiffStart(state.doc.content);
if (start != null) {
var diff = node.content.findDiffEnd(state.doc.content);
if (diff) {
var endA = diff.a, endB = diff.b;
var overlap = start - Math.min(endA, endB);
if (overlap > 0) {
endA += overlap;
endB += overlap;
}
this._innerView.dispatch(state.tr
.replace(start, endB, node.slice(start, endA))
.setMeta("fromOutside", true));
}
}
}
if (!this._isEditing) {
this.renderMath();
}
return true;
};
MathView.prototype.updateCursorPos = function (state) {
var pos = this._getPos();
var size = this._node.nodeSize;
var inPmSelection = state.selection.from < pos + size && pos < state.selection.to;
if (!inPmSelection) {
this.cursorSide = pos < state.selection.from ? "end" : "start";
}
};
// == Events ===================================== //
MathView.prototype.selectNode = function () {
if (!this._outerView.editable) {
return;
}
this.dom.classList.add("ProseMirror-selectednode");
if (!this._isEditing) {
this.openEditor();
}
};
MathView.prototype.deselectNode = function () {
this.dom.classList.remove("ProseMirror-selectednode");
if (this._isEditing) {
this.closeEditor();
}
};
MathView.prototype.stopEvent = function (event) {
return (this._innerView !== undefined &&
event.target !== undefined &&
this._innerView.dom.contains(event.target));
};
MathView.prototype.ignoreMutation = function () {
return true;
};
// == Rendering ===================================== //
MathView.prototype.renderMath = function () {
if (!this._mathRenderElt) {
return;
}
// get tex string to render
var content = this._node.content.content;
var texString = "";
if (content.length > 0 && content[0].textContent !== null) {
texString = content[0].textContent.trim();
}
// empty math?
if (texString.length < 1) {
this.dom.classList.add("empty-math");
// clear rendered math, since this node is in an invalid state
while (this._mathRenderElt.firstChild) {
this._mathRenderElt.firstChild.remove();
}
// do not render empty math
return;
}
else {
this.dom.classList.remove("empty-math");
}
// render katex, but fail gracefully
try {
this.options.renderer(texString, this._mathRenderElt);
this._mathRenderElt.classList.remove("parse-error");
this.dom.setAttribute("title", "");
}
catch (err) {
if (err instanceof Error) {
console.error(err);
this._mathRenderElt.classList.add("parse-error");
this.dom.setAttribute("title", err.toString());
}
}
};
// == Inner Editor ================================== //
MathView.prototype.dispatchInner = function (tr) {
if (!this._innerView) {
return;
}
var _a = this._innerView.state.applyTransaction(tr), state = _a.state, transactions = _a.transactions;
this._innerView.updateState(state);
if (!tr.getMeta("fromOutside")) {
var outerTr = this._outerView.state.tr, offsetMap = StepMap.offset(this._getPos() + 1);
for (var i = 0; i < transactions.length; i++) {
var steps = transactions[i].steps;
for (var j = 0; j < steps.length; j++) {
var mapped = steps[j].map(offsetMap);
if (!mapped) {
throw Error("step discarded!");
}
outerTr.step(mapped);
}
}
if (outerTr.docChanged)
this._outerView.dispatch(outerTr);
}
};
MathView.prototype.openEditor = function () {
var _this = this;
var _a;
if (this._innerView) {
throw Error("inner view should not exist!");
}
if (!this._mathSrcElt)
throw new Error("_mathSrcElt does not exist!");
// create a nested ProseMirror view
this._innerView = new EditorView(this._mathSrcElt, {
state: EditorState.create({
doc: this._node,
plugins: [
keymap({
Tab: function (state, dispatch) {
if (dispatch) {
dispatch(state.tr.insertText("\t"));
}
return true;
},
Backspace: chainCommands(deleteSelection, function (state, dispatch, tr_inner) {
// default backspace behavior for non-empty selections
if (!state.selection.empty) {
return false;
}
// default backspace behavior when math node is non-empty
if (_this._node.textContent.length > 0) {
return false;
}
// otherwise, we want to delete the empty math node and focus the outer view
_this._outerView.dispatch(_this._outerView.state.tr.insertText(""));
_this._outerView.focus();
return true;
}),
// "Ctrl-Backspace": (state, dispatch, tr_inner) => {
// // delete math node and focus the outer view
// this._outerView.dispatch(this._outerView.state.tr.insertText(""));
// this._outerView.focus();
// return true;
// },
Enter: chainCommands(newlineInCode, collapseMathCmd(this._outerView, +1, false)),
"Ctrl-Enter": collapseMathCmd(this._outerView, +1, false),
ArrowLeft: collapseMathCmd(this._outerView, -1, true),
ArrowRight: collapseMathCmd(this._outerView, +1, true),
ArrowUp: collapseMathCmd(this._outerView, -1, true),
ArrowDown: collapseMathCmd(this._outerView, +1, true),
}),
],
}),
dispatchTransaction: this.dispatchInner.bind(this),
});
// focus element
var innerState = this._innerView.state;
this._innerView.focus();
// request outer cursor position before math node was selected
var maybePos = (_a = this._mathPluginKey.getState(this._outerView.state)) === null || _a === void 0 ? void 0 : _a.prevCursorPos;
if (maybePos === null || maybePos === undefined) {
console.error("[prosemirror-math] Error: Unable to fetch math plugin state from key.");
}
var prevCursorPos = maybePos !== null && maybePos !== void 0 ? maybePos : 0;
// compute position that cursor should appear within the expanded math node
var innerPos = prevCursorPos <= this._getPos() ? 0 : this._node.nodeSize - 2;
this._innerView.dispatch(innerState.tr.setSelection(TextSelection.create(innerState.doc, innerPos)));
this._isEditing = true;
};
/**
* Called when the inner ProseMirror editor should close.
*
* @param render Optionally update the rendered math after closing. (which
* is generally what we want to do, since the user is done editing!)
*/
MathView.prototype.closeEditor = function (render) {
if (render === void 0) { render = true; }
if (this._innerView) {
this._innerView.destroy();
this._innerView = undefined;
}
if (render) {
this.renderMath();
}
this._isEditing = false;
};
return MathView;
}());
export { MathView };

View File

@@ -0,0 +1,24 @@
import { Node as ProseNode } from "prosemirror-model";
import { Plugin as ProsePlugin } from "prosemirror-state";
import { MathView } from "./math-node-view";
import { EditorView } from "prosemirror-view";
export interface IMathPluginState {
macros: {
[cmd: string]: string;
};
/** A list of currently active `NodeView`s, in insertion order. */
activeNodeViews: MathView[];
/**
* Used to determine whether to place the cursor in the front- or back-most
* position when expanding a math node, without overriding the default arrow
* key behavior.
*/
prevCursorPos: number;
}
/**
* Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
* @param inline TRUE for block math, FALSE for inline math.
* @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
*/
export declare function createMathView(inline: boolean): (node: ProseNode, view: EditorView, getPos: boolean | (() => number)) => MathView;
export declare const mathPlugin: ProsePlugin<IMathPluginState>;

View File

@@ -0,0 +1,71 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
import { Plugin as ProsePlugin, PluginKey, } from "prosemirror-state";
import { MathView } from "./math-node-view";
import { KatexRenderer } from "./renderers/katex";
// uniquely identifies the prosemirror-math plugin
var MATH_PLUGIN_KEY = new PluginKey("prosemirror-math");
/**
* Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
* @param inline TRUE for block math, FALSE for inline math.
* @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
*/
export function createMathView(inline) {
return function (node, view, getPos) {
/** @todo is this necessary?
* Docs says that for any function proprs, the current plugin instance
* will be bound to `this`. However, the typings don't reflect this.
*/
var pluginState = MATH_PLUGIN_KEY.getState(view.state);
if (!pluginState) {
throw new Error("no math plugin!");
}
var nodeViews = pluginState.activeNodeViews;
// set up NodeView
var nodeView = new MathView(node, view, getPos, {
inline: inline,
renderer: inline ? KatexRenderer.inline : KatexRenderer.block,
tagName: inline ? "span" : "div",
}, MATH_PLUGIN_KEY, function () {
nodeViews.splice(nodeViews.indexOf(nodeView));
});
nodeViews.push(nodeView);
return nodeView;
};
}
var mathPluginSpec = {
key: MATH_PLUGIN_KEY,
state: {
init: function (config, instance) {
return {
macros: {},
activeNodeViews: [],
prevCursorPos: 0,
};
},
apply: function (tr, value, oldState, newState) {
// produce updated state field for this plugin
var newPos = newState.selection.from;
var oldPos = oldState.selection.from;
return {
// these values are left unchanged
activeNodeViews: value.activeNodeViews,
macros: value.macros,
// update with the second-most recent cursor pos
prevCursorPos: oldPos !== newPos ? oldPos : value.prevCursorPos,
};
},
/** @todo (8/21/20) implement serialization for math plugin */
// toJSON(value) { },
// fromJSON(config, value, state){ return {}; }
},
props: {
nodeViews: {
math_inline: createMathView(true),
math_display: createMathView(false),
},
},
};
export var mathPlugin = new ProsePlugin(mathPluginSpec);

View File

@@ -0,0 +1,45 @@
import { MarkSpec, NodeSpec, Schema, SchemaSpec } from "prosemirror-model";
import { SchemaSpecMarkT, SchemaSpecNodeT } from "./utils/types";
/**
* Borrowed from ProseMirror typings, modified to exclude OrderedMaps in spec,
* in order to help with the schema-building functions below.
*
* NOTE: TypeScript's typings for the spread operator { ...a, ...b } are only
* an approximation to the true type, and have difficulty with optional fields.
* So, unlike the SchemaSpec type, the `marks` field is NOT optional here.
*
* function example<T extends string>(x: { [name in T]: string; } | null) {
* const s = { ...x }; // inferred to have type `{}`.
* }
*
* @see https://github.com/microsoft/TypeScript/issues/10727
*/
interface SchemaSpecJson<N extends string = any, M extends string = any> extends SchemaSpec<N, M> {
nodes: {
[name in N]: NodeSpec;
};
marks: {
[name in M]: MarkSpec;
};
topNode?: string;
}
declare type MathSpecNodeT = SchemaSpecNodeT<typeof mathSchemaSpec>;
declare type MathSpecMarkT = SchemaSpecMarkT<typeof mathSchemaSpec>;
export declare const mathSchemaSpec: SchemaSpecJson<"math_inline" | "paragraph" | "text" | "doc" | "math_display", "math_select">;
/**
* Use the prosemirror-math default SchemaSpec to create a new Schema.
*/
export declare function createMathSchema(): Schema<"math_inline" | "paragraph" | "text" | "doc" | "math_display", "math_select">;
/**
* Create a new SchemaSpec by adding math nodes to an existing spec.
* @deprecated This function is included for demonstration/testing only. For the
* time being, I highly recommend adding the math nodes manually to your own
* ProseMirror spec to avoid unexpected interactions between the math nodes
* and your own spec. Use the example spec for reference.
*
* @param baseSpec The SchemaSpec to extend. Must specify a `marks` field, and
* must be a raw object (not an OrderedMap).
*/
export declare function extendMathSchemaSpec<N extends string, M extends string>(baseSpec: SchemaSpecJson<N, M>): SchemaSpecJson<N | MathSpecNodeT, M | MathSpecMarkT>;
export {};

View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
// prosemirror imports
import { Schema, } from "prosemirror-model";
import { defaultBlockMathParseRules, defaultInlineMathParseRules, } from "./plugins/math-paste-rules";
////////////////////////////////////////////////////////////
// force typescript to infer generic type arguments for SchemaSpec
function createSchemaSpec(spec) {
return spec;
}
// bare minimum ProseMirror schema for working with math nodes
export var mathSchemaSpec = createSchemaSpec({
nodes: {
// :: NodeSpec top-level document node
doc: {
content: "block+",
},
paragraph: {
content: "inline*",
group: "block",
parseDOM: [{ tag: "p" }],
toDOM: function () {
return ["p", 0];
},
},
math_inline: {
group: "inline math",
content: "text*",
inline: true,
atom: true,
toDOM: function () { return ["math-inline", { class: "math-node" }, 0]; },
parseDOM: __spreadArray([{ tag: "math-inline" }], __read(defaultInlineMathParseRules), false),
},
math_display: {
group: "block math",
content: "text*",
atom: true,
code: true,
toDOM: function () { return ["math-display", { class: "math-node" }, 0]; },
parseDOM: __spreadArray([{ tag: "math-display" }], __read(defaultBlockMathParseRules), false),
},
text: {
group: "inline",
},
},
marks: {
math_select: {
toDOM: function () {
return ["math-select", 0];
},
parseDOM: [{ tag: "math-select" }],
},
},
});
/**
* Use the prosemirror-math default SchemaSpec to create a new Schema.
*/
export function createMathSchema() {
return new Schema(mathSchemaSpec);
}
/**
* Create a new SchemaSpec by adding math nodes to an existing spec.
* @deprecated This function is included for demonstration/testing only. For the
* time being, I highly recommend adding the math nodes manually to your own
* ProseMirror spec to avoid unexpected interactions between the math nodes
* and your own spec. Use the example spec for reference.
*
* @param baseSpec The SchemaSpec to extend. Must specify a `marks` field, and
* must be a raw object (not an OrderedMap).
*/
export function extendMathSchemaSpec(baseSpec) {
var nodes = __assign(__assign({}, baseSpec.nodes), mathSchemaSpec.nodes);
var marks = __assign(__assign({}, baseSpec.marks), mathSchemaSpec.marks);
return { nodes: nodes, marks: marks, topNode: baseSpec.topNode };
}

View File

@@ -0,0 +1,2 @@
import { Command as ProseCommand } from "prosemirror-state";
export declare const mathBackspaceCmd: ProseCommand;

View File

@@ -0,0 +1,25 @@
import { NodeSelection } from "prosemirror-state";
export var mathBackspaceCmd = function (state, dispatch) {
// check node before
var $from = state.selection.$from;
var nodeBefore = $from.nodeBefore;
if (!nodeBefore) {
return false;
}
if (nodeBefore.type.name == "math_inline") {
// select math node
var index = $from.index($from.depth);
var $beforePos = state.doc.resolve($from.posAtIndex(index - 1));
if (dispatch) {
dispatch(state.tr.setSelection(new NodeSelection($beforePos)));
}
return true;
}
else if (nodeBefore.type.name == "math_block") {
/** @todo (8/1/20) implement backspace for math blocks
* check how code blocks behave when pressing backspace
*/
return false;
}
return false;
};

View File

@@ -0,0 +1,7 @@
import { InputRule } from "prosemirror-inputrules";
import { NodeType } from "prosemirror-model";
export declare const REGEX_INLINE_MATH_DOLLARS: RegExp;
export declare const REGEX_INLINE_MATH_DOLLARS_ESCAPED: RegExp;
export declare const REGEX_BLOCK_MATH_DOLLARS: RegExp;
export declare function makeInlineMathInputRule(pattern: RegExp, nodeType: NodeType, getAttrs?: (match: string[]) => any): InputRule;
export declare function makeBlockMathInputRule(pattern: RegExp, nodeType: NodeType, getAttrs?: (match: string[]) => any): InputRule;

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
import { InputRule } from "prosemirror-inputrules";
import { NodeSelection } from "prosemirror-state";
////////////////////////////////////////////////////////////
// ---- Inline Input Rules ------------------------------ //
// simple input rule for inline math
export var REGEX_INLINE_MATH_DOLLARS = /\$\$(.+)\$\$/; //new RegExp("\$(.+)\$", "i");
// negative lookbehind regex notation allows for escaped \$ delimiters
// (requires browser supporting ECMA2018 standard -- currently only Chrome / FF)
// (see https://javascript.info/regexp-lookahead-lookbehind)
export var REGEX_INLINE_MATH_DOLLARS_ESCAPED = (function () {
// attempt to create regex with negative lookbehind
try {
return new RegExp("(?<!\\\\)\\$(.+)(?<!\\\\)\\$");
}
catch (e) {
return REGEX_INLINE_MATH_DOLLARS;
}
})();
// ---- Block Input Rules ------------------------------- //
// simple inputrule for block math
export var REGEX_BLOCK_MATH_DOLLARS = /\$\$\$\s+$/; //new RegExp("\$\$\s+$", "i");
////////////////////////////////////////////////////////////
export function makeInlineMathInputRule(pattern, nodeType, getAttrs) {
return new InputRule(pattern, function (state, match, start, end) {
var $start = state.doc.resolve(start);
var index = $start.index();
var $end = state.doc.resolve(end);
// get attrs
var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
// check if replacement valid
if (!$start.parent.canReplaceWith(index, $end.index(), nodeType)) {
return null;
}
// perform replacement
return state.tr.replaceRangeWith(start, end, nodeType.create(attrs, nodeType.schema.text(match[1])));
});
}
export function makeBlockMathInputRule(pattern, nodeType, getAttrs) {
return new InputRule(pattern, function (state, match, start, end) {
var $start = state.doc.resolve(start);
var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
if (!$start
.node(-1)
.canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType))
return null;
var tr = state.tr
.delete(start, end)
.setBlockType(start, start, nodeType, attrs);
return tr.setSelection(NodeSelection.create(tr.doc, tr.mapping.map($start.pos - 1)));
});
}

View File

@@ -0,0 +1,56 @@
/**
* Note that for some of the `ParseRule`s defined below,
* we define a `getAttrs` function, which, other than
* defining node attributes, can be used to describe complex
* match conditions for a rule.
* Returning `false` from `ParseRule.getAttrs` prevents the
* rule from matching, while returning `null` indicates that
* the default set of note attributes should be used.
*/
import { ParseRule } from "prosemirror-model";
/**
* Wikipedia formats block math inside a <dl>...</dl> element, as below.
*
* - Evidently no CSS class is used to distinguish inline vs block math
* - Sometimes the `\displaystyle` TeX command is present even in inline math
*
* ```html
* <dl><dd><span class="mwe-math-element">
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
* <semantics>
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
* <annotation encoding="application/x-tex">...</annotation>
* </semantics>
* </math>
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
* </span>
* </span></dd></dl>
* ```
*/
export declare const wikipediaBlockMathParseRule: ParseRule;
/**
* Parse rule for inline math content on Wikipedia of the following form:
*
* ```html
* <span class="mwe-math-element">
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
* <semantics>
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
* <annotation encoding="application/x-tex">...</annotation>
* </semantics>
* </math>
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
* </span>
* </span>
* ```
*/
export declare const wikipediaInlineMathParseRule: ParseRule;
export declare const defaultInlineMathParseRules: ParseRule[];
export declare const defaultBlockMathParseRules: ParseRule[];

View File

@@ -0,0 +1,193 @@
/**
* Note that for some of the `ParseRule`s defined below,
* we define a `getAttrs` function, which, other than
* defining node attributes, can be used to describe complex
* match conditions for a rule.
* Returning `false` from `ParseRule.getAttrs` prevents the
* rule from matching, while returning `null` indicates that
* the default set of note attributes should be used.
*/
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
import { Fragment, } from "prosemirror-model";
////////////////////////////////////////////////////////////
function getFirstMatch(root, rules) {
var e_1, _a;
try {
for (var rules_1 = __values(rules), rules_1_1 = rules_1.next(); !rules_1_1.done; rules_1_1 = rules_1.next()) {
var rule = rules_1_1.value;
var match = rule(root);
if (match !== false) {
return match;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (rules_1_1 && !rules_1_1.done && (_a = rules_1.return)) _a.call(rules_1);
}
finally { if (e_1) throw e_1.error; }
}
return false;
}
function makeTextFragment(text, schema) {
return Fragment.from(schema.text(text));
}
////////////////////////////////////////////////////////////
// -- Wikipedia ----------------------------------------- //
/**
* Look for a child node that matches the following template:
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
*/
function texFromMediaWikiFallbackImage(root) {
var _a;
var match = root.querySelector("img.mwe-math-fallback-image-inline[alt]");
return (_a = match === null || match === void 0 ? void 0 : match.getAttribute("alt")) !== null && _a !== void 0 ? _a : false;
}
/**
* Look for a child node that matches the following template:
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
*/
function texFromMathML_01(root) {
var _a;
var match = root.querySelector("math[alttext]");
return (_a = match === null || match === void 0 ? void 0 : match.getAttribute("alttext")) !== null && _a !== void 0 ? _a : false;
}
/**
* Look for a child node that matches the following template:
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
*/
function texFromMathML_02(root) {
var _a;
var match = root.querySelector("math annotation[encoding='application/x-tex'");
return (_a = match === null || match === void 0 ? void 0 : match.textContent) !== null && _a !== void 0 ? _a : false;
}
/**
* Look for a child node that matches the following template:
* <script type="math/tex"></script>
*/
function texFromScriptTag(root) {
var _a;
var match = root.querySelector("script[type*='math/tex']");
return (_a = match === null || match === void 0 ? void 0 : match.textContent) !== null && _a !== void 0 ? _a : false;
}
function matchWikipedia(root) {
var match = getFirstMatch(root, [
texFromMediaWikiFallbackImage,
texFromMathML_01,
texFromMathML_02,
]);
// TODO: if no tex string was found, but we have MathML, try to parse it
return match;
}
/**
* Wikipedia formats block math inside a <dl>...</dl> element, as below.
*
* - Evidently no CSS class is used to distinguish inline vs block math
* - Sometimes the `\displaystyle` TeX command is present even in inline math
*
* ```html
* <dl><dd><span class="mwe-math-element">
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
* <semantics>
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
* <annotation encoding="application/x-tex">...</annotation>
* </semantics>
* </math>
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
* </span>
* </span></dd></dl>
* ```
*/
export var wikipediaBlockMathParseRule = {
tag: "dl",
getAttrs: function (p) {
var dl = p;
// <dl> must contain exactly one child
if (dl.childElementCount !== 1) {
return false;
}
var dd = dl.firstChild;
if (dd.tagName !== "DD") {
return false;
}
// <dd> must contain exactly one child
if (dd.childElementCount !== 1) {
return false;
}
var mweElt = dd.firstChild;
if (!mweElt.classList.contains("mwe-math-element")) {
return false;
}
// success! proceed to `getContent` for further processing
return null;
},
getContent: function (p, schema) {
// search the matched element for a TeX string
var match = matchWikipedia(p);
// return a fragment representing the math node's children
var texString = match || "\\text{\\color{red}(paste error)}";
return makeTextFragment(texString, schema);
},
};
/**
* Parse rule for inline math content on Wikipedia of the following form:
*
* ```html
* <span class="mwe-math-element">
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
* <semantics>
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
* <annotation encoding="application/x-tex">...</annotation>
* </semantics>
* </math>
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
* </span>
* </span>
* ```
*/
export var wikipediaInlineMathParseRule = {
tag: "span",
getAttrs: function (p) {
var span = p;
if (!span.classList.contains("mwe-math-element")) {
return false;
}
// success! proceed to `getContent` for further processing
return null;
},
getContent: function (p, schema) {
// search the matched element for a TeX string
var match = matchWikipedia(p);
// return a fragment representing the math node's children
var texString = match || "\\text{\\color{red}(paste error)}";
return makeTextFragment(texString, schema);
},
};
// -- MathJax ------------------------------------------- //
////////////////////////////////////////////////////////////
export var defaultInlineMathParseRules = [
wikipediaInlineMathParseRule,
];
export var defaultBlockMathParseRules = [
wikipediaBlockMathParseRule,
];

View File

@@ -0,0 +1,12 @@
import { Plugin as ProsePlugin } from "prosemirror-state";
/**
* Due to the internals of KaTeX, by default, selecting rendered
* math will put a box around each individual character of a
* math expression. This plugin attempts to make math selections
* slightly prettier by instead setting a background color on the node.
*
* (remember to use the included math.css!)
*
* @todo (6/13/20) math selection rectangles are not quite even with text
*/
export declare const mathSelectPlugin: ProsePlugin;

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// prosemirror imports
import { Plugin as ProsePlugin, } from "prosemirror-state";
import { DecorationSet, Decoration } from "prosemirror-view";
////////////////////////////////////////////////////////////
/**
* Uses the selection to determine which math_select decorations
* should be applied to the given document.
* @param arg Should be either a Transaction or an EditorState,
* although any object with `selection` and `doc` will work.
*/
var checkSelection = function (arg) {
var _a = arg.selection, from = _a.from, to = _a.to;
var content = arg.selection.content().content;
var result = [];
content.descendants(function (node, pos, parent) {
if (node.type.name == "text") {
return false;
}
if (node.type.name.startsWith("math_")) {
result.push({
start: Math.max(from + pos - 1, 0),
end: from + pos + node.nodeSize - 1,
});
return false;
}
return true;
});
return DecorationSet.create(arg.doc, result.map(function (_a) {
var start = _a.start, end = _a.end;
return Decoration.node(start, end, { class: "math-select" });
}));
};
/**
* Due to the internals of KaTeX, by default, selecting rendered
* math will put a box around each individual character of a
* math expression. This plugin attempts to make math selections
* slightly prettier by instead setting a background color on the node.
*
* (remember to use the included math.css!)
*
* @todo (6/13/20) math selection rectangles are not quite even with text
*/
export var mathSelectPlugin = new ProsePlugin({
state: {
init: function (config, partialState) {
return checkSelection(partialState);
},
apply: function (tr, value, oldState, newState) {
if (!tr.selection || !tr.selectionSet) {
return oldState;
}
var sel = checkSelection(tr);
return sel;
},
},
props: {
decorations: function (state) {
return mathSelectPlugin.getState(state);
},
},
});

View File

@@ -0,0 +1,3 @@
import { MathRenderer } from "./types";
import "katex/contrib/mhchem/mhchem";
export declare const KatexRenderer: MathRenderer;

View File

@@ -0,0 +1,19 @@
import katex from "katex";
// Chemistry formulas support
import "katex/contrib/mhchem/mhchem";
export var KatexRenderer = {
inline: function (text, element) {
katex.render(text, element, {
displayMode: false,
globalGroup: true,
throwOnError: false,
});
},
block: function (text, element) {
katex.render(text, element, {
displayMode: true,
globalGroup: true,
throwOnError: false,
});
},
};

View File

@@ -0,0 +1,5 @@
export declare type MathRenderFn = (text: string, element: HTMLElement) => void;
export declare type MathRenderer = {
inline: MathRenderFn;
block: MathRenderFn;
};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,34 @@
import { Node as ProseNode, Mark, Slice, NodeType, MarkType, Fragment } from "prosemirror-model";
declare type TypedNode<T extends string> = ProseNode & {
type: NodeType & {
name: T;
};
};
declare type TypedMark<T extends string> = Mark & {
type: MarkType & {
name: T;
};
};
declare type NodeSerializer<T extends string> = (node: TypedNode<T>) => string;
declare type MarkSerializer<T extends string> = (mark: TypedMark<T>) => string;
declare class ProseMirrorTextSerializer {
nodes: {
[name: string]: NodeSerializer<string> | undefined;
};
marks: {
[name: string]: MarkSerializer<string> | undefined;
};
constructor(fns: {
nodes?: {
[name: string]: NodeSerializer<string> | undefined;
};
marks?: {
[name: string]: MarkSerializer<string> | undefined;
};
}, base?: ProseMirrorTextSerializer);
serializeFragment(fragment: Fragment): string;
serializeSlice(slice: Slice): string;
serializeNode(node: ProseNode): string | null;
}
export declare const mathSerializer: ProseMirrorTextSerializer;
export {};

View File

@@ -0,0 +1,71 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var ProseMirrorTextSerializer = /** @class */ (function () {
function ProseMirrorTextSerializer(fns, base) {
// use base serializer as a fallback
this.nodes = __assign(__assign({}, base === null || base === void 0 ? void 0 : base.nodes), fns.nodes);
this.marks = __assign(__assign({}, base === null || base === void 0 ? void 0 : base.marks), fns.marks);
}
ProseMirrorTextSerializer.prototype.serializeFragment = function (fragment) {
var _this = this;
// adapted from the undocumented `Fragment.textBetween` function
// https://github.com/ProseMirror/prosemirror-model/blob/eef20c8c6dbf841b1d70859df5d59c21b5108a4f/src/fragment.js#L46
var blockSeparator = "\n\n";
var leafText = undefined;
var text = "";
var separated = true;
var from = 0;
var to = fragment.size;
fragment.nodesBetween(from, to, function (node, pos) {
var _a;
// check if one of our custom serializers handles this node
var serialized = _this.serializeNode(node);
if (serialized !== null) {
text += serialized;
return false;
}
if (node.isText) {
text += ((_a = node.text) === null || _a === void 0 ? void 0 : _a.slice(Math.max(from, pos) - pos, to - pos)) || "";
separated = !blockSeparator;
}
else if (node.isLeaf && leafText) {
text += leafText;
separated = !blockSeparator;
}
else if (!separated && node.isBlock) {
text += blockSeparator;
separated = true;
}
}, 0);
return text;
};
ProseMirrorTextSerializer.prototype.serializeSlice = function (slice) {
return this.serializeFragment(slice.content);
};
ProseMirrorTextSerializer.prototype.serializeNode = function (node) {
// check if one of our custom serializers handles this node
var nodeSerializer = this.nodes[node.type.name];
if (nodeSerializer !== undefined) {
return nodeSerializer(node);
}
else {
return null;
}
};
return ProseMirrorTextSerializer;
}());
export var mathSerializer = new ProseMirrorTextSerializer({
nodes: {
math_inline: function (node) { return "$".concat(node.textContent, "$"); },
math_display: function (node) { return "\n\n$$\n".concat(node.textContent, "\n$$"); },
},
});

View File

@@ -0,0 +1,5 @@
import { Schema, SchemaSpec } from "prosemirror-model";
export declare type SchemaSpecNodeT<Spec> = Spec extends SchemaSpec<infer N, infer _> ? N : never;
export declare type SchemaSpecMarkT<Spec> = Spec extends SchemaSpec<infer _, infer M> ? M : never;
export declare type SchemaNodeT<S> = S extends Schema<infer N, infer _> ? N : never;
export declare type SchemaMarkT<S> = S extends Schema<infer _, infer M> ? M : never;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -53,6 +53,8 @@ import { EmbedNode } from "./extensions/embed";
import { CodeBlock } from "./extensions/code-block"; import { CodeBlock } from "./extensions/code-block";
import { ListItem } from "./extensions/list-item"; import { ListItem } from "./extensions/list-item";
import { Link } from "@tiptap/extension-link"; import { Link } from "@tiptap/extension-link";
import { Codemark } from "./extensions/code-mark";
import { MathInline, MathBlock } from "./extensions/math";
import { NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react"; import { NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react";
import { OutlineList } from "./extensions/outline-list"; import { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item"; import { OutlineListItem } from "./extensions/outline-list-item";
@@ -130,6 +132,9 @@ var useTiptap = function (options, deps) {
OutlineListItem, OutlineListItem,
OutlineList, OutlineList,
ListItem, ListItem,
Codemark,
MathInline,
MathBlock,
], ],
onBeforeCreate: function (_a) { onBeforeCreate: function (_a) {
var editor = _a.editor; var editor = _a.editor;

View File

@@ -14,8 +14,8 @@ export declare type ToolButtonProps = ButtonProps & {
}; };
export declare const ToolButton: React.NamedExoticComponent<ButtonProps & { export declare const ToolButton: React.NamedExoticComponent<ButtonProps & {
icon: IconNames; icon: IconNames;
iconColor?: "background" | "border" | "text" | "blue" | "gray" | "green" | "orange" | "purple" | "red" | "yellow" | "checked" | "disabled" | "placeholder" | "icon" | "overlay" | "hover" | keyof import("@notesnook/theme/dist/theme/colorscheme/static").StaticColors | "primary" | "bgTransparent" | "accent" | "bgSecondary" | "bgSecondaryText" | "fontSecondary" | "fontTertiary" | "secondary" | undefined; iconColor?: "background" | "border" | "text" | "blue" | "gray" | "green" | "orange" | "purple" | "red" | "yellow" | "checked" | "disabled" | "placeholder" | "icon" | keyof import("@notesnook/theme/dist/theme/colorscheme/static").StaticColors | "primary" | "bgTransparent" | "accent" | "bgSecondary" | "bgSecondaryText" | "hover" | "fontSecondary" | "fontTertiary" | "overlay" | "secondary" | undefined;
iconSize?: number | "small" | "big" | "medium" | undefined; iconSize?: number | "big" | "small" | "medium" | undefined;
toggled: boolean; toggled: boolean;
buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined> | undefined; buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined> | undefined;
variant?: ToolButtonVariant | undefined; variant?: ToolButtonVariant | undefined;

View File

@@ -1,4 +1,3 @@
/// <reference types="react" />
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { MenuButton } from "../../components/menu/types"; import { MenuButton } from "../../components/menu/types";
import { ToolProps } from "../types"; import { ToolProps } from "../types";

View File

@@ -37,8 +37,10 @@
"@tiptap/starter-kit": "^2.0.0-beta.185", "@tiptap/starter-kit": "^2.0.0-beta.185",
"detect-indent": "^7.0.0", "detect-indent": "^7.0.0",
"emotion-theming": "^10.0.19", "emotion-theming": "^10.0.19",
"katex": "^0.13.24",
"lowlight": "^2.6.1", "lowlight": "^2.6.1",
"prism-themes": "^1.9.0", "prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.0",
"prosemirror-tables": "^1.1.1", "prosemirror-tables": "^1.1.1",
"prosemirror-utils": "github:atlassian/prosemirror-utils", "prosemirror-utils": "github:atlassian/prosemirror-utils",
"prosemirror-view": "^1.24.1", "prosemirror-view": "^1.24.1",
@@ -57,6 +59,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/esm": "^3.2.0", "@types/esm": "^3.2.0",
"@types/katex": "^0.14.0",
"@types/node": "^16.11.11", "@types/node": "^16.11.11",
"@types/prismjs": "^1.26.0", "@types/prismjs": "^1.26.0",
"@types/react": "^17.0.37", "@types/react": "^17.0.37",
@@ -4564,6 +4567,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"node_modules/@types/katex": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz",
"integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==",
"dev": true
},
"node_modules/@types/minimatch": { "node_modules/@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -15131,6 +15140,29 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/katex": {
"version": "0.13.24",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.13.24.tgz",
"integrity": "sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"dependencies": {
"commander": "^8.0.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"engines": {
"node": ">= 12"
}
},
"node_modules/killable": { "node_modules/killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -16567,9 +16599,9 @@
} }
}, },
"node_modules/orderedmap": { "node_modules/orderedmap": {
"version": "1.1.7", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.7.tgz", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.0.0.tgz",
"integrity": "sha512-B1SuadDDwIRXXutaJQ1xjreGL3hxujpexBG4PquoXbgJD8bjp2k8b8qI/mk7q0LUdIx7T8IALWB8mPbfsjbGCw==" "integrity": "sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ=="
}, },
"node_modules/os-browserify": { "node_modules/os-browserify": {
"version": "0.3.0", "version": "0.3.0",
@@ -18709,6 +18741,17 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/prosemirror-codemark": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/prosemirror-codemark/-/prosemirror-codemark-0.4.0.tgz",
"integrity": "sha512-bl0UMClJHr7fiWq8LjK5jcvVqX0t4HLLUGhxu5pAbhBwPU8qKLaVUEdZvo4ioMExF5q8WICnqiUaVv5N9TH2uw==",
"peerDependencies": {
"prosemirror-inputrules": "^1.2.0",
"prosemirror-model": "^1.18.1",
"prosemirror-state": "^1.4.1",
"prosemirror-view": "^1.26.2"
}
},
"node_modules/prosemirror-commands": { "node_modules/prosemirror-commands": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz",
@@ -18750,6 +18793,16 @@
"rope-sequence": "^1.3.0" "rope-sequence": "^1.3.0"
} }
}, },
"node_modules/prosemirror-inputrules": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz",
"integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==",
"peer": true,
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-keymap": { "node_modules/prosemirror-keymap": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz",
@@ -18760,11 +18813,11 @@
} }
}, },
"node_modules/prosemirror-model": { "node_modules/prosemirror-model": {
"version": "1.17.0", "version": "1.18.1",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.17.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.18.1.tgz",
"integrity": "sha512-RJBDgZs/W26yyx1itrk5b3H9FxIro3K7Xjc2QWJI99Gu1nxYAnIggqI3fIOD8Jd/6QZfM+t6elZFJPycVexMTA==", "integrity": "sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==",
"dependencies": { "dependencies": {
"orderedmap": "^1.1.0" "orderedmap": "^2.0.0"
} }
}, },
"node_modules/prosemirror-schema-list": { "node_modules/prosemirror-schema-list": {
@@ -18778,9 +18831,9 @@
} }
}, },
"node_modules/prosemirror-state": { "node_modules/prosemirror-state": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.1.tgz",
"integrity": "sha512-mVDZdjNX/YT5FvypiwbphJe9psA5h+j9apsSszVRFc6oKFoIInvzdujh8QW9f9lwHtSYajLxNiM1hPhd0Sl1XA==", "integrity": "sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==",
"dependencies": { "dependencies": {
"prosemirror-model": "^1.0.0", "prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0" "prosemirror-transform": "^1.0.0"
@@ -28787,6 +28840,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"@types/katex": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz",
"integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==",
"dev": true
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -37049,6 +37108,21 @@
"object.assign": "^4.1.2" "object.assign": "^4.1.2"
} }
}, },
"katex": {
"version": "0.13.24",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.13.24.tgz",
"integrity": "sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==",
"requires": {
"commander": "^8.0.0"
},
"dependencies": {
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
}
}
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -38203,9 +38277,9 @@
} }
}, },
"orderedmap": { "orderedmap": {
"version": "1.1.7", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.7.tgz", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.0.0.tgz",
"integrity": "sha512-B1SuadDDwIRXXutaJQ1xjreGL3hxujpexBG4PquoXbgJD8bjp2k8b8qI/mk7q0LUdIx7T8IALWB8mPbfsjbGCw==" "integrity": "sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ=="
}, },
"os-browserify": { "os-browserify": {
"version": "0.3.0", "version": "0.3.0",
@@ -39964,6 +40038,12 @@
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
"integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==" "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
}, },
"prosemirror-codemark": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/prosemirror-codemark/-/prosemirror-codemark-0.4.0.tgz",
"integrity": "sha512-bl0UMClJHr7fiWq8LjK5jcvVqX0t4HLLUGhxu5pAbhBwPU8qKLaVUEdZvo4ioMExF5q8WICnqiUaVv5N9TH2uw==",
"requires": {}
},
"prosemirror-commands": { "prosemirror-commands": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz",
@@ -40005,6 +40085,16 @@
"rope-sequence": "^1.3.0" "rope-sequence": "^1.3.0"
} }
}, },
"prosemirror-inputrules": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz",
"integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==",
"peer": true,
"requires": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-keymap": { "prosemirror-keymap": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz",
@@ -40015,11 +40105,11 @@
} }
}, },
"prosemirror-model": { "prosemirror-model": {
"version": "1.17.0", "version": "1.18.1",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.17.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.18.1.tgz",
"integrity": "sha512-RJBDgZs/W26yyx1itrk5b3H9FxIro3K7Xjc2QWJI99Gu1nxYAnIggqI3fIOD8Jd/6QZfM+t6elZFJPycVexMTA==", "integrity": "sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==",
"requires": { "requires": {
"orderedmap": "^1.1.0" "orderedmap": "^2.0.0"
} }
}, },
"prosemirror-schema-list": { "prosemirror-schema-list": {
@@ -40033,9 +40123,9 @@
} }
}, },
"prosemirror-state": { "prosemirror-state": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.1.tgz",
"integrity": "sha512-mVDZdjNX/YT5FvypiwbphJe9psA5h+j9apsSszVRFc6oKFoIInvzdujh8QW9f9lwHtSYajLxNiM1hPhd0Sl1XA==", "integrity": "sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==",
"requires": { "requires": {
"prosemirror-model": "^1.0.0", "prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0" "prosemirror-transform": "^1.0.0"

View File

@@ -33,8 +33,10 @@
"@tiptap/starter-kit": "^2.0.0-beta.185", "@tiptap/starter-kit": "^2.0.0-beta.185",
"detect-indent": "^7.0.0", "detect-indent": "^7.0.0",
"emotion-theming": "^10.0.19", "emotion-theming": "^10.0.19",
"katex": "^0.13.24",
"lowlight": "^2.6.1", "lowlight": "^2.6.1",
"prism-themes": "^1.9.0", "prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.0",
"prosemirror-tables": "^1.1.1", "prosemirror-tables": "^1.1.1",
"prosemirror-utils": "github:atlassian/prosemirror-utils", "prosemirror-utils": "github:atlassian/prosemirror-utils",
"prosemirror-view": "^1.24.1", "prosemirror-view": "^1.24.1",
@@ -53,6 +55,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/esm": "^3.2.0", "@types/esm": "^3.2.0",
"@types/katex": "^0.14.0",
"@types/node": "^16.11.11", "@types/node": "^16.11.11",
"@types/prismjs": "^1.26.0", "@types/prismjs": "^1.26.0",
"@types/react": "^17.0.37", "@types/react": "^17.0.37",

View File

@@ -0,0 +1,10 @@
import { Extension } from "@tiptap/core";
import codemark from "prosemirror-codemark";
// import "prosemirror-codemark/dist/codemark.css";
export const Codemark = Extension.create({
name: "codemarkPlugin",
addProseMirrorPlugins() {
return codemark({ markType: this.editor.schema.marks.code });
},
});

View File

@@ -0,0 +1,2 @@
export * from "./code-mark";
export { Codemark as default } from "./code-mark";

View File

@@ -0,0 +1,2 @@
export { MathInline } from "./math-inline";
export { MathBlock } from "./math-block";

View File

@@ -0,0 +1,39 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { inputRules } from "prosemirror-inputrules";
import {
mathPlugin,
makeBlockMathInputRule,
REGEX_BLOCK_MATH_DOLLARS,
} from "./plugin";
export const MathBlock = Node.create({
name: "math_display",
group: "block math",
content: "text*", // important!
atom: true, // important!
code: true,
parseHTML() {
return [
{
tag: `div[class*='math-display']`, // important!
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes({ class: "math-display math-node" }, HTMLAttributes),
0,
];
},
addProseMirrorPlugins() {
const inputRulePlugin = inputRules({
rules: [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, this.type)],
});
return [inputRulePlugin];
},
});

View File

@@ -0,0 +1,42 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { inputRules } from "prosemirror-inputrules";
import {
makeInlineMathInputRule,
REGEX_INLINE_MATH_DOLLARS,
mathPlugin,
} from "./plugin";
import "katex/dist/katex.min.css";
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: "span[class*='math-inline']", // important!,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"span",
mergeAttributes({ class: "math-inline math-node" }, HTMLAttributes),
0,
];
},
addProseMirrorPlugins() {
const inputRulePlugin = inputRules({
rules: [makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)],
});
return [mathPlugin, inputRulePlugin];
},
});

View File

@@ -0,0 +1,72 @@
import { Command } from "prosemirror-state";
import { EditorState, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
/**
* A ProseMirror command for determining whether to exit a math block, based on
* specific conditions. Normally called when the user has
*
* @param outerView The main ProseMirror EditorView containing this math node.
* @param dir Used to indicate desired cursor position upon closing a math node.
* When set to -1, cursor will be placed BEFORE the math node.
* When set to +1, cursor will be placed AFTER the math node.
* @param borderMode An exit condition based on cursor position and direction.
* @param requireEmptySelection When TRUE, only exit the math node when the
* (inner) selection is empty.
* @returns A new ProseMirror command based on the input configuration.
*/
export function collapseMathCmd(
outerView: EditorView,
dir: 1 | -1,
requireOnBorder: boolean,
requireEmptySelection: boolean = true
): Command {
// create a new ProseMirror command based on the input conditions
return (
innerState: EditorState,
dispatch: ((tr: Transaction) => void) | undefined
) => {
// get selection info
let outerState: EditorState = outerView.state;
let { to: outerTo, from: outerFrom } = outerState.selection;
let { to: innerTo, from: innerFrom } = innerState.selection;
// only exit math node when selection is empty
if (requireEmptySelection && innerTo !== innerFrom) {
return false;
}
let currentPos: number = dir > 0 ? innerTo : innerFrom;
// when requireOnBorder is TRUE, collapse only when cursor
// is about to leave the bounds of the math node
if (requireOnBorder) {
// (subtract two from nodeSize to account for start and end tokens)
let nodeSize = innerState.doc.nodeSize - 2;
// early return if exit conditions not met
if (dir > 0 && currentPos < nodeSize) {
return false;
}
if (dir < 0 && currentPos > 0) {
return false;
}
}
// all exit conditions met, so close the math node by moving the cursor outside
if (dispatch) {
// set outer selection to be outside of the nodeview
let targetPos: number = dir > 0 ? outerTo : outerFrom;
outerView.dispatch(
outerState.tr.setSelection(
TextSelection.create(outerState.doc, targetPos)
)
);
// must return focus to the outer view, otherwise no cursor will appear
outerView.focus();
}
return true;
};
}

View File

@@ -0,0 +1,40 @@
import { Command } from "prosemirror-state";
import { NodeType } from "prosemirror-model";
import { EditorState, NodeSelection, Transaction } from "prosemirror-state";
////////////////////////////////////////////////////////////////////////////////
/**
* Returns a new command that can be used to inserts a new math node at the
* user's current document position, provided that the document schema actually
* allows a math node to be placed there.
*
* @param mathNodeType An instance for either your math_inline or math_display
* NodeType. Must belong to the same schema that your EditorState uses!
* @param initialText (optional) The initial source content for the math editor.
*/
export function insertMathCmd(
mathNodeType: NodeType,
initialText = ""
): Command {
return function (
state: EditorState,
dispatch: ((tr: Transaction) => void) | undefined
) {
let { $from } = state.selection,
index = $from.index();
if (!$from.parent.canReplaceWith(index, index, mathNodeType)) {
return false;
}
if (dispatch) {
let mathNode = mathNodeType.create(
{},
initialText ? state.schema.text(initialText) : null
);
let tr = state.tr.replaceSelectionWith(mathNode);
tr = tr.setSelection(NodeSelection.create(tr.doc, $from.pos));
dispatch(tr);
}
return true;
};
}

View File

@@ -0,0 +1,41 @@
import { Command } from "prosemirror-state";
import { EditorState, TextSelection, Transaction } from "prosemirror-state";
////////////////////////////////////////////////////////////////////////////////
/**
* Some browsers (cough firefox cough) don't properly handle cursor movement on
* the edges of a NodeView, so we need to make the desired behavior explicit.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1252108
*/
export function nudgeCursorCmd(dir: -1 | 0 | 1): Command {
return (
innerState: EditorState,
dispatch: ((tr: Transaction) => void) | undefined
) => {
let { to, from } = innerState.selection;
// compute target position
let emptySelection: boolean = to === from;
let currentPos: number = dir < 0 ? from : to;
let increment: number = emptySelection ? dir : 0;
let nodeSize: number = innerState.doc.nodeSize;
let targetPos: number = Math.max(
0,
Math.min(nodeSize, currentPos + increment)
);
if (dispatch) {
dispatch(
innerState.tr.setSelection(
TextSelection.create(innerState.doc, targetPos)
)
);
}
return true;
};
}
export const nudgeCursorForwardCmd: Command = nudgeCursorCmd(+1);
export const nudgeCursorBackCmd: Command = nudgeCursorCmd(-1);

View File

@@ -0,0 +1,24 @@
// (https://stackoverflow.com/a/53098695/1444650)
// import needed to make this a module
import { Fragment, Node as ProseNode } from "prosemirror-model";
import { EditorState, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
declare module "prosemirror-model" {
interface Fragment {
// as of (3/31/20) official @types/prosemirror-model
// was missing Fragment.content, so we define it here
content: Node[];
}
interface NodeType {
hasRequiredAttrs(): boolean;
createAndFill(attrs?:Object, content?: Fragment|ProseNode|ProseNode[], marks?:Mark[]): ProseNode;
}
interface ResolvedPos {
// missing declaration as of (7/25/20)
/** Get the position at the given index in the parent node at the given depth (which defaults to this.depth). */
posAtIndex(index:number, depth?:number):number;
}
}

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// core functionality
export { MathView, type ICursorPosObserver } from "./math-node-view";
export {
mathPlugin,
createMathView,
type IMathPluginState,
} from "./math-plugin";
export { mathSchemaSpec, createMathSchema } from "./math-schema";
// recommended plugins
export { mathBackspaceCmd } from "./plugins/math-backspace";
export {
makeBlockMathInputRule,
makeInlineMathInputRule,
REGEX_BLOCK_MATH_DOLLARS,
REGEX_INLINE_MATH_DOLLARS,
REGEX_INLINE_MATH_DOLLARS_ESCAPED,
} from "./plugins/math-input-rules";
// optional / experimental plugins
export { mathSelectPlugin } from "./plugins/math-select";
// commands
export { insertMathCmd } from "./commands/insert-math-cmd";
// utilities
export { mathSerializer } from "./utils/text-serializer";
export * from "./utils/types";

View File

@@ -0,0 +1,401 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// prosemirror imports
import { Node as ProseNode } from "prosemirror-model";
import {
EditorState,
Transaction,
TextSelection,
PluginKey,
} from "prosemirror-state";
import {
NodeView,
EditorView,
Decoration,
DecorationSource,
} from "prosemirror-view";
import { StepMap } from "prosemirror-transform";
import { keymap } from "prosemirror-keymap";
import {
newlineInCode,
chainCommands,
deleteSelection,
} from "prosemirror-commands";
import { collapseMathCmd } from "./commands/collapse-math-cmd";
import { IMathPluginState } from "./math-plugin";
import { MathRenderer, MathRenderFn } from "./renderers/types";
//// INLINE MATH NODEVIEW //////////////////////////////////
export interface ICursorPosObserver {
/** indicates on which side cursor should appear when this node is selected */
cursorSide: "start" | "end";
/** */
updateCursorPos(state: EditorState): void;
}
interface IMathViewOptions {
/** Dom element name to use for this NodeView */
tagName?: string;
/** Used to render the Tex input */
renderer: MathRenderFn;
/** Should be true if node is inline */
inline?: boolean;
}
export class MathView implements NodeView, ICursorPosObserver {
// nodeview params
private _node: ProseNode;
private _outerView: EditorView;
private _getPos: () => number;
// nodeview dom
dom: HTMLElement;
private _mathRenderElt: HTMLElement | undefined;
private _mathSrcElt: HTMLElement | undefined;
private _innerView: EditorView | undefined;
// internal state
cursorSide: "start" | "end";
private _tagName: string;
private _isEditing: boolean;
private _mathPluginKey: PluginKey<IMathPluginState>;
private options: IMathViewOptions;
// == Lifecycle ===================================== //
/**
* @param onDestroy Callback for when this NodeView is destroyed.
* This NodeView should unregister itself from the list of ICursorPosObservers.
*
* Math Views support the following options:
* @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
* @option tagName HTML tag name to use for this NodeView. If none is provided,
* will use the node name with underscores converted to hyphens.
*/
constructor(
node: ProseNode,
view: EditorView,
getPos: () => number,
options: IMathViewOptions,
mathPluginKey: PluginKey<IMathPluginState>,
onDestroy?: () => void
) {
// store arguments
this.options = options;
this._node = node;
this._outerView = view;
this._getPos = getPos;
this._mathPluginKey = mathPluginKey;
// editing state
this.cursorSide = "start";
this._isEditing = false;
// options
this._tagName = options.tagName || this._node.type.name.replace("_", "-");
// create dom representation of nodeview
this.dom = document.createElement(this._tagName);
if (options.inline) this.dom.classList.add("math-inline");
else this.dom.classList.add("math-display");
this.dom.classList.add("math-node");
this._mathRenderElt = document.createElement("span");
this._mathRenderElt.textContent = "";
this._mathRenderElt.classList.add("math-render");
this.dom.appendChild(this._mathRenderElt);
this._mathSrcElt = document.createElement("span");
this._mathSrcElt.classList.add("math-src");
this.dom.appendChild(this._mathSrcElt);
// ensure
this.dom.addEventListener("click", () => this.ensureFocus());
// render initial content
this.renderMath();
}
destroy() {
// close the inner editor without rendering
this.closeEditor(false);
// clean up dom elements
if (this._mathRenderElt) {
this._mathRenderElt.remove();
delete this._mathRenderElt;
}
if (this._mathSrcElt) {
this._mathSrcElt.remove();
delete this._mathSrcElt;
}
this.dom.remove();
}
/**
* Ensure focus on the inner editor whenever this node has focus.
* This helps to prevent accidental deletions of math blocks.
*/
ensureFocus() {
if (this._innerView && this._outerView.hasFocus()) {
this._innerView.focus();
}
}
// == Updates ======================================= //
update(
node: ProseNode,
_decorations: readonly Decoration[],
_innerDecorations: DecorationSource
) {
if (!node.sameMarkup(this._node)) return false;
this._node = node;
if (this._innerView) {
let state = this._innerView.state;
let start = node.content.findDiffStart(state.doc.content);
if (start != null) {
let diff = node.content.findDiffEnd(state.doc.content as any);
if (diff) {
let { a: endA, b: endB } = diff;
let overlap = start - Math.min(endA, endB);
if (overlap > 0) {
endA += overlap;
endB += overlap;
}
this._innerView.dispatch(
state.tr
.replace(start, endB, node.slice(start, endA))
.setMeta("fromOutside", true)
);
}
}
}
if (!this._isEditing) {
this.renderMath();
}
return true;
}
updateCursorPos(state: EditorState): void {
const pos = this._getPos();
const size = this._node.nodeSize;
const inPmSelection =
state.selection.from < pos + size && pos < state.selection.to;
if (!inPmSelection) {
this.cursorSide = pos < state.selection.from ? "end" : "start";
}
}
// == Events ===================================== //
selectNode() {
if (!this._outerView.editable) {
return;
}
this.dom.classList.add("ProseMirror-selectednode");
if (!this._isEditing) {
this.openEditor();
}
}
deselectNode() {
this.dom.classList.remove("ProseMirror-selectednode");
if (this._isEditing) {
this.closeEditor();
}
}
stopEvent(event: Event): boolean {
return (
this._innerView !== undefined &&
event.target !== undefined &&
this._innerView.dom.contains(event.target as Node)
);
}
ignoreMutation() {
return true;
}
// == Rendering ===================================== //
renderMath() {
if (!this._mathRenderElt) {
return;
}
// get tex string to render
let content = this._node.content.content;
let texString = "";
if (content.length > 0 && content[0].textContent !== null) {
texString = content[0].textContent.trim();
}
// empty math?
if (texString.length < 1) {
this.dom.classList.add("empty-math");
// clear rendered math, since this node is in an invalid state
while (this._mathRenderElt.firstChild) {
this._mathRenderElt.firstChild.remove();
}
// do not render empty math
return;
} else {
this.dom.classList.remove("empty-math");
}
// render katex, but fail gracefully
try {
this.options.renderer(texString, this._mathRenderElt);
this._mathRenderElt.classList.remove("parse-error");
this.dom.setAttribute("title", "");
} catch (err) {
if (err instanceof Error) {
console.error(err);
this._mathRenderElt.classList.add("parse-error");
this.dom.setAttribute("title", err.toString());
}
}
}
// == Inner Editor ================================== //
dispatchInner(tr: Transaction) {
if (!this._innerView) {
return;
}
let { state, transactions } = this._innerView.state.applyTransaction(tr);
this._innerView.updateState(state);
if (!tr.getMeta("fromOutside")) {
let outerTr = this._outerView.state.tr,
offsetMap = StepMap.offset(this._getPos() + 1);
for (let i = 0; i < transactions.length; i++) {
let steps = transactions[i].steps;
for (let j = 0; j < steps.length; j++) {
let mapped = steps[j].map(offsetMap);
if (!mapped) {
throw Error("step discarded!");
}
outerTr.step(mapped);
}
}
if (outerTr.docChanged) this._outerView.dispatch(outerTr);
}
}
openEditor() {
if (this._innerView) {
throw Error("inner view should not exist!");
}
if (!this._mathSrcElt) throw new Error("_mathSrcElt does not exist!");
// create a nested ProseMirror view
this._innerView = new EditorView(this._mathSrcElt, {
state: EditorState.create({
doc: this._node,
plugins: [
keymap({
Tab: (state, dispatch) => {
if (dispatch) {
dispatch(state.tr.insertText("\t"));
}
return true;
},
Backspace: chainCommands(
deleteSelection,
(state, dispatch, tr_inner) => {
// default backspace behavior for non-empty selections
if (!state.selection.empty) {
return false;
}
// default backspace behavior when math node is non-empty
if (this._node.textContent.length > 0) {
return false;
}
// otherwise, we want to delete the empty math node and focus the outer view
this._outerView.dispatch(
this._outerView.state.tr.insertText("")
);
this._outerView.focus();
return true;
}
),
// "Ctrl-Backspace": (state, dispatch, tr_inner) => {
// // delete math node and focus the outer view
// this._outerView.dispatch(this._outerView.state.tr.insertText(""));
// this._outerView.focus();
// return true;
// },
Enter: chainCommands(
newlineInCode,
collapseMathCmd(this._outerView, +1, false)
),
"Ctrl-Enter": collapseMathCmd(this._outerView, +1, false),
ArrowLeft: collapseMathCmd(this._outerView, -1, true),
ArrowRight: collapseMathCmd(this._outerView, +1, true),
ArrowUp: collapseMathCmd(this._outerView, -1, true),
ArrowDown: collapseMathCmd(this._outerView, +1, true),
}),
],
}),
dispatchTransaction: this.dispatchInner.bind(this),
});
// focus element
let innerState = this._innerView.state;
this._innerView.focus();
// request outer cursor position before math node was selected
let maybePos = this._mathPluginKey.getState(
this._outerView.state
)?.prevCursorPos;
if (maybePos === null || maybePos === undefined) {
console.error(
"[prosemirror-math] Error: Unable to fetch math plugin state from key."
);
}
let prevCursorPos: number = maybePos ?? 0;
// compute position that cursor should appear within the expanded math node
let innerPos =
prevCursorPos <= this._getPos() ? 0 : this._node.nodeSize - 2;
this._innerView.dispatch(
innerState.tr.setSelection(TextSelection.create(innerState.doc, innerPos))
);
this._isEditing = true;
}
/**
* Called when the inner ProseMirror editor should close.
*
* @param render Optionally update the rendered math after closing. (which
* is generally what we want to do, since the user is done editing!)
*/
closeEditor(render: boolean = true) {
if (this._innerView) {
this._innerView.destroy();
this._innerView = undefined;
}
if (render) {
this.renderMath();
}
this._isEditing = false;
}
}

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// prosemirror imports
import { Schema, Node as ProseNode } from "prosemirror-model";
import {
Plugin as ProsePlugin,
PluginKey,
PluginSpec,
} from "prosemirror-state";
import { MathView } from "./math-node-view";
import { EditorView } from "prosemirror-view";
import { KatexRenderer } from "./renderers/katex";
////////////////////////////////////////////////////////////
export interface IMathPluginState {
macros: { [cmd: string]: string };
/** A list of currently active `NodeView`s, in insertion order. */
activeNodeViews: MathView[];
/**
* Used to determine whether to place the cursor in the front- or back-most
* position when expanding a math node, without overriding the default arrow
* key behavior.
*/
prevCursorPos: number;
}
// uniquely identifies the prosemirror-math plugin
const MATH_PLUGIN_KEY = new PluginKey<IMathPluginState>("prosemirror-math");
/**
* Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
* @param inline TRUE for block math, FALSE for inline math.
* @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
*/
export function createMathView(inline: boolean) {
return (
node: ProseNode,
view: EditorView,
getPos: boolean | (() => number)
): MathView => {
/** @todo is this necessary?
* Docs says that for any function proprs, the current plugin instance
* will be bound to `this`. However, the typings don't reflect this.
*/
let pluginState = MATH_PLUGIN_KEY.getState(view.state);
if (!pluginState) {
throw new Error("no math plugin!");
}
let nodeViews = pluginState.activeNodeViews;
// set up NodeView
let nodeView = new MathView(
node,
view,
getPos as () => number,
{
inline,
renderer: inline ? KatexRenderer.inline : KatexRenderer.block,
tagName: inline ? "span" : "div",
},
MATH_PLUGIN_KEY,
() => {
nodeViews.splice(nodeViews.indexOf(nodeView));
}
);
nodeViews.push(nodeView);
return nodeView;
};
}
let mathPluginSpec: PluginSpec<IMathPluginState> = {
key: MATH_PLUGIN_KEY,
state: {
init(config, instance) {
return {
macros: {},
activeNodeViews: [],
prevCursorPos: 0,
};
},
apply(tr, value, oldState, newState) {
// produce updated state field for this plugin
const newPos = newState.selection.from;
const oldPos = oldState.selection.from;
return {
// these values are left unchanged
activeNodeViews: value.activeNodeViews,
macros: value.macros,
// update with the second-most recent cursor pos
prevCursorPos: oldPos !== newPos ? oldPos : value.prevCursorPos,
};
},
/** @todo (8/21/20) implement serialization for math plugin */
// toJSON(value) { },
// fromJSON(config, value, state){ return {}; }
},
props: {
nodeViews: {
math_inline: createMathView(true),
math_display: createMathView(false),
},
},
};
export const mathPlugin = new ProsePlugin(mathPluginSpec);

View File

@@ -0,0 +1,126 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// prosemirror imports
import {
Node as ProseNode,
Fragment,
MarkSpec,
NodeSpec,
Schema,
SchemaSpec,
NodeType,
} from "prosemirror-model";
import {
defaultBlockMathParseRules,
defaultInlineMathParseRules,
} from "./plugins/math-paste-rules";
import { SchemaSpecMarkT, SchemaSpecNodeT } from "./utils/types";
////////////////////////////////////////////////////////////
/**
* Borrowed from ProseMirror typings, modified to exclude OrderedMaps in spec,
* in order to help with the schema-building functions below.
*
* NOTE: TypeScript's typings for the spread operator { ...a, ...b } are only
* an approximation to the true type, and have difficulty with optional fields.
* So, unlike the SchemaSpec type, the `marks` field is NOT optional here.
*
* function example<T extends string>(x: { [name in T]: string; } | null) {
* const s = { ...x }; // inferred to have type `{}`.
* }
*
* @see https://github.com/microsoft/TypeScript/issues/10727
*/
interface SchemaSpecJson<N extends string = any, M extends string = any>
extends SchemaSpec<N, M> {
nodes: { [name in N]: NodeSpec };
marks: { [name in M]: MarkSpec };
topNode?: string;
}
type MathSpecNodeT = SchemaSpecNodeT<typeof mathSchemaSpec>;
type MathSpecMarkT = SchemaSpecMarkT<typeof mathSchemaSpec>;
////////////////////////////////////////////////////////////
// force typescript to infer generic type arguments for SchemaSpec
function createSchemaSpec<N extends string = any, M extends string = any>(
spec: SchemaSpecJson<N, M>
): SchemaSpecJson<N, M> {
return spec;
}
// bare minimum ProseMirror schema for working with math nodes
export const mathSchemaSpec = createSchemaSpec({
nodes: {
// :: NodeSpec top-level document node
doc: {
content: "block+",
},
paragraph: {
content: "inline*",
group: "block",
parseDOM: [{ tag: "p" }],
toDOM() {
return ["p", 0];
},
},
math_inline: {
group: "inline math",
content: "text*",
inline: true,
atom: true,
toDOM: () => ["math-inline", { class: "math-node" }, 0],
parseDOM: [{ tag: "math-inline" }, ...defaultInlineMathParseRules],
},
math_display: {
group: "block math",
content: "text*",
atom: true,
code: true,
toDOM: () => ["math-display", { class: "math-node" }, 0],
parseDOM: [{ tag: "math-display" }, ...defaultBlockMathParseRules],
},
text: {
group: "inline",
},
},
marks: {
math_select: {
toDOM() {
return ["math-select", 0];
},
parseDOM: [{ tag: "math-select" }],
},
},
});
/**
* Use the prosemirror-math default SchemaSpec to create a new Schema.
*/
export function createMathSchema() {
return new Schema(mathSchemaSpec);
}
/**
* Create a new SchemaSpec by adding math nodes to an existing spec.
* @deprecated This function is included for demonstration/testing only. For the
* time being, I highly recommend adding the math nodes manually to your own
* ProseMirror spec to avoid unexpected interactions between the math nodes
* and your own spec. Use the example spec for reference.
*
* @param baseSpec The SchemaSpec to extend. Must specify a `marks` field, and
* must be a raw object (not an OrderedMap).
*/
export function extendMathSchemaSpec<N extends string, M extends string>(
baseSpec: SchemaSpecJson<N, M>
): SchemaSpecJson<N | MathSpecNodeT, M | MathSpecMarkT> {
let nodes = { ...baseSpec.nodes, ...mathSchemaSpec.nodes };
let marks = { ...baseSpec.marks, ...mathSchemaSpec.marks };
return { nodes, marks, topNode: baseSpec.topNode };
}

View File

@@ -0,0 +1,28 @@
import { NodeSelection } from "prosemirror-state";
import { Command as ProseCommand } from "prosemirror-state";
export const mathBackspaceCmd: ProseCommand = (state, dispatch) => {
// check node before
let { $from } = state.selection;
let nodeBefore = $from.nodeBefore;
if (!nodeBefore) {
return false;
}
if (nodeBefore.type.name == "math_inline") {
// select math node
let index = $from.index($from.depth);
let $beforePos = state.doc.resolve($from.posAtIndex(index - 1));
if (dispatch) {
dispatch(state.tr.setSelection(new NodeSelection($beforePos)));
}
return true;
} else if (nodeBefore.type.name == "math_block") {
/** @todo (8/1/20) implement backspace for math blocks
* check how code blocks behave when pressing backspace
*/
return false;
}
return false;
};

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
import { InputRule } from "prosemirror-inputrules";
import { NodeType } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";
////////////////////////////////////////////////////////////
// ---- Inline Input Rules ------------------------------ //
// simple input rule for inline math
export const REGEX_INLINE_MATH_DOLLARS: RegExp = /\$\$(.+)\$\$/; //new RegExp("\$(.+)\$", "i");
// negative lookbehind regex notation allows for escaped \$ delimiters
// (requires browser supporting ECMA2018 standard -- currently only Chrome / FF)
// (see https://javascript.info/regexp-lookahead-lookbehind)
export const REGEX_INLINE_MATH_DOLLARS_ESCAPED: RegExp = (() => {
// attempt to create regex with negative lookbehind
try {
return new RegExp("(?<!\\\\)\\$(.+)(?<!\\\\)\\$");
} catch (e) {
return REGEX_INLINE_MATH_DOLLARS;
}
})();
// ---- Block Input Rules ------------------------------- //
// simple inputrule for block math
export const REGEX_BLOCK_MATH_DOLLARS: RegExp = /\$\$\$\s+$/; //new RegExp("\$\$\s+$", "i");
////////////////////////////////////////////////////////////
export function makeInlineMathInputRule(
pattern: RegExp,
nodeType: NodeType,
getAttrs?: (match: string[]) => any
) {
return new InputRule(pattern, (state, match, start, end) => {
let $start = state.doc.resolve(start);
let index = $start.index();
let $end = state.doc.resolve(end);
// get attrs
let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
// check if replacement valid
if (!$start.parent.canReplaceWith(index, $end.index(), nodeType)) {
return null;
}
// perform replacement
return state.tr.replaceRangeWith(
start,
end,
nodeType.create(attrs, nodeType.schema.text(match[1]))
);
});
}
export function makeBlockMathInputRule(
pattern: RegExp,
nodeType: NodeType,
getAttrs?: (match: string[]) => any
) {
return new InputRule(pattern, (state, match, start, end) => {
let $start = state.doc.resolve(start);
let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
if (
!$start
.node(-1)
.canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType)
)
return null;
let tr = state.tr
.delete(start, end)
.setBlockType(start, start, nodeType, attrs);
return tr.setSelection(
NodeSelection.create(tr.doc, tr.mapping.map($start.pos - 1))
);
});
}

View File

@@ -0,0 +1,201 @@
/**
* Note that for some of the `ParseRule`s defined below,
* we define a `getAttrs` function, which, other than
* defining node attributes, can be used to describe complex
* match conditions for a rule.
* Returning `false` from `ParseRule.getAttrs` prevents the
* rule from matching, while returning `null` indicates that
* the default set of note attributes should be used.
*/
import {
Node as ProseNode,
Fragment,
ParseRule,
Schema,
NodeType,
} from "prosemirror-model";
////////////////////////////////////////////////////////////
function getFirstMatch(
root: Element,
rules: ((root: Element) => false | string)[]
): false | string {
for (let rule of rules) {
let match: false | string = rule(root);
if (match !== false) {
return match;
}
}
return false;
}
function makeTextFragment<S extends Schema<any, any>>(
text: string,
schema: S
): Fragment {
return Fragment.from(schema.text(text) as ProseNode);
}
////////////////////////////////////////////////////////////
// -- Wikipedia ----------------------------------------- //
/**
* Look for a child node that matches the following template:
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
*/
function texFromMediaWikiFallbackImage(root: Element): false | string {
let match = root.querySelector("img.mwe-math-fallback-image-inline[alt]");
return match?.getAttribute("alt") ?? false;
}
/**
* Look for a child node that matches the following template:
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
*/
function texFromMathML_01(root: Element): false | string {
let match = root.querySelector("math[alttext]");
return match?.getAttribute("alttext") ?? false;
}
/**
* Look for a child node that matches the following template:
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
*/
function texFromMathML_02(root: Element): false | string {
let match = root.querySelector(
"math annotation[encoding='application/x-tex'"
);
return match?.textContent ?? false;
}
/**
* Look for a child node that matches the following template:
* <script type="math/tex"></script>
*/
function texFromScriptTag(root: Element): false | string {
let match = root.querySelector("script[type*='math/tex']");
return match?.textContent ?? false;
}
function matchWikipedia(root: Element): false | string {
let match: false | string = getFirstMatch(root, [
texFromMediaWikiFallbackImage,
texFromMathML_01,
texFromMathML_02,
]);
// TODO: if no tex string was found, but we have MathML, try to parse it
return match;
}
/**
* Wikipedia formats block math inside a <dl>...</dl> element, as below.
*
* - Evidently no CSS class is used to distinguish inline vs block math
* - Sometimes the `\displaystyle` TeX command is present even in inline math
*
* ```html
* <dl><dd><span class="mwe-math-element">
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
* <semantics>
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
* <annotation encoding="application/x-tex">...</annotation>
* </semantics>
* </math>
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
* </span>
* </span></dd></dl>
* ```
*/
export const wikipediaBlockMathParseRule: ParseRule = {
tag: "dl",
getAttrs(p: Node | string): false | null {
let dl = p as HTMLDListElement;
// <dl> must contain exactly one child
if (dl.childElementCount !== 1) {
return false;
}
let dd = dl.firstChild as Element;
if (dd.tagName !== "DD") {
return false;
}
// <dd> must contain exactly one child
if (dd.childElementCount !== 1) {
return false;
}
let mweElt = dd.firstChild as Element;
if (!mweElt.classList.contains("mwe-math-element")) {
return false;
}
// success! proceed to `getContent` for further processing
return null;
},
getContent<S extends Schema<any, any>>(p: Node, schema: S): Fragment {
// search the matched element for a TeX string
let match: false | string = matchWikipedia(p as Element);
// return a fragment representing the math node's children
let texString: string = match || "\\text{\\color{red}(paste error)}";
return makeTextFragment(texString, schema);
},
};
/**
* Parse rule for inline math content on Wikipedia of the following form:
*
* ```html
* <span class="mwe-math-element">
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
* <semantics>
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
* <annotation encoding="application/x-tex">...</annotation>
* </semantics>
* </math>
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
* class="mwe-math-fallback-image-inline"
* alt="..." />
* </span>
* </span>
* ```
*/
export const wikipediaInlineMathParseRule: ParseRule = {
tag: "span",
getAttrs(p: Node | string): false | null {
let span = p as HTMLSpanElement;
if (!span.classList.contains("mwe-math-element")) {
return false;
}
// success! proceed to `getContent` for further processing
return null;
},
getContent<S extends Schema<any, any>>(p: Node, schema: S): Fragment {
// search the matched element for a TeX string
let match: false | string = matchWikipedia(p as Element);
// return a fragment representing the math node's children
let texString: string = match || "\\text{\\color{red}(paste error)}";
return makeTextFragment(texString, schema);
},
};
// -- MathJax ------------------------------------------- //
////////////////////////////////////////////////////////////
export const defaultInlineMathParseRules: ParseRule[] = [
wikipediaInlineMathParseRule,
];
export const defaultBlockMathParseRules: ParseRule[] = [
wikipediaBlockMathParseRule,
];

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
// prosemirror imports
import {
EditorState,
Transaction,
Selection as ProseSelection,
Plugin as ProsePlugin,
EditorStateConfig,
} from "prosemirror-state";
import { DecorationSet, Decoration } from "prosemirror-view";
import { Fragment, Node as ProseNode } from "prosemirror-model";
////////////////////////////////////////////////////////////
/**
* Uses the selection to determine which math_select decorations
* should be applied to the given document.
* @param arg Should be either a Transaction or an EditorState,
* although any object with `selection` and `doc` will work.
*/
const checkSelection = (arg: { selection: ProseSelection; doc: ProseNode }) => {
let { from, to } = arg.selection;
let content: Fragment = arg.selection.content().content;
let result: { start: number; end: number }[] = [];
content.descendants(
(node: ProseNode, pos: number, parent: ProseNode | null) => {
if (node.type.name == "text") {
return false;
}
if (node.type.name.startsWith("math_")) {
result.push({
start: Math.max(from + pos - 1, 0),
end: from + pos + node.nodeSize - 1,
});
return false;
}
return true;
}
);
return DecorationSet.create(
arg.doc,
result.map(({ start, end }) =>
Decoration.node(start, end, { class: "math-select" })
)
);
};
/**
* Due to the internals of KaTeX, by default, selecting rendered
* math will put a box around each individual character of a
* math expression. This plugin attempts to make math selections
* slightly prettier by instead setting a background color on the node.
*
* (remember to use the included math.css!)
*
* @todo (6/13/20) math selection rectangles are not quite even with text
*/
export const mathSelectPlugin: ProsePlugin = new ProsePlugin({
state: {
init(config: EditorStateConfig, partialState: EditorState) {
return checkSelection(partialState);
},
apply(
tr: Transaction,
value: any,
oldState: EditorState,
newState: EditorState
) {
if (!tr.selection || !tr.selectionSet) {
return oldState;
}
let sel = checkSelection(tr);
return sel;
},
},
props: {
decorations: (state: EditorState) => {
return mathSelectPlugin.getState(state);
},
},
});

View File

@@ -0,0 +1,22 @@
import { MathRenderer } from "./types";
import katex from "katex";
// Chemistry formulas support
import "katex/contrib/mhchem/mhchem";
export const KatexRenderer: MathRenderer = {
inline: (text, element) => {
katex.render(text, element, {
displayMode: false,
globalGroup: true,
throwOnError: false,
});
},
block: (text, element) => {
katex.render(text, element, {
displayMode: true,
globalGroup: true,
throwOnError: false,
});
},
};

View File

@@ -0,0 +1,5 @@
export type MathRenderFn = (text: string, element: HTMLElement) => void;
export type MathRenderer = {
inline: MathRenderFn;
block: MathRenderFn;
};

View File

@@ -0,0 +1,97 @@
import {
Node as ProseNode,
Mark,
Slice,
NodeType,
MarkType,
Fragment,
} from "prosemirror-model";
////////////////////////////////////////////////////////////////////////////////
type TypedNode<T extends string> = ProseNode & {
type: NodeType & { name: T };
};
type TypedMark<T extends string> = Mark & {
type: MarkType & { name: T };
};
type NodeSerializer<T extends string> = (node: TypedNode<T>) => string;
type MarkSerializer<T extends string> = (mark: TypedMark<T>) => string;
class ProseMirrorTextSerializer {
public nodes: { [name: string]: NodeSerializer<string> | undefined };
public marks: { [name: string]: MarkSerializer<string> | undefined };
constructor(
fns: {
nodes?: { [name: string]: NodeSerializer<string> | undefined };
marks?: { [name: string]: MarkSerializer<string> | undefined };
},
base?: ProseMirrorTextSerializer
) {
// use base serializer as a fallback
this.nodes = { ...base?.nodes, ...fns.nodes };
this.marks = { ...base?.marks, ...fns.marks };
}
serializeFragment(fragment: Fragment): string {
// adapted from the undocumented `Fragment.textBetween` function
// https://github.com/ProseMirror/prosemirror-model/blob/eef20c8c6dbf841b1d70859df5d59c21b5108a4f/src/fragment.js#L46
let blockSeparator = "\n\n";
let leafText: string | undefined = undefined;
let text: string = "";
let separated: boolean = true;
let from = 0;
let to = fragment.size;
fragment.nodesBetween(
from,
to,
(node, pos) => {
// check if one of our custom serializers handles this node
let serialized: string | null = this.serializeNode(node);
if (serialized !== null) {
text += serialized;
return false;
}
if (node.isText) {
text += node.text?.slice(Math.max(from, pos) - pos, to - pos) || "";
separated = !blockSeparator;
} else if (node.isLeaf && leafText) {
text += leafText;
separated = !blockSeparator;
} else if (!separated && node.isBlock) {
text += blockSeparator;
separated = true;
}
},
0
);
return text;
}
serializeSlice(slice: Slice): string {
return this.serializeFragment(slice.content);
}
serializeNode(node: ProseNode): string | null {
// check if one of our custom serializers handles this node
let nodeSerializer = this.nodes[node.type.name];
if (nodeSerializer !== undefined) {
return nodeSerializer(node);
} else {
return null;
}
}
}
export const mathSerializer = new ProseMirrorTextSerializer({
nodes: {
math_inline: (node) => `$${node.textContent}$`,
math_display: (node) => `\n\n$$\n${node.textContent}\n$$`,
},
});

View File

@@ -0,0 +1,10 @@
import { Schema, SchemaSpec } from "prosemirror-model";
////////////////////////////////////////////////////////////////////////////////
// infer generic `Nodes` and `Marks` type parameters for a SchemaSpec
export type SchemaSpecNodeT<Spec> = Spec extends SchemaSpec<infer N, infer _> ? N : never;
export type SchemaSpecMarkT<Spec> = Spec extends SchemaSpec<infer _, infer M> ? M : never;
export type SchemaNodeT<S> = S extends Schema<infer N, infer _> ? N : never;
export type SchemaMarkT<S> = S extends Schema<infer _, infer M> ? M : never;

View File

@@ -32,6 +32,8 @@ import { EmbedNode } from "./extensions/embed";
import { CodeBlock } from "./extensions/code-block"; import { CodeBlock } from "./extensions/code-block";
import { ListItem } from "./extensions/list-item"; import { ListItem } from "./extensions/list-item";
import { Link } from "@tiptap/extension-link"; import { Link } from "@tiptap/extension-link";
import { Codemark } from "./extensions/code-mark";
import { MathInline, MathBlock } from "./extensions/math";
import { import {
NodeViewSelectionNotifier, NodeViewSelectionNotifier,
usePortalProvider, usePortalProvider,
@@ -123,6 +125,9 @@ const useTiptap = (
OutlineListItem, OutlineListItem,
OutlineList, OutlineList,
ListItem, ListItem,
Codemark,
MathInline,
MathBlock,
], ],
onBeforeCreate: ({ editor }) => { onBeforeCreate: ({ editor }) => {
if (theme) { if (theme) {

View File

@@ -221,3 +221,168 @@ pre *::selection {
border-left: 5px solid var(--border); border-left: 5px solid var(--border);
padding-left: 15px; padding-left: 15px;
} }
/****************************************************************/
/* Styles taken from https://github.com/curvenote/prosemirror-codemark/blob/main/src/codemark.css */
/****************************************************************/
@keyframes blink {
49% {
border-color: unset;
}
50% {
border-color: #fff;
}
99% {
border-color: #fff;
}
}
.no-cursor {
caret-color: transparent;
}
div:focus .fake-cursor,
span:focus .fake-cursor {
margin-right: -1px;
border-left-width: 1px;
border-left-style: solid;
animation: blink 1s;
animation-iteration-count: infinite;
position: relative;
z-index: 1;
}
/*******************************************/
/* MATH STYLES */
/*******************************************/
/*---------------------------------------------------------
* Author: Benjamin R. Bray
* License: MIT (see LICENSE in project root for details)
*--------------------------------------------------------*/
/* == Math Nodes ======================================== */
.math-node {
min-width: 1em;
min-height: 1em;
font-size: 0.95em;
font-family: "Consolas", "Ubuntu Mono", monospace;
cursor: auto;
}
.math-node.empty-math .math-render::before {
content: "(empty)";
color: red;
}
.math-node .math-render.parse-error::before {
content: "(math error)";
color: red;
cursor: help;
}
.math-node.ProseMirror-selectednode {
outline: none;
}
.math-node .math-src {
display: none;
color: var(--text);
tab-size: 4;
}
.math-node.ProseMirror-selectednode .math-src {
display: inline;
}
.math-node.ProseMirror-selectednode .math-render {
display: none;
}
/* -- Inline Math --------------------------------------- */
.math-inline {
display: inline;
white-space: nowrap;
}
.math-inline .math-render {
display: inline-block;
/* font-size: 0.85em; */
cursor: pointer;
}
.math-inline .math-src .ProseMirror {
display: inline;
/* Necessary to fix FireFox bug with contenteditable, https://bugzilla.mozilla.org/show_bug.cgi?id=1252108 */
border-right: 1px solid transparent;
border-left: 1px solid transparent;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace !important;
}
.math-inline.ProseMirror-selectednode {
background-color: var(--bgSecondary);
padding: 3px;
border-radius: 5px;
border: 1px solid var(--border);
}
.math-inline .math-src::after,
.math-inline .math-src::before {
content: "$$";
color: var(--disabled);
}
/* -- Block Math ---------------------------------------- */
.math-display {
display: block;
}
.math-display .math-render {
display: block;
}
.math-display.ProseMirror-selectednode {
background-color: var(--bgSecondary);
padding: 10px;
border-radius: 5px;
border: 1px solid var(--border);
}
.math-display .math-src .ProseMirror {
width: 100%;
display: block;
margin-top: 10px;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace !important;
}
/* .math-display .math-src::after, */
.math-display .math-src::before {
content: "Math";
text-align: left;
color: var(--disabled);
margin-bottom: 10px;
}
.math-display .katex-display {
margin: 0;
}
/* -- Selection Plugin ---------------------------------- */
/* p::selection,
p > *::selection {
background-color: #c0c0c0;
}
.katex-html *::selection {
background-color: none !important;
}
.math-node.math-select .math-render {
background-color: #c0c0c0ff;
}
.math-inline.math-select .math-render {
padding-top: 2px;
} */