diff --git a/packages/editor/dist/components/button.d.ts b/packages/editor/dist/components/button.d.ts index f118730d4..d585b4df9 100644 --- a/packages/editor/dist/components/button.d.ts +++ b/packages/editor/dist/components/button.d.ts @@ -1,3 +1,3 @@ /// import { ButtonProps } from "rebass"; -export declare const Button: import("react").ForwardRefExoticComponent & import("react").RefAttributes>; +export declare const Button: import("react").ForwardRefExoticComponent & import("react").RefAttributes>; diff --git a/packages/editor/dist/components/menu/usefocus.d.ts b/packages/editor/dist/components/menu/usefocus.d.ts index 36b4ee2ca..66f324efc 100644 --- a/packages/editor/dist/components/menu/usefocus.d.ts +++ b/packages/editor/dist/components/menu/usefocus.d.ts @@ -1,4 +1,3 @@ -/// import { MenuItem } from "./types"; export declare function useFocus(items: MenuItem[], onAction: (event: KeyboardEvent) => void, onClose: (event: KeyboardEvent) => void): { focusIndex: number; diff --git a/packages/editor/dist/extensions/codemark/codemark.d.ts b/packages/editor/dist/extensions/codemark/codemark.d.ts new file mode 100644 index 000000000..003c47b2f --- /dev/null +++ b/packages/editor/dist/extensions/codemark/codemark.d.ts @@ -0,0 +1,2 @@ +import { Extension } from "@tiptap/core"; +export declare const Codemark: Extension; diff --git a/packages/editor/dist/extensions/codemark/codemark.js b/packages/editor/dist/extensions/codemark/codemark.js new file mode 100644 index 000000000..153f554ff --- /dev/null +++ b/packages/editor/dist/extensions/codemark/codemark.js @@ -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 }); + }, +}); diff --git a/packages/editor/dist/extensions/codemark/index.d.ts b/packages/editor/dist/extensions/codemark/index.d.ts new file mode 100644 index 000000000..8c4911b98 --- /dev/null +++ b/packages/editor/dist/extensions/codemark/index.d.ts @@ -0,0 +1,2 @@ +export * from "./code-mark"; +export { Codemark as default } from "./code-mark"; diff --git a/packages/editor/dist/extensions/codemark/index.js b/packages/editor/dist/extensions/codemark/index.js new file mode 100644 index 000000000..8c4911b98 --- /dev/null +++ b/packages/editor/dist/extensions/codemark/index.js @@ -0,0 +1,2 @@ +export * from "./code-mark"; +export { Codemark as default } from "./code-mark"; diff --git a/packages/editor/dist/extensions/math/index.d.ts b/packages/editor/dist/extensions/math/index.d.ts new file mode 100644 index 000000000..1331fdb01 --- /dev/null +++ b/packages/editor/dist/extensions/math/index.d.ts @@ -0,0 +1,2 @@ +export { MathInline } from "./math-inline"; +export { MathBlock } from "./math-block"; diff --git a/packages/editor/dist/extensions/math/index.js b/packages/editor/dist/extensions/math/index.js new file mode 100644 index 000000000..1331fdb01 --- /dev/null +++ b/packages/editor/dist/extensions/math/index.js @@ -0,0 +1,2 @@ +export { MathInline } from "./math-inline"; +export { MathBlock } from "./math-block"; diff --git a/packages/editor/dist/extensions/math/mathblock.d.ts b/packages/editor/dist/extensions/math/mathblock.d.ts new file mode 100644 index 000000000..419bb7458 --- /dev/null +++ b/packages/editor/dist/extensions/math/mathblock.d.ts @@ -0,0 +1,2 @@ +import { Node } from "@tiptap/core"; +export declare const MathBlock: Node; diff --git a/packages/editor/dist/extensions/math/mathblock.js b/packages/editor/dist/extensions/math/mathblock.js new file mode 100644 index 000000000..af36cedd1 --- /dev/null +++ b/packages/editor/dist/extensions/math/mathblock.js @@ -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]; + }, +}); diff --git a/packages/editor/dist/extensions/math/mathinline.d.ts b/packages/editor/dist/extensions/math/mathinline.d.ts new file mode 100644 index 000000000..7b3a718d8 --- /dev/null +++ b/packages/editor/dist/extensions/math/mathinline.d.ts @@ -0,0 +1,3 @@ +import { Node } from "@tiptap/core"; +import "katex/dist/katex.min.css"; +export declare const MathInline: Node; diff --git a/packages/editor/dist/extensions/math/mathinline.js b/packages/editor/dist/extensions/math/mathinline.js new file mode 100644 index 000000000..de3fc67c9 --- /dev/null +++ b/packages/editor/dist/extensions/math/mathinline.js @@ -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]; + }, +}); diff --git a/packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.d.ts b/packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.d.ts new file mode 100644 index 000000000..c315ac00a --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.d.ts @@ -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; diff --git a/packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.js b/packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.js new file mode 100644 index 000000000..617ed9838 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.js @@ -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; + }; +} diff --git a/packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.d.ts b/packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.d.ts new file mode 100644 index 000000000..52e3b7a12 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.d.ts @@ -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; diff --git a/packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.js b/packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.js new file mode 100644 index 000000000..ce7bfcf35 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.js @@ -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; + }; +} diff --git a/packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.d.ts b/packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.d.ts new file mode 100644 index 000000000..d1361e53e --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.d.ts @@ -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; diff --git a/packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.js b/packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.js new file mode 100644 index 000000000..2421f67fd --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.js @@ -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); diff --git a/packages/editor/dist/extensions/math/plugin/index.d.ts b/packages/editor/dist/extensions/math/plugin/index.d.ts new file mode 100644 index 000000000..aac183ae8 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/index.d.ts @@ -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"; diff --git a/packages/editor/dist/extensions/math/plugin/index.js b/packages/editor/dist/extensions/math/plugin/index.js new file mode 100644 index 000000000..5433d8045 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/index.js @@ -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"; diff --git a/packages/editor/dist/extensions/math/plugin/math-nodeview.d.ts b/packages/editor/dist/extensions/math/plugin/math-nodeview.d.ts new file mode 100644 index 000000000..b11bb5d34 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/math-nodeview.d.ts @@ -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, 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 {}; diff --git a/packages/editor/dist/extensions/math/plugin/math-nodeview.js b/packages/editor/dist/extensions/math/plugin/math-nodeview.js new file mode 100644 index 000000000..43e424707 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/math-nodeview.js @@ -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 }; diff --git a/packages/editor/dist/extensions/math/plugin/math-plugin.d.ts b/packages/editor/dist/extensions/math/plugin/math-plugin.d.ts new file mode 100644 index 000000000..b7bd86a3f --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/math-plugin.d.ts @@ -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; diff --git a/packages/editor/dist/extensions/math/plugin/math-plugin.js b/packages/editor/dist/extensions/math/plugin/math-plugin.js new file mode 100644 index 000000000..f5d52a27e --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/math-plugin.js @@ -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); diff --git a/packages/editor/dist/extensions/math/plugin/math-schema.d.ts b/packages/editor/dist/extensions/math/plugin/math-schema.d.ts new file mode 100644 index 000000000..8331ba152 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/math-schema.d.ts @@ -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(x: { [name in T]: string; } | null) { + * const s = { ...x }; // inferred to have type `{}`. + * } + * + * @see https://github.com/microsoft/TypeScript/issues/10727 + */ +interface SchemaSpecJson extends SchemaSpec { + nodes: { + [name in N]: NodeSpec; + }; + marks: { + [name in M]: MarkSpec; + }; + topNode?: string; +} +declare type MathSpecNodeT = SchemaSpecNodeT; +declare type MathSpecMarkT = SchemaSpecMarkT; +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(baseSpec: SchemaSpecJson): SchemaSpecJson; +export {}; diff --git a/packages/editor/dist/extensions/math/plugin/math-schema.js b/packages/editor/dist/extensions/math/plugin/math-schema.js new file mode 100644 index 000000000..b8384789e --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/math-schema.js @@ -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 }; +} diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-backspace.d.ts b/packages/editor/dist/extensions/math/plugin/plugins/math-backspace.d.ts new file mode 100644 index 000000000..634f31931 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-backspace.d.ts @@ -0,0 +1,2 @@ +import { Command as ProseCommand } from "prosemirror-state"; +export declare const mathBackspaceCmd: ProseCommand; diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-backspace.js b/packages/editor/dist/extensions/math/plugin/plugins/math-backspace.js new file mode 100644 index 000000000..23646606f --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-backspace.js @@ -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; +}; diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.d.ts b/packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.d.ts new file mode 100644 index 000000000..92f1be7e5 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.d.ts @@ -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; diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.js b/packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.js new file mode 100644 index 000000000..6b4868f8f --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.js @@ -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("(?... 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 + *
+ * + * + * + * ... + * ... + * + * + * ... + * + *
+ * ``` + */ +export declare const wikipediaBlockMathParseRule: ParseRule; +/** + * Parse rule for inline math content on Wikipedia of the following form: + * + * ```html + * + * + * + * + * ... + * ... + * + * + * ... + * + * + * ``` + */ +export declare const wikipediaInlineMathParseRule: ParseRule; +export declare const defaultInlineMathParseRules: ParseRule[]; +export declare const defaultBlockMathParseRules: ParseRule[]; diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.js b/packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.js new file mode 100644 index 000000000..bc92772b6 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.js @@ -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: + * ... + */ +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: + * + */ +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: + * + */ +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: + * + */ +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
...
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 + *
+ * + * + * + * ... + * ... + * + * + * ... + * + *
+ * ``` + */ +export var wikipediaBlockMathParseRule = { + tag: "dl", + getAttrs: function (p) { + var dl = p; + //
must contain exactly one child + if (dl.childElementCount !== 1) { + return false; + } + var dd = dl.firstChild; + if (dd.tagName !== "DD") { + return false; + } + //
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 + * + * + * + * + * ... + * ... + * + * + * ... + * + * + * ``` + */ +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, +]; diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-select.d.ts b/packages/editor/dist/extensions/math/plugin/plugins/math-select.d.ts new file mode 100644 index 000000000..fa6e7915e --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-select.d.ts @@ -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; diff --git a/packages/editor/dist/extensions/math/plugin/plugins/math-select.js b/packages/editor/dist/extensions/math/plugin/plugins/math-select.js new file mode 100644 index 000000000..c5a81288c --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/plugins/math-select.js @@ -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); + }, + }, +}); diff --git a/packages/editor/dist/extensions/math/plugin/renderers/katex.d.ts b/packages/editor/dist/extensions/math/plugin/renderers/katex.d.ts new file mode 100644 index 000000000..5a0ac882a --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/renderers/katex.d.ts @@ -0,0 +1,3 @@ +import { MathRenderer } from "./types"; +import "katex/contrib/mhchem/mhchem"; +export declare const KatexRenderer: MathRenderer; diff --git a/packages/editor/dist/extensions/math/plugin/renderers/katex.js b/packages/editor/dist/extensions/math/plugin/renderers/katex.js new file mode 100644 index 000000000..ba719b03a --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/renderers/katex.js @@ -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, + }); + }, +}; diff --git a/packages/editor/dist/extensions/math/plugin/renderers/types.d.ts b/packages/editor/dist/extensions/math/plugin/renderers/types.d.ts new file mode 100644 index 000000000..437b6338b --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/renderers/types.d.ts @@ -0,0 +1,5 @@ +export declare type MathRenderFn = (text: string, element: HTMLElement) => void; +export declare type MathRenderer = { + inline: MathRenderFn; + block: MathRenderFn; +}; diff --git a/packages/editor/dist/extensions/math/plugin/renderers/types.js b/packages/editor/dist/extensions/math/plugin/renderers/types.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/renderers/types.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/editor/dist/extensions/math/plugin/utils/text-serializer.d.ts b/packages/editor/dist/extensions/math/plugin/utils/text-serializer.d.ts new file mode 100644 index 000000000..4e03d2a81 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/utils/text-serializer.d.ts @@ -0,0 +1,34 @@ +import { Node as ProseNode, Mark, Slice, NodeType, MarkType, Fragment } from "prosemirror-model"; +declare type TypedNode = ProseNode & { + type: NodeType & { + name: T; + }; +}; +declare type TypedMark = Mark & { + type: MarkType & { + name: T; + }; +}; +declare type NodeSerializer = (node: TypedNode) => string; +declare type MarkSerializer = (mark: TypedMark) => string; +declare class ProseMirrorTextSerializer { + nodes: { + [name: string]: NodeSerializer | undefined; + }; + marks: { + [name: string]: MarkSerializer | undefined; + }; + constructor(fns: { + nodes?: { + [name: string]: NodeSerializer | undefined; + }; + marks?: { + [name: string]: MarkSerializer | undefined; + }; + }, base?: ProseMirrorTextSerializer); + serializeFragment(fragment: Fragment): string; + serializeSlice(slice: Slice): string; + serializeNode(node: ProseNode): string | null; +} +export declare const mathSerializer: ProseMirrorTextSerializer; +export {}; diff --git a/packages/editor/dist/extensions/math/plugin/utils/text-serializer.js b/packages/editor/dist/extensions/math/plugin/utils/text-serializer.js new file mode 100644 index 000000000..8ba3753ba --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/utils/text-serializer.js @@ -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$$"); }, + }, +}); diff --git a/packages/editor/dist/extensions/math/plugin/utils/types.d.ts b/packages/editor/dist/extensions/math/plugin/utils/types.d.ts new file mode 100644 index 000000000..6792b9f33 --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/utils/types.d.ts @@ -0,0 +1,5 @@ +import { Schema, SchemaSpec } from "prosemirror-model"; +export declare type SchemaSpecNodeT = Spec extends SchemaSpec ? N : never; +export declare type SchemaSpecMarkT = Spec extends SchemaSpec ? M : never; +export declare type SchemaNodeT = S extends Schema ? N : never; +export declare type SchemaMarkT = S extends Schema ? M : never; diff --git a/packages/editor/dist/extensions/math/plugin/utils/types.js b/packages/editor/dist/extensions/math/plugin/utils/types.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/editor/dist/extensions/math/plugin/utils/types.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/editor/dist/index.js b/packages/editor/dist/index.js index d6617d91f..24644e49c 100644 --- a/packages/editor/dist/index.js +++ b/packages/editor/dist/index.js @@ -53,6 +53,8 @@ import { EmbedNode } from "./extensions/embed"; import { CodeBlock } from "./extensions/code-block"; import { ListItem } from "./extensions/list-item"; 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 { OutlineList } from "./extensions/outline-list"; import { OutlineListItem } from "./extensions/outline-list-item"; @@ -130,6 +132,9 @@ var useTiptap = function (options, deps) { OutlineListItem, OutlineList, ListItem, + Codemark, + MathInline, + MathBlock, ], onBeforeCreate: function (_a) { var editor = _a.editor; diff --git a/packages/editor/dist/toolbar/components/toolbutton.d.ts b/packages/editor/dist/toolbar/components/toolbutton.d.ts index 2cde85f8c..74e32eb1e 100644 --- a/packages/editor/dist/toolbar/components/toolbutton.d.ts +++ b/packages/editor/dist/toolbar/components/toolbutton.d.ts @@ -14,8 +14,8 @@ export declare type ToolButtonProps = ButtonProps & { }; export declare const ToolButton: React.NamedExoticComponent | undefined; variant?: ToolButtonVariant | undefined; diff --git a/packages/editor/dist/toolbar/tools/utils.d.ts b/packages/editor/dist/toolbar/tools/utils.d.ts index 2aaf4fe1f..173fe5f55 100644 --- a/packages/editor/dist/toolbar/tools/utils.d.ts +++ b/packages/editor/dist/toolbar/tools/utils.d.ts @@ -1,4 +1,3 @@ -/// import { Editor } from "@tiptap/core"; import { MenuButton } from "../../components/menu/types"; import { ToolProps } from "../types"; diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 86eb7ae87..50064dab7 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -37,8 +37,10 @@ "@tiptap/starter-kit": "^2.0.0-beta.185", "detect-indent": "^7.0.0", "emotion-theming": "^10.0.19", + "katex": "^0.13.24", "lowlight": "^2.6.1", "prism-themes": "^1.9.0", + "prosemirror-codemark": "^0.4.0", "prosemirror-tables": "^1.1.1", "prosemirror-utils": "github:atlassian/prosemirror-utils", "prosemirror-view": "^1.24.1", @@ -57,6 +59,7 @@ }, "devDependencies": { "@types/esm": "^3.2.0", + "@types/katex": "^0.14.0", "@types/node": "^16.11.11", "@types/prismjs": "^1.26.0", "@types/react": "^17.0.37", @@ -4564,6 +4567,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "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": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -15131,6 +15140,29 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -16567,9 +16599,9 @@ } }, "node_modules/orderedmap": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.7.tgz", - "integrity": "sha512-B1SuadDDwIRXXutaJQ1xjreGL3hxujpexBG4PquoXbgJD8bjp2k8b8qI/mk7q0LUdIx7T8IALWB8mPbfsjbGCw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.0.0.tgz", + "integrity": "sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ==" }, "node_modules/os-browserify": { "version": "0.3.0", @@ -18709,6 +18741,17 @@ "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz", @@ -18750,6 +18793,16 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz", @@ -18760,11 +18813,11 @@ } }, "node_modules/prosemirror-model": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.17.0.tgz", - "integrity": "sha512-RJBDgZs/W26yyx1itrk5b3H9FxIro3K7Xjc2QWJI99Gu1nxYAnIggqI3fIOD8Jd/6QZfM+t6elZFJPycVexMTA==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.18.1.tgz", + "integrity": "sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==", "dependencies": { - "orderedmap": "^1.1.0" + "orderedmap": "^2.0.0" } }, "node_modules/prosemirror-schema-list": { @@ -18778,9 +18831,9 @@ } }, "node_modules/prosemirror-state": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.0.tgz", - "integrity": "sha512-mVDZdjNX/YT5FvypiwbphJe9psA5h+j9apsSszVRFc6oKFoIInvzdujh8QW9f9lwHtSYajLxNiM1hPhd0Sl1XA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.1.tgz", + "integrity": "sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" @@ -28787,6 +28840,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "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": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -37049,6 +37108,21 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -38203,9 +38277,9 @@ } }, "orderedmap": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.7.tgz", - "integrity": "sha512-B1SuadDDwIRXXutaJQ1xjreGL3hxujpexBG4PquoXbgJD8bjp2k8b8qI/mk7q0LUdIx7T8IALWB8mPbfsjbGCw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.0.0.tgz", + "integrity": "sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ==" }, "os-browserify": { "version": "0.3.0", @@ -39964,6 +40038,12 @@ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz", @@ -40005,6 +40085,16 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz", @@ -40015,11 +40105,11 @@ } }, "prosemirror-model": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.17.0.tgz", - "integrity": "sha512-RJBDgZs/W26yyx1itrk5b3H9FxIro3K7Xjc2QWJI99Gu1nxYAnIggqI3fIOD8Jd/6QZfM+t6elZFJPycVexMTA==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.18.1.tgz", + "integrity": "sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==", "requires": { - "orderedmap": "^1.1.0" + "orderedmap": "^2.0.0" } }, "prosemirror-schema-list": { @@ -40033,9 +40123,9 @@ } }, "prosemirror-state": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.0.tgz", - "integrity": "sha512-mVDZdjNX/YT5FvypiwbphJe9psA5h+j9apsSszVRFc6oKFoIInvzdujh8QW9f9lwHtSYajLxNiM1hPhd0Sl1XA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.1.tgz", + "integrity": "sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" diff --git a/packages/editor/package.json b/packages/editor/package.json index 6ad9d299f..be8801b5b 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -33,8 +33,10 @@ "@tiptap/starter-kit": "^2.0.0-beta.185", "detect-indent": "^7.0.0", "emotion-theming": "^10.0.19", + "katex": "^0.13.24", "lowlight": "^2.6.1", "prism-themes": "^1.9.0", + "prosemirror-codemark": "^0.4.0", "prosemirror-tables": "^1.1.1", "prosemirror-utils": "github:atlassian/prosemirror-utils", "prosemirror-view": "^1.24.1", @@ -53,6 +55,7 @@ }, "devDependencies": { "@types/esm": "^3.2.0", + "@types/katex": "^0.14.0", "@types/node": "^16.11.11", "@types/prismjs": "^1.26.0", "@types/react": "^17.0.37", diff --git a/packages/editor/src/extensions/code-mark/code-mark.ts b/packages/editor/src/extensions/code-mark/code-mark.ts new file mode 100644 index 000000000..fab58e498 --- /dev/null +++ b/packages/editor/src/extensions/code-mark/code-mark.ts @@ -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 }); + }, +}); diff --git a/packages/editor/src/extensions/code-mark/index.ts b/packages/editor/src/extensions/code-mark/index.ts new file mode 100644 index 000000000..8c4911b98 --- /dev/null +++ b/packages/editor/src/extensions/code-mark/index.ts @@ -0,0 +1,2 @@ +export * from "./code-mark"; +export { Codemark as default } from "./code-mark"; diff --git a/packages/editor/src/extensions/math/index.ts b/packages/editor/src/extensions/math/index.ts new file mode 100644 index 000000000..1331fdb01 --- /dev/null +++ b/packages/editor/src/extensions/math/index.ts @@ -0,0 +1,2 @@ +export { MathInline } from "./math-inline"; +export { MathBlock } from "./math-block"; diff --git a/packages/editor/src/extensions/math/math-block.ts b/packages/editor/src/extensions/math/math-block.ts new file mode 100644 index 000000000..fac66712a --- /dev/null +++ b/packages/editor/src/extensions/math/math-block.ts @@ -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]; + }, +}); diff --git a/packages/editor/src/extensions/math/math-inline.ts b/packages/editor/src/extensions/math/math-inline.ts new file mode 100644 index 000000000..40ac3fd91 --- /dev/null +++ b/packages/editor/src/extensions/math/math-inline.ts @@ -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]; + }, +}); diff --git a/packages/editor/src/extensions/math/plugin/commands/collapse-math-cmd.ts b/packages/editor/src/extensions/math/plugin/commands/collapse-math-cmd.ts new file mode 100644 index 000000000..c2fd675a0 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/commands/collapse-math-cmd.ts @@ -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; + }; +} diff --git a/packages/editor/src/extensions/math/plugin/commands/insert-math-cmd.ts b/packages/editor/src/extensions/math/plugin/commands/insert-math-cmd.ts new file mode 100644 index 000000000..88be960b6 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/commands/insert-math-cmd.ts @@ -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; + }; +} diff --git a/packages/editor/src/extensions/math/plugin/commands/move-cursor-cmd.ts b/packages/editor/src/extensions/math/plugin/commands/move-cursor-cmd.ts new file mode 100644 index 000000000..d23ef1efb --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/commands/move-cursor-cmd.ts @@ -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); diff --git a/packages/editor/src/extensions/math/plugin/global.d.ts b/packages/editor/src/extensions/math/plugin/global.d.ts new file mode 100644 index 000000000..11c872594 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/global.d.ts @@ -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; + } +} \ No newline at end of file diff --git a/packages/editor/src/extensions/math/plugin/index.ts b/packages/editor/src/extensions/math/plugin/index.ts new file mode 100644 index 000000000..3a1a38b66 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/index.ts @@ -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"; diff --git a/packages/editor/src/extensions/math/plugin/math-node-view.ts b/packages/editor/src/extensions/math/plugin/math-node-view.ts new file mode 100644 index 000000000..29d787dc7 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/math-node-view.ts @@ -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; + 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, + 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; + } +} diff --git a/packages/editor/src/extensions/math/plugin/math-plugin.ts b/packages/editor/src/extensions/math/plugin/math-plugin.ts new file mode 100644 index 000000000..e5702ec72 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/math-plugin.ts @@ -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("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 = { + 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); diff --git a/packages/editor/src/extensions/math/plugin/math-schema.ts b/packages/editor/src/extensions/math/plugin/math-schema.ts new file mode 100644 index 000000000..6c70bdba3 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/math-schema.ts @@ -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(x: { [name in T]: string; } | null) { + * const s = { ...x }; // inferred to have type `{}`. + * } + * + * @see https://github.com/microsoft/TypeScript/issues/10727 + */ +interface SchemaSpecJson + extends SchemaSpec { + nodes: { [name in N]: NodeSpec }; + marks: { [name in M]: MarkSpec }; + topNode?: string; +} + +type MathSpecNodeT = SchemaSpecNodeT; +type MathSpecMarkT = SchemaSpecMarkT; + +//////////////////////////////////////////////////////////// + +// force typescript to infer generic type arguments for SchemaSpec +function createSchemaSpec( + spec: SchemaSpecJson +): SchemaSpecJson { + 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( + baseSpec: SchemaSpecJson +): SchemaSpecJson { + let nodes = { ...baseSpec.nodes, ...mathSchemaSpec.nodes }; + let marks = { ...baseSpec.marks, ...mathSchemaSpec.marks }; + return { nodes, marks, topNode: baseSpec.topNode }; +} diff --git a/packages/editor/src/extensions/math/plugin/plugins/math-backspace.ts b/packages/editor/src/extensions/math/plugin/plugins/math-backspace.ts new file mode 100644 index 000000000..ec7670b4c --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/plugins/math-backspace.ts @@ -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; +}; diff --git a/packages/editor/src/extensions/math/plugin/plugins/math-input-rules.ts b/packages/editor/src/extensions/math/plugin/plugins/math-input-rules.ts new file mode 100644 index 000000000..387bd1f29 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/plugins/math-input-rules.ts @@ -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("(? 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)) + ); + }); +} diff --git a/packages/editor/src/extensions/math/plugin/plugins/math-paste-rules.ts b/packages/editor/src/extensions/math/plugin/plugins/math-paste-rules.ts new file mode 100644 index 000000000..89cdecc32 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/plugins/math-paste-rules.ts @@ -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>( + text: string, + schema: S +): Fragment { + return Fragment.from(schema.text(text) as ProseNode); +} + +//////////////////////////////////////////////////////////// + +// -- Wikipedia ----------------------------------------- // + +/** + * Look for a child node that matches the following template: + * ... + */ +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: + * + */ +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: + * + */ +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: + * + */ +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
...
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 + *
+ * + * + * + * ... + * ... + * + * + * ... + * + *
+ * ``` + */ +export const wikipediaBlockMathParseRule: ParseRule = { + tag: "dl", + getAttrs(p: Node | string): false | null { + let dl = p as HTMLDListElement; + + //
must contain exactly one child + if (dl.childElementCount !== 1) { + return false; + } + let dd = dl.firstChild as Element; + if (dd.tagName !== "DD") { + return false; + } + + //
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>(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 + * + * + * + * + * ... + * ... + * + * + * ... + * + * + * ``` + */ +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>(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, +]; diff --git a/packages/editor/src/extensions/math/plugin/plugins/math-select.ts b/packages/editor/src/extensions/math/plugin/plugins/math-select.ts new file mode 100644 index 000000000..bd2ab58a7 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/plugins/math-select.ts @@ -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); + }, + }, +}); diff --git a/packages/editor/src/extensions/math/plugin/renderers/katex.ts b/packages/editor/src/extensions/math/plugin/renderers/katex.ts new file mode 100644 index 000000000..2fb35f813 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/renderers/katex.ts @@ -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, + }); + }, +}; diff --git a/packages/editor/src/extensions/math/plugin/renderers/types.ts b/packages/editor/src/extensions/math/plugin/renderers/types.ts new file mode 100644 index 000000000..ffd5bf1c5 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/renderers/types.ts @@ -0,0 +1,5 @@ +export type MathRenderFn = (text: string, element: HTMLElement) => void; +export type MathRenderer = { + inline: MathRenderFn; + block: MathRenderFn; +}; diff --git a/packages/editor/src/extensions/math/plugin/utils/text-serializer.ts b/packages/editor/src/extensions/math/plugin/utils/text-serializer.ts new file mode 100644 index 000000000..b56fed4e4 --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/utils/text-serializer.ts @@ -0,0 +1,97 @@ +import { + Node as ProseNode, + Mark, + Slice, + NodeType, + MarkType, + Fragment, +} from "prosemirror-model"; + +//////////////////////////////////////////////////////////////////////////////// + +type TypedNode = ProseNode & { + type: NodeType & { name: T }; +}; +type TypedMark = Mark & { + type: MarkType & { name: T }; +}; + +type NodeSerializer = (node: TypedNode) => string; +type MarkSerializer = (mark: TypedMark) => string; + +class ProseMirrorTextSerializer { + public nodes: { [name: string]: NodeSerializer | undefined }; + public marks: { [name: string]: MarkSerializer | undefined }; + + constructor( + fns: { + nodes?: { [name: string]: NodeSerializer | undefined }; + marks?: { [name: string]: MarkSerializer | 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$$`, + }, +}); diff --git a/packages/editor/src/extensions/math/plugin/utils/types.ts b/packages/editor/src/extensions/math/plugin/utils/types.ts new file mode 100644 index 000000000..299f378eb --- /dev/null +++ b/packages/editor/src/extensions/math/plugin/utils/types.ts @@ -0,0 +1,10 @@ +import { Schema, SchemaSpec } from "prosemirror-model"; + +//////////////////////////////////////////////////////////////////////////////// + +// infer generic `Nodes` and `Marks` type parameters for a SchemaSpec +export type SchemaSpecNodeT = Spec extends SchemaSpec ? N : never; +export type SchemaSpecMarkT = Spec extends SchemaSpec ? M : never; + +export type SchemaNodeT = S extends Schema ? N : never; +export type SchemaMarkT = S extends Schema ? M : never; \ No newline at end of file diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index d8be0ae04..8d7338bb2 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -32,6 +32,8 @@ import { EmbedNode } from "./extensions/embed"; import { CodeBlock } from "./extensions/code-block"; import { ListItem } from "./extensions/list-item"; import { Link } from "@tiptap/extension-link"; +import { Codemark } from "./extensions/code-mark"; +import { MathInline, MathBlock } from "./extensions/math"; import { NodeViewSelectionNotifier, usePortalProvider, @@ -123,6 +125,9 @@ const useTiptap = ( OutlineListItem, OutlineList, ListItem, + Codemark, + MathInline, + MathBlock, ], onBeforeCreate: ({ editor }) => { if (theme) { diff --git a/packages/editor/src/styles.css b/packages/editor/src/styles.css index e70d8b02b..119db826e 100644 --- a/packages/editor/src/styles.css +++ b/packages/editor/src/styles.css @@ -221,3 +221,168 @@ pre *::selection { border-left: 5px solid var(--border); 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; +} */