mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-28 16:06:47 +01:00
feat: add math support
This commit is contained in:
2
packages/editor/dist/components/button.d.ts
vendored
2
packages/editor/dist/components/button.d.ts
vendored
File diff suppressed because one or more lines are too long
@@ -1,4 +1,3 @@
|
|||||||
/// <reference types="react" />
|
|
||||||
import { MenuItem } from "./types";
|
import { MenuItem } from "./types";
|
||||||
export declare function useFocus(items: MenuItem[], onAction: (event: KeyboardEvent) => void, onClose: (event: KeyboardEvent) => void): {
|
export declare function useFocus(items: MenuItem[], onAction: (event: KeyboardEvent) => void, onClose: (event: KeyboardEvent) => void): {
|
||||||
focusIndex: number;
|
focusIndex: number;
|
||||||
|
|||||||
2
packages/editor/dist/extensions/codemark/codemark.d.ts
vendored
Normal file
2
packages/editor/dist/extensions/codemark/codemark.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
export declare const Codemark: Extension<any, any>;
|
||||||
9
packages/editor/dist/extensions/codemark/codemark.js
vendored
Normal file
9
packages/editor/dist/extensions/codemark/codemark.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
import codemark from "prosemirror-codemark";
|
||||||
|
// import "prosemirror-codemark/dist/codemark.css";
|
||||||
|
export var Codemark = Extension.create({
|
||||||
|
name: "codemarkPlugin",
|
||||||
|
addProseMirrorPlugins: function () {
|
||||||
|
return codemark({ markType: this.editor.schema.marks.code });
|
||||||
|
},
|
||||||
|
});
|
||||||
2
packages/editor/dist/extensions/codemark/index.d.ts
vendored
Normal file
2
packages/editor/dist/extensions/codemark/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./code-mark";
|
||||||
|
export { Codemark as default } from "./code-mark";
|
||||||
2
packages/editor/dist/extensions/codemark/index.js
vendored
Normal file
2
packages/editor/dist/extensions/codemark/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./code-mark";
|
||||||
|
export { Codemark as default } from "./code-mark";
|
||||||
2
packages/editor/dist/extensions/math/index.d.ts
vendored
Normal file
2
packages/editor/dist/extensions/math/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { MathInline } from "./math-inline";
|
||||||
|
export { MathBlock } from "./math-block";
|
||||||
2
packages/editor/dist/extensions/math/index.js
vendored
Normal file
2
packages/editor/dist/extensions/math/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { MathInline } from "./math-inline";
|
||||||
|
export { MathBlock } from "./math-block";
|
||||||
2
packages/editor/dist/extensions/math/mathblock.d.ts
vendored
Normal file
2
packages/editor/dist/extensions/math/mathblock.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { Node } from "@tiptap/core";
|
||||||
|
export declare const MathBlock: Node<any, any>;
|
||||||
31
packages/editor/dist/extensions/math/mathblock.js
vendored
Normal file
31
packages/editor/dist/extensions/math/mathblock.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { inputRules } from "prosemirror-inputrules";
|
||||||
|
import { makeBlockMathInputRule, REGEX_BLOCK_MATH_DOLLARS, } from "./plugin";
|
||||||
|
export var MathBlock = Node.create({
|
||||||
|
name: "math_display",
|
||||||
|
group: "block math",
|
||||||
|
content: "text*",
|
||||||
|
atom: true,
|
||||||
|
code: true,
|
||||||
|
parseHTML: function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "div[class*='math-display']", // important!
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
renderHTML: function (_a) {
|
||||||
|
var HTMLAttributes = _a.HTMLAttributes;
|
||||||
|
return [
|
||||||
|
"div",
|
||||||
|
mergeAttributes({ class: "math-display math-node" }, HTMLAttributes),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins: function () {
|
||||||
|
var inputRulePlugin = inputRules({
|
||||||
|
rules: [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, this.type)],
|
||||||
|
});
|
||||||
|
return [inputRulePlugin];
|
||||||
|
},
|
||||||
|
});
|
||||||
3
packages/editor/dist/extensions/math/mathinline.d.ts
vendored
Normal file
3
packages/editor/dist/extensions/math/mathinline.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Node } from "@tiptap/core";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
export declare const MathInline: Node<any, any>;
|
||||||
33
packages/editor/dist/extensions/math/mathinline.js
vendored
Normal file
33
packages/editor/dist/extensions/math/mathinline.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { inputRules } from "prosemirror-inputrules";
|
||||||
|
import { makeInlineMathInputRule, REGEX_INLINE_MATH_DOLLARS, mathPlugin, } from "./plugin";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
export var MathInline = Node.create({
|
||||||
|
name: "math_inline",
|
||||||
|
group: "inline math",
|
||||||
|
content: "text*",
|
||||||
|
inline: true,
|
||||||
|
atom: true,
|
||||||
|
code: true,
|
||||||
|
parseHTML: function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "span[class*='math-inline']", // important!,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
renderHTML: function (_a) {
|
||||||
|
var HTMLAttributes = _a.HTMLAttributes;
|
||||||
|
return [
|
||||||
|
"span",
|
||||||
|
mergeAttributes({ class: "math-inline math-node" }, HTMLAttributes),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins: function () {
|
||||||
|
var inputRulePlugin = inputRules({
|
||||||
|
rules: [makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)],
|
||||||
|
});
|
||||||
|
return [mathPlugin, inputRulePlugin];
|
||||||
|
},
|
||||||
|
});
|
||||||
16
packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.d.ts
vendored
Normal file
16
packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Command } from "prosemirror-state";
|
||||||
|
import { EditorView } from "prosemirror-view";
|
||||||
|
/**
|
||||||
|
* A ProseMirror command for determining whether to exit a math block, based on
|
||||||
|
* specific conditions. Normally called when the user has
|
||||||
|
*
|
||||||
|
* @param outerView The main ProseMirror EditorView containing this math node.
|
||||||
|
* @param dir Used to indicate desired cursor position upon closing a math node.
|
||||||
|
* When set to -1, cursor will be placed BEFORE the math node.
|
||||||
|
* When set to +1, cursor will be placed AFTER the math node.
|
||||||
|
* @param borderMode An exit condition based on cursor position and direction.
|
||||||
|
* @param requireEmptySelection When TRUE, only exit the math node when the
|
||||||
|
* (inner) selection is empty.
|
||||||
|
* @returns A new ProseMirror command based on the input configuration.
|
||||||
|
*/
|
||||||
|
export declare function collapseMathCmd(outerView: EditorView, dir: 1 | -1, requireOnBorder: boolean, requireEmptySelection?: boolean): Command;
|
||||||
51
packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.js
vendored
Normal file
51
packages/editor/dist/extensions/math/plugin/commands/collapse-math-cmd.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { TextSelection } from "prosemirror-state";
|
||||||
|
/**
|
||||||
|
* A ProseMirror command for determining whether to exit a math block, based on
|
||||||
|
* specific conditions. Normally called when the user has
|
||||||
|
*
|
||||||
|
* @param outerView The main ProseMirror EditorView containing this math node.
|
||||||
|
* @param dir Used to indicate desired cursor position upon closing a math node.
|
||||||
|
* When set to -1, cursor will be placed BEFORE the math node.
|
||||||
|
* When set to +1, cursor will be placed AFTER the math node.
|
||||||
|
* @param borderMode An exit condition based on cursor position and direction.
|
||||||
|
* @param requireEmptySelection When TRUE, only exit the math node when the
|
||||||
|
* (inner) selection is empty.
|
||||||
|
* @returns A new ProseMirror command based on the input configuration.
|
||||||
|
*/
|
||||||
|
export function collapseMathCmd(outerView, dir, requireOnBorder, requireEmptySelection) {
|
||||||
|
if (requireEmptySelection === void 0) { requireEmptySelection = true; }
|
||||||
|
// create a new ProseMirror command based on the input conditions
|
||||||
|
return function (innerState, dispatch) {
|
||||||
|
// get selection info
|
||||||
|
var outerState = outerView.state;
|
||||||
|
var _a = outerState.selection, outerTo = _a.to, outerFrom = _a.from;
|
||||||
|
var _b = innerState.selection, innerTo = _b.to, innerFrom = _b.from;
|
||||||
|
// only exit math node when selection is empty
|
||||||
|
if (requireEmptySelection && innerTo !== innerFrom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var currentPos = dir > 0 ? innerTo : innerFrom;
|
||||||
|
// when requireOnBorder is TRUE, collapse only when cursor
|
||||||
|
// is about to leave the bounds of the math node
|
||||||
|
if (requireOnBorder) {
|
||||||
|
// (subtract two from nodeSize to account for start and end tokens)
|
||||||
|
var nodeSize = innerState.doc.nodeSize - 2;
|
||||||
|
// early return if exit conditions not met
|
||||||
|
if (dir > 0 && currentPos < nodeSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dir < 0 && currentPos > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// all exit conditions met, so close the math node by moving the cursor outside
|
||||||
|
if (dispatch) {
|
||||||
|
// set outer selection to be outside of the nodeview
|
||||||
|
var targetPos = dir > 0 ? outerTo : outerFrom;
|
||||||
|
outerView.dispatch(outerState.tr.setSelection(TextSelection.create(outerState.doc, targetPos)));
|
||||||
|
// must return focus to the outer view, otherwise no cursor will appear
|
||||||
|
outerView.focus();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
12
packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.d.ts
vendored
Normal file
12
packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Command } from "prosemirror-state";
|
||||||
|
import { NodeType } from "prosemirror-model";
|
||||||
|
/**
|
||||||
|
* Returns a new command that can be used to inserts a new math node at the
|
||||||
|
* user's current document position, provided that the document schema actually
|
||||||
|
* allows a math node to be placed there.
|
||||||
|
*
|
||||||
|
* @param mathNodeType An instance for either your math_inline or math_display
|
||||||
|
* NodeType. Must belong to the same schema that your EditorState uses!
|
||||||
|
* @param initialText (optional) The initial source content for the math editor.
|
||||||
|
*/
|
||||||
|
export declare function insertMathCmd(mathNodeType: NodeType, initialText?: string): Command;
|
||||||
27
packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.js
vendored
Normal file
27
packages/editor/dist/extensions/math/plugin/commands/insert-math-cmd.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { NodeSelection } from "prosemirror-state";
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/**
|
||||||
|
* Returns a new command that can be used to inserts a new math node at the
|
||||||
|
* user's current document position, provided that the document schema actually
|
||||||
|
* allows a math node to be placed there.
|
||||||
|
*
|
||||||
|
* @param mathNodeType An instance for either your math_inline or math_display
|
||||||
|
* NodeType. Must belong to the same schema that your EditorState uses!
|
||||||
|
* @param initialText (optional) The initial source content for the math editor.
|
||||||
|
*/
|
||||||
|
export function insertMathCmd(mathNodeType, initialText) {
|
||||||
|
if (initialText === void 0) { initialText = ""; }
|
||||||
|
return function (state, dispatch) {
|
||||||
|
var $from = state.selection.$from, index = $from.index();
|
||||||
|
if (!$from.parent.canReplaceWith(index, index, mathNodeType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dispatch) {
|
||||||
|
var mathNode = mathNodeType.create({}, initialText ? state.schema.text(initialText) : null);
|
||||||
|
var tr = state.tr.replaceSelectionWith(mathNode);
|
||||||
|
tr = tr.setSelection(NodeSelection.create(tr.doc, $from.pos));
|
||||||
|
dispatch(tr);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
10
packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.d.ts
vendored
Normal file
10
packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Command } from "prosemirror-state";
|
||||||
|
/**
|
||||||
|
* Some browsers (cough firefox cough) don't properly handle cursor movement on
|
||||||
|
* the edges of a NodeView, so we need to make the desired behavior explicit.
|
||||||
|
*
|
||||||
|
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1252108
|
||||||
|
*/
|
||||||
|
export declare function nudgeCursorCmd(dir: -1 | 0 | 1): Command;
|
||||||
|
export declare const nudgeCursorForwardCmd: Command;
|
||||||
|
export declare const nudgeCursorBackCmd: Command;
|
||||||
25
packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.js
vendored
Normal file
25
packages/editor/dist/extensions/math/plugin/commands/move-cursor-cmd.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { TextSelection } from "prosemirror-state";
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/**
|
||||||
|
* Some browsers (cough firefox cough) don't properly handle cursor movement on
|
||||||
|
* the edges of a NodeView, so we need to make the desired behavior explicit.
|
||||||
|
*
|
||||||
|
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1252108
|
||||||
|
*/
|
||||||
|
export function nudgeCursorCmd(dir) {
|
||||||
|
return function (innerState, dispatch) {
|
||||||
|
var _a = innerState.selection, to = _a.to, from = _a.from;
|
||||||
|
// compute target position
|
||||||
|
var emptySelection = to === from;
|
||||||
|
var currentPos = dir < 0 ? from : to;
|
||||||
|
var increment = emptySelection ? dir : 0;
|
||||||
|
var nodeSize = innerState.doc.nodeSize;
|
||||||
|
var targetPos = Math.max(0, Math.min(nodeSize, currentPos + increment));
|
||||||
|
if (dispatch) {
|
||||||
|
dispatch(innerState.tr.setSelection(TextSelection.create(innerState.doc, targetPos)));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export var nudgeCursorForwardCmd = nudgeCursorCmd(+1);
|
||||||
|
export var nudgeCursorBackCmd = nudgeCursorCmd(-1);
|
||||||
9
packages/editor/dist/extensions/math/plugin/index.d.ts
vendored
Normal file
9
packages/editor/dist/extensions/math/plugin/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { MathView, type ICursorPosObserver } from "./math-node-view";
|
||||||
|
export { mathPlugin, createMathView, type IMathPluginState, } from "./math-plugin";
|
||||||
|
export { mathSchemaSpec, createMathSchema } from "./math-schema";
|
||||||
|
export { mathBackspaceCmd } from "./plugins/math-backspace";
|
||||||
|
export { makeBlockMathInputRule, makeInlineMathInputRule, REGEX_BLOCK_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS_ESCAPED, } from "./plugins/math-input-rules";
|
||||||
|
export { mathSelectPlugin } from "./plugins/math-select";
|
||||||
|
export { insertMathCmd } from "./commands/insert-math-cmd";
|
||||||
|
export { mathSerializer } from "./utils/text-serializer";
|
||||||
|
export * from "./utils/types";
|
||||||
18
packages/editor/dist/extensions/math/plugin/index.js
vendored
Normal file
18
packages/editor/dist/extensions/math/plugin/index.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
// core functionality
|
||||||
|
export { MathView } from "./math-node-view";
|
||||||
|
export { mathPlugin, createMathView, } from "./math-plugin";
|
||||||
|
export { mathSchemaSpec, createMathSchema } from "./math-schema";
|
||||||
|
// recommended plugins
|
||||||
|
export { mathBackspaceCmd } from "./plugins/math-backspace";
|
||||||
|
export { makeBlockMathInputRule, makeInlineMathInputRule, REGEX_BLOCK_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS, REGEX_INLINE_MATH_DOLLARS_ESCAPED, } from "./plugins/math-input-rules";
|
||||||
|
// optional / experimental plugins
|
||||||
|
export { mathSelectPlugin } from "./plugins/math-select";
|
||||||
|
// commands
|
||||||
|
export { insertMathCmd } from "./commands/insert-math-cmd";
|
||||||
|
// utilities
|
||||||
|
export { mathSerializer } from "./utils/text-serializer";
|
||||||
|
export * from "./utils/types";
|
||||||
66
packages/editor/dist/extensions/math/plugin/math-nodeview.d.ts
vendored
Normal file
66
packages/editor/dist/extensions/math/plugin/math-nodeview.d.ts
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Node as ProseNode } from "prosemirror-model";
|
||||||
|
import { EditorState, Transaction, PluginKey } from "prosemirror-state";
|
||||||
|
import { NodeView, EditorView, Decoration, DecorationSource } from "prosemirror-view";
|
||||||
|
import { IMathPluginState } from "./math-plugin";
|
||||||
|
import { MathRenderFn } from "./renderers/types";
|
||||||
|
export interface ICursorPosObserver {
|
||||||
|
/** indicates on which side cursor should appear when this node is selected */
|
||||||
|
cursorSide: "start" | "end";
|
||||||
|
/** */
|
||||||
|
updateCursorPos(state: EditorState): void;
|
||||||
|
}
|
||||||
|
interface IMathViewOptions {
|
||||||
|
/** Dom element name to use for this NodeView */
|
||||||
|
tagName?: string;
|
||||||
|
/** Used to render the Tex input */
|
||||||
|
renderer: MathRenderFn;
|
||||||
|
/** Should be true if node is inline */
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
|
export declare class MathView implements NodeView, ICursorPosObserver {
|
||||||
|
private _node;
|
||||||
|
private _outerView;
|
||||||
|
private _getPos;
|
||||||
|
dom: HTMLElement;
|
||||||
|
private _mathRenderElt;
|
||||||
|
private _mathSrcElt;
|
||||||
|
private _innerView;
|
||||||
|
cursorSide: "start" | "end";
|
||||||
|
private _tagName;
|
||||||
|
private _isEditing;
|
||||||
|
private _mathPluginKey;
|
||||||
|
private options;
|
||||||
|
/**
|
||||||
|
* @param onDestroy Callback for when this NodeView is destroyed.
|
||||||
|
* This NodeView should unregister itself from the list of ICursorPosObservers.
|
||||||
|
*
|
||||||
|
* Math Views support the following options:
|
||||||
|
* @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
|
||||||
|
* @option tagName HTML tag name to use for this NodeView. If none is provided,
|
||||||
|
* will use the node name with underscores converted to hyphens.
|
||||||
|
*/
|
||||||
|
constructor(node: ProseNode, view: EditorView, getPos: () => number, options: IMathViewOptions, mathPluginKey: PluginKey<IMathPluginState>, onDestroy?: () => void);
|
||||||
|
destroy(): void;
|
||||||
|
/**
|
||||||
|
* Ensure focus on the inner editor whenever this node has focus.
|
||||||
|
* This helps to prevent accidental deletions of math blocks.
|
||||||
|
*/
|
||||||
|
ensureFocus(): void;
|
||||||
|
update(node: ProseNode, _decorations: readonly Decoration[], _innerDecorations: DecorationSource): boolean;
|
||||||
|
updateCursorPos(state: EditorState): void;
|
||||||
|
selectNode(): void;
|
||||||
|
deselectNode(): void;
|
||||||
|
stopEvent(event: Event): boolean;
|
||||||
|
ignoreMutation(): boolean;
|
||||||
|
renderMath(): void;
|
||||||
|
dispatchInner(tr: Transaction): void;
|
||||||
|
openEditor(): void;
|
||||||
|
/**
|
||||||
|
* Called when the inner ProseMirror editor should close.
|
||||||
|
*
|
||||||
|
* @param render Optionally update the rendered math after closing. (which
|
||||||
|
* is generally what we want to do, since the user is done editing!)
|
||||||
|
*/
|
||||||
|
closeEditor(render?: boolean): void;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
282
packages/editor/dist/extensions/math/plugin/math-nodeview.js
vendored
Normal file
282
packages/editor/dist/extensions/math/plugin/math-nodeview.js
vendored
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
import { EditorState, TextSelection, } from "prosemirror-state";
|
||||||
|
import { EditorView, } from "prosemirror-view";
|
||||||
|
import { StepMap } from "prosemirror-transform";
|
||||||
|
import { keymap } from "prosemirror-keymap";
|
||||||
|
import { newlineInCode, chainCommands, deleteSelection, } from "prosemirror-commands";
|
||||||
|
import { collapseMathCmd } from "./commands/collapse-math-cmd";
|
||||||
|
var MathView = /** @class */ (function () {
|
||||||
|
// == Lifecycle ===================================== //
|
||||||
|
/**
|
||||||
|
* @param onDestroy Callback for when this NodeView is destroyed.
|
||||||
|
* This NodeView should unregister itself from the list of ICursorPosObservers.
|
||||||
|
*
|
||||||
|
* Math Views support the following options:
|
||||||
|
* @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
|
||||||
|
* @option tagName HTML tag name to use for this NodeView. If none is provided,
|
||||||
|
* will use the node name with underscores converted to hyphens.
|
||||||
|
*/
|
||||||
|
function MathView(node, view, getPos, options, mathPluginKey, onDestroy) {
|
||||||
|
var _this = this;
|
||||||
|
// store arguments
|
||||||
|
this.options = options;
|
||||||
|
this._node = node;
|
||||||
|
this._outerView = view;
|
||||||
|
this._getPos = getPos;
|
||||||
|
this._mathPluginKey = mathPluginKey;
|
||||||
|
// editing state
|
||||||
|
this.cursorSide = "start";
|
||||||
|
this._isEditing = false;
|
||||||
|
// options
|
||||||
|
this._tagName = options.tagName || this._node.type.name.replace("_", "-");
|
||||||
|
// create dom representation of nodeview
|
||||||
|
this.dom = document.createElement(this._tagName);
|
||||||
|
if (options.inline)
|
||||||
|
this.dom.classList.add("math-inline");
|
||||||
|
else
|
||||||
|
this.dom.classList.add("math-display");
|
||||||
|
this.dom.classList.add("math-node");
|
||||||
|
this._mathRenderElt = document.createElement("span");
|
||||||
|
this._mathRenderElt.textContent = "";
|
||||||
|
this._mathRenderElt.classList.add("math-render");
|
||||||
|
this.dom.appendChild(this._mathRenderElt);
|
||||||
|
this._mathSrcElt = document.createElement("span");
|
||||||
|
this._mathSrcElt.classList.add("math-src");
|
||||||
|
this.dom.appendChild(this._mathSrcElt);
|
||||||
|
// ensure
|
||||||
|
this.dom.addEventListener("click", function () { return _this.ensureFocus(); });
|
||||||
|
// render initial content
|
||||||
|
this.renderMath();
|
||||||
|
}
|
||||||
|
MathView.prototype.destroy = function () {
|
||||||
|
// close the inner editor without rendering
|
||||||
|
this.closeEditor(false);
|
||||||
|
// clean up dom elements
|
||||||
|
if (this._mathRenderElt) {
|
||||||
|
this._mathRenderElt.remove();
|
||||||
|
delete this._mathRenderElt;
|
||||||
|
}
|
||||||
|
if (this._mathSrcElt) {
|
||||||
|
this._mathSrcElt.remove();
|
||||||
|
delete this._mathSrcElt;
|
||||||
|
}
|
||||||
|
this.dom.remove();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Ensure focus on the inner editor whenever this node has focus.
|
||||||
|
* This helps to prevent accidental deletions of math blocks.
|
||||||
|
*/
|
||||||
|
MathView.prototype.ensureFocus = function () {
|
||||||
|
if (this._innerView && this._outerView.hasFocus()) {
|
||||||
|
this._innerView.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// == Updates ======================================= //
|
||||||
|
MathView.prototype.update = function (node, _decorations, _innerDecorations) {
|
||||||
|
if (!node.sameMarkup(this._node))
|
||||||
|
return false;
|
||||||
|
this._node = node;
|
||||||
|
if (this._innerView) {
|
||||||
|
var state = this._innerView.state;
|
||||||
|
var start = node.content.findDiffStart(state.doc.content);
|
||||||
|
if (start != null) {
|
||||||
|
var diff = node.content.findDiffEnd(state.doc.content);
|
||||||
|
if (diff) {
|
||||||
|
var endA = diff.a, endB = diff.b;
|
||||||
|
var overlap = start - Math.min(endA, endB);
|
||||||
|
if (overlap > 0) {
|
||||||
|
endA += overlap;
|
||||||
|
endB += overlap;
|
||||||
|
}
|
||||||
|
this._innerView.dispatch(state.tr
|
||||||
|
.replace(start, endB, node.slice(start, endA))
|
||||||
|
.setMeta("fromOutside", true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this._isEditing) {
|
||||||
|
this.renderMath();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
MathView.prototype.updateCursorPos = function (state) {
|
||||||
|
var pos = this._getPos();
|
||||||
|
var size = this._node.nodeSize;
|
||||||
|
var inPmSelection = state.selection.from < pos + size && pos < state.selection.to;
|
||||||
|
if (!inPmSelection) {
|
||||||
|
this.cursorSide = pos < state.selection.from ? "end" : "start";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// == Events ===================================== //
|
||||||
|
MathView.prototype.selectNode = function () {
|
||||||
|
if (!this._outerView.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dom.classList.add("ProseMirror-selectednode");
|
||||||
|
if (!this._isEditing) {
|
||||||
|
this.openEditor();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MathView.prototype.deselectNode = function () {
|
||||||
|
this.dom.classList.remove("ProseMirror-selectednode");
|
||||||
|
if (this._isEditing) {
|
||||||
|
this.closeEditor();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MathView.prototype.stopEvent = function (event) {
|
||||||
|
return (this._innerView !== undefined &&
|
||||||
|
event.target !== undefined &&
|
||||||
|
this._innerView.dom.contains(event.target));
|
||||||
|
};
|
||||||
|
MathView.prototype.ignoreMutation = function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// == Rendering ===================================== //
|
||||||
|
MathView.prototype.renderMath = function () {
|
||||||
|
if (!this._mathRenderElt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// get tex string to render
|
||||||
|
var content = this._node.content.content;
|
||||||
|
var texString = "";
|
||||||
|
if (content.length > 0 && content[0].textContent !== null) {
|
||||||
|
texString = content[0].textContent.trim();
|
||||||
|
}
|
||||||
|
// empty math?
|
||||||
|
if (texString.length < 1) {
|
||||||
|
this.dom.classList.add("empty-math");
|
||||||
|
// clear rendered math, since this node is in an invalid state
|
||||||
|
while (this._mathRenderElt.firstChild) {
|
||||||
|
this._mathRenderElt.firstChild.remove();
|
||||||
|
}
|
||||||
|
// do not render empty math
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.dom.classList.remove("empty-math");
|
||||||
|
}
|
||||||
|
// render katex, but fail gracefully
|
||||||
|
try {
|
||||||
|
this.options.renderer(texString, this._mathRenderElt);
|
||||||
|
this._mathRenderElt.classList.remove("parse-error");
|
||||||
|
this.dom.setAttribute("title", "");
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
console.error(err);
|
||||||
|
this._mathRenderElt.classList.add("parse-error");
|
||||||
|
this.dom.setAttribute("title", err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// == Inner Editor ================================== //
|
||||||
|
MathView.prototype.dispatchInner = function (tr) {
|
||||||
|
if (!this._innerView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var _a = this._innerView.state.applyTransaction(tr), state = _a.state, transactions = _a.transactions;
|
||||||
|
this._innerView.updateState(state);
|
||||||
|
if (!tr.getMeta("fromOutside")) {
|
||||||
|
var outerTr = this._outerView.state.tr, offsetMap = StepMap.offset(this._getPos() + 1);
|
||||||
|
for (var i = 0; i < transactions.length; i++) {
|
||||||
|
var steps = transactions[i].steps;
|
||||||
|
for (var j = 0; j < steps.length; j++) {
|
||||||
|
var mapped = steps[j].map(offsetMap);
|
||||||
|
if (!mapped) {
|
||||||
|
throw Error("step discarded!");
|
||||||
|
}
|
||||||
|
outerTr.step(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outerTr.docChanged)
|
||||||
|
this._outerView.dispatch(outerTr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MathView.prototype.openEditor = function () {
|
||||||
|
var _this = this;
|
||||||
|
var _a;
|
||||||
|
if (this._innerView) {
|
||||||
|
throw Error("inner view should not exist!");
|
||||||
|
}
|
||||||
|
if (!this._mathSrcElt)
|
||||||
|
throw new Error("_mathSrcElt does not exist!");
|
||||||
|
// create a nested ProseMirror view
|
||||||
|
this._innerView = new EditorView(this._mathSrcElt, {
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: this._node,
|
||||||
|
plugins: [
|
||||||
|
keymap({
|
||||||
|
Tab: function (state, dispatch) {
|
||||||
|
if (dispatch) {
|
||||||
|
dispatch(state.tr.insertText("\t"));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
Backspace: chainCommands(deleteSelection, function (state, dispatch, tr_inner) {
|
||||||
|
// default backspace behavior for non-empty selections
|
||||||
|
if (!state.selection.empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// default backspace behavior when math node is non-empty
|
||||||
|
if (_this._node.textContent.length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// otherwise, we want to delete the empty math node and focus the outer view
|
||||||
|
_this._outerView.dispatch(_this._outerView.state.tr.insertText(""));
|
||||||
|
_this._outerView.focus();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
// "Ctrl-Backspace": (state, dispatch, tr_inner) => {
|
||||||
|
// // delete math node and focus the outer view
|
||||||
|
// this._outerView.dispatch(this._outerView.state.tr.insertText(""));
|
||||||
|
// this._outerView.focus();
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
Enter: chainCommands(newlineInCode, collapseMathCmd(this._outerView, +1, false)),
|
||||||
|
"Ctrl-Enter": collapseMathCmd(this._outerView, +1, false),
|
||||||
|
ArrowLeft: collapseMathCmd(this._outerView, -1, true),
|
||||||
|
ArrowRight: collapseMathCmd(this._outerView, +1, true),
|
||||||
|
ArrowUp: collapseMathCmd(this._outerView, -1, true),
|
||||||
|
ArrowDown: collapseMathCmd(this._outerView, +1, true),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
dispatchTransaction: this.dispatchInner.bind(this),
|
||||||
|
});
|
||||||
|
// focus element
|
||||||
|
var innerState = this._innerView.state;
|
||||||
|
this._innerView.focus();
|
||||||
|
// request outer cursor position before math node was selected
|
||||||
|
var maybePos = (_a = this._mathPluginKey.getState(this._outerView.state)) === null || _a === void 0 ? void 0 : _a.prevCursorPos;
|
||||||
|
if (maybePos === null || maybePos === undefined) {
|
||||||
|
console.error("[prosemirror-math] Error: Unable to fetch math plugin state from key.");
|
||||||
|
}
|
||||||
|
var prevCursorPos = maybePos !== null && maybePos !== void 0 ? maybePos : 0;
|
||||||
|
// compute position that cursor should appear within the expanded math node
|
||||||
|
var innerPos = prevCursorPos <= this._getPos() ? 0 : this._node.nodeSize - 2;
|
||||||
|
this._innerView.dispatch(innerState.tr.setSelection(TextSelection.create(innerState.doc, innerPos)));
|
||||||
|
this._isEditing = true;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Called when the inner ProseMirror editor should close.
|
||||||
|
*
|
||||||
|
* @param render Optionally update the rendered math after closing. (which
|
||||||
|
* is generally what we want to do, since the user is done editing!)
|
||||||
|
*/
|
||||||
|
MathView.prototype.closeEditor = function (render) {
|
||||||
|
if (render === void 0) { render = true; }
|
||||||
|
if (this._innerView) {
|
||||||
|
this._innerView.destroy();
|
||||||
|
this._innerView = undefined;
|
||||||
|
}
|
||||||
|
if (render) {
|
||||||
|
this.renderMath();
|
||||||
|
}
|
||||||
|
this._isEditing = false;
|
||||||
|
};
|
||||||
|
return MathView;
|
||||||
|
}());
|
||||||
|
export { MathView };
|
||||||
24
packages/editor/dist/extensions/math/plugin/math-plugin.d.ts
vendored
Normal file
24
packages/editor/dist/extensions/math/plugin/math-plugin.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Node as ProseNode } from "prosemirror-model";
|
||||||
|
import { Plugin as ProsePlugin } from "prosemirror-state";
|
||||||
|
import { MathView } from "./math-node-view";
|
||||||
|
import { EditorView } from "prosemirror-view";
|
||||||
|
export interface IMathPluginState {
|
||||||
|
macros: {
|
||||||
|
[cmd: string]: string;
|
||||||
|
};
|
||||||
|
/** A list of currently active `NodeView`s, in insertion order. */
|
||||||
|
activeNodeViews: MathView[];
|
||||||
|
/**
|
||||||
|
* Used to determine whether to place the cursor in the front- or back-most
|
||||||
|
* position when expanding a math node, without overriding the default arrow
|
||||||
|
* key behavior.
|
||||||
|
*/
|
||||||
|
prevCursorPos: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
|
||||||
|
* @param inline TRUE for block math, FALSE for inline math.
|
||||||
|
* @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
|
||||||
|
*/
|
||||||
|
export declare function createMathView(inline: boolean): (node: ProseNode, view: EditorView, getPos: boolean | (() => number)) => MathView;
|
||||||
|
export declare const mathPlugin: ProsePlugin<IMathPluginState>;
|
||||||
71
packages/editor/dist/extensions/math/plugin/math-plugin.js
vendored
Normal file
71
packages/editor/dist/extensions/math/plugin/math-plugin.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
import { Plugin as ProsePlugin, PluginKey, } from "prosemirror-state";
|
||||||
|
import { MathView } from "./math-node-view";
|
||||||
|
import { KatexRenderer } from "./renderers/katex";
|
||||||
|
// uniquely identifies the prosemirror-math plugin
|
||||||
|
var MATH_PLUGIN_KEY = new PluginKey("prosemirror-math");
|
||||||
|
/**
|
||||||
|
* Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
|
||||||
|
* @param inline TRUE for block math, FALSE for inline math.
|
||||||
|
* @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
|
||||||
|
*/
|
||||||
|
export function createMathView(inline) {
|
||||||
|
return function (node, view, getPos) {
|
||||||
|
/** @todo is this necessary?
|
||||||
|
* Docs says that for any function proprs, the current plugin instance
|
||||||
|
* will be bound to `this`. However, the typings don't reflect this.
|
||||||
|
*/
|
||||||
|
var pluginState = MATH_PLUGIN_KEY.getState(view.state);
|
||||||
|
if (!pluginState) {
|
||||||
|
throw new Error("no math plugin!");
|
||||||
|
}
|
||||||
|
var nodeViews = pluginState.activeNodeViews;
|
||||||
|
// set up NodeView
|
||||||
|
var nodeView = new MathView(node, view, getPos, {
|
||||||
|
inline: inline,
|
||||||
|
renderer: inline ? KatexRenderer.inline : KatexRenderer.block,
|
||||||
|
tagName: inline ? "span" : "div",
|
||||||
|
}, MATH_PLUGIN_KEY, function () {
|
||||||
|
nodeViews.splice(nodeViews.indexOf(nodeView));
|
||||||
|
});
|
||||||
|
nodeViews.push(nodeView);
|
||||||
|
return nodeView;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var mathPluginSpec = {
|
||||||
|
key: MATH_PLUGIN_KEY,
|
||||||
|
state: {
|
||||||
|
init: function (config, instance) {
|
||||||
|
return {
|
||||||
|
macros: {},
|
||||||
|
activeNodeViews: [],
|
||||||
|
prevCursorPos: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
apply: function (tr, value, oldState, newState) {
|
||||||
|
// produce updated state field for this plugin
|
||||||
|
var newPos = newState.selection.from;
|
||||||
|
var oldPos = oldState.selection.from;
|
||||||
|
return {
|
||||||
|
// these values are left unchanged
|
||||||
|
activeNodeViews: value.activeNodeViews,
|
||||||
|
macros: value.macros,
|
||||||
|
// update with the second-most recent cursor pos
|
||||||
|
prevCursorPos: oldPos !== newPos ? oldPos : value.prevCursorPos,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** @todo (8/21/20) implement serialization for math plugin */
|
||||||
|
// toJSON(value) { },
|
||||||
|
// fromJSON(config, value, state){ return {}; }
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
nodeViews: {
|
||||||
|
math_inline: createMathView(true),
|
||||||
|
math_display: createMathView(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export var mathPlugin = new ProsePlugin(mathPluginSpec);
|
||||||
45
packages/editor/dist/extensions/math/plugin/math-schema.d.ts
vendored
Normal file
45
packages/editor/dist/extensions/math/plugin/math-schema.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { MarkSpec, NodeSpec, Schema, SchemaSpec } from "prosemirror-model";
|
||||||
|
import { SchemaSpecMarkT, SchemaSpecNodeT } from "./utils/types";
|
||||||
|
/**
|
||||||
|
* Borrowed from ProseMirror typings, modified to exclude OrderedMaps in spec,
|
||||||
|
* in order to help with the schema-building functions below.
|
||||||
|
*
|
||||||
|
* NOTE: TypeScript's typings for the spread operator { ...a, ...b } are only
|
||||||
|
* an approximation to the true type, and have difficulty with optional fields.
|
||||||
|
* So, unlike the SchemaSpec type, the `marks` field is NOT optional here.
|
||||||
|
*
|
||||||
|
* function example<T extends string>(x: { [name in T]: string; } | null) {
|
||||||
|
* const s = { ...x }; // inferred to have type `{}`.
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @see https://github.com/microsoft/TypeScript/issues/10727
|
||||||
|
*/
|
||||||
|
interface SchemaSpecJson<N extends string = any, M extends string = any> extends SchemaSpec<N, M> {
|
||||||
|
nodes: {
|
||||||
|
[name in N]: NodeSpec;
|
||||||
|
};
|
||||||
|
marks: {
|
||||||
|
[name in M]: MarkSpec;
|
||||||
|
};
|
||||||
|
topNode?: string;
|
||||||
|
}
|
||||||
|
declare type MathSpecNodeT = SchemaSpecNodeT<typeof mathSchemaSpec>;
|
||||||
|
declare type MathSpecMarkT = SchemaSpecMarkT<typeof mathSchemaSpec>;
|
||||||
|
export declare const mathSchemaSpec: SchemaSpecJson<"math_inline" | "paragraph" | "text" | "doc" | "math_display", "math_select">;
|
||||||
|
/**
|
||||||
|
* Use the prosemirror-math default SchemaSpec to create a new Schema.
|
||||||
|
*/
|
||||||
|
export declare function createMathSchema(): Schema<"math_inline" | "paragraph" | "text" | "doc" | "math_display", "math_select">;
|
||||||
|
/**
|
||||||
|
* Create a new SchemaSpec by adding math nodes to an existing spec.
|
||||||
|
|
||||||
|
* @deprecated This function is included for demonstration/testing only. For the
|
||||||
|
* time being, I highly recommend adding the math nodes manually to your own
|
||||||
|
* ProseMirror spec to avoid unexpected interactions between the math nodes
|
||||||
|
* and your own spec. Use the example spec for reference.
|
||||||
|
*
|
||||||
|
* @param baseSpec The SchemaSpec to extend. Must specify a `marks` field, and
|
||||||
|
* must be a raw object (not an OrderedMap).
|
||||||
|
*/
|
||||||
|
export declare function extendMathSchemaSpec<N extends string, M extends string>(baseSpec: SchemaSpecJson<N, M>): SchemaSpecJson<N | MathSpecNodeT, M | MathSpecMarkT>;
|
||||||
|
export {};
|
||||||
114
packages/editor/dist/extensions/math/plugin/math-schema.js
vendored
Normal file
114
packages/editor/dist/extensions/math/plugin/math-schema.js
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __read = (this && this.__read) || function (o, n) {
|
||||||
|
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
||||||
|
if (!m) return o;
|
||||||
|
var i = m.call(o), r, ar = [], e;
|
||||||
|
try {
|
||||||
|
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
||||||
|
}
|
||||||
|
catch (error) { e = { error: error }; }
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
if (r && !r.done && (m = i["return"])) m.call(i);
|
||||||
|
}
|
||||||
|
finally { if (e) throw e.error; }
|
||||||
|
}
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
||||||
|
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||||
|
if (ar || !(i in from)) {
|
||||||
|
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||||
|
ar[i] = from[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to.concat(ar || Array.prototype.slice.call(from));
|
||||||
|
};
|
||||||
|
// prosemirror imports
|
||||||
|
import { Schema, } from "prosemirror-model";
|
||||||
|
import { defaultBlockMathParseRules, defaultInlineMathParseRules, } from "./plugins/math-paste-rules";
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// force typescript to infer generic type arguments for SchemaSpec
|
||||||
|
function createSchemaSpec(spec) {
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
// bare minimum ProseMirror schema for working with math nodes
|
||||||
|
export var mathSchemaSpec = createSchemaSpec({
|
||||||
|
nodes: {
|
||||||
|
// :: NodeSpec top-level document node
|
||||||
|
doc: {
|
||||||
|
content: "block+",
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
content: "inline*",
|
||||||
|
group: "block",
|
||||||
|
parseDOM: [{ tag: "p" }],
|
||||||
|
toDOM: function () {
|
||||||
|
return ["p", 0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
math_inline: {
|
||||||
|
group: "inline math",
|
||||||
|
content: "text*",
|
||||||
|
inline: true,
|
||||||
|
atom: true,
|
||||||
|
toDOM: function () { return ["math-inline", { class: "math-node" }, 0]; },
|
||||||
|
parseDOM: __spreadArray([{ tag: "math-inline" }], __read(defaultInlineMathParseRules), false),
|
||||||
|
},
|
||||||
|
math_display: {
|
||||||
|
group: "block math",
|
||||||
|
content: "text*",
|
||||||
|
atom: true,
|
||||||
|
code: true,
|
||||||
|
toDOM: function () { return ["math-display", { class: "math-node" }, 0]; },
|
||||||
|
parseDOM: __spreadArray([{ tag: "math-display" }], __read(defaultBlockMathParseRules), false),
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
group: "inline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marks: {
|
||||||
|
math_select: {
|
||||||
|
toDOM: function () {
|
||||||
|
return ["math-select", 0];
|
||||||
|
},
|
||||||
|
parseDOM: [{ tag: "math-select" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Use the prosemirror-math default SchemaSpec to create a new Schema.
|
||||||
|
*/
|
||||||
|
export function createMathSchema() {
|
||||||
|
return new Schema(mathSchemaSpec);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create a new SchemaSpec by adding math nodes to an existing spec.
|
||||||
|
|
||||||
|
* @deprecated This function is included for demonstration/testing only. For the
|
||||||
|
* time being, I highly recommend adding the math nodes manually to your own
|
||||||
|
* ProseMirror spec to avoid unexpected interactions between the math nodes
|
||||||
|
* and your own spec. Use the example spec for reference.
|
||||||
|
*
|
||||||
|
* @param baseSpec The SchemaSpec to extend. Must specify a `marks` field, and
|
||||||
|
* must be a raw object (not an OrderedMap).
|
||||||
|
*/
|
||||||
|
export function extendMathSchemaSpec(baseSpec) {
|
||||||
|
var nodes = __assign(__assign({}, baseSpec.nodes), mathSchemaSpec.nodes);
|
||||||
|
var marks = __assign(__assign({}, baseSpec.marks), mathSchemaSpec.marks);
|
||||||
|
return { nodes: nodes, marks: marks, topNode: baseSpec.topNode };
|
||||||
|
}
|
||||||
2
packages/editor/dist/extensions/math/plugin/plugins/math-backspace.d.ts
vendored
Normal file
2
packages/editor/dist/extensions/math/plugin/plugins/math-backspace.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { Command as ProseCommand } from "prosemirror-state";
|
||||||
|
export declare const mathBackspaceCmd: ProseCommand;
|
||||||
25
packages/editor/dist/extensions/math/plugin/plugins/math-backspace.js
vendored
Normal file
25
packages/editor/dist/extensions/math/plugin/plugins/math-backspace.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NodeSelection } from "prosemirror-state";
|
||||||
|
export var mathBackspaceCmd = function (state, dispatch) {
|
||||||
|
// check node before
|
||||||
|
var $from = state.selection.$from;
|
||||||
|
var nodeBefore = $from.nodeBefore;
|
||||||
|
if (!nodeBefore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (nodeBefore.type.name == "math_inline") {
|
||||||
|
// select math node
|
||||||
|
var index = $from.index($from.depth);
|
||||||
|
var $beforePos = state.doc.resolve($from.posAtIndex(index - 1));
|
||||||
|
if (dispatch) {
|
||||||
|
dispatch(state.tr.setSelection(new NodeSelection($beforePos)));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (nodeBefore.type.name == "math_block") {
|
||||||
|
/** @todo (8/1/20) implement backspace for math blocks
|
||||||
|
* check how code blocks behave when pressing backspace
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
7
packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.d.ts
vendored
Normal file
7
packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { InputRule } from "prosemirror-inputrules";
|
||||||
|
import { NodeType } from "prosemirror-model";
|
||||||
|
export declare const REGEX_INLINE_MATH_DOLLARS: RegExp;
|
||||||
|
export declare const REGEX_INLINE_MATH_DOLLARS_ESCAPED: RegExp;
|
||||||
|
export declare const REGEX_BLOCK_MATH_DOLLARS: RegExp;
|
||||||
|
export declare function makeInlineMathInputRule(pattern: RegExp, nodeType: NodeType, getAttrs?: (match: string[]) => any): InputRule;
|
||||||
|
export declare function makeBlockMathInputRule(pattern: RegExp, nodeType: NodeType, getAttrs?: (match: string[]) => any): InputRule;
|
||||||
55
packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.js
vendored
Normal file
55
packages/editor/dist/extensions/math/plugin/plugins/math-inputrules.js
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
import { InputRule } from "prosemirror-inputrules";
|
||||||
|
import { NodeSelection } from "prosemirror-state";
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// ---- Inline Input Rules ------------------------------ //
|
||||||
|
// simple input rule for inline math
|
||||||
|
export var REGEX_INLINE_MATH_DOLLARS = /\$\$(.+)\$\$/; //new RegExp("\$(.+)\$", "i");
|
||||||
|
// negative lookbehind regex notation allows for escaped \$ delimiters
|
||||||
|
// (requires browser supporting ECMA2018 standard -- currently only Chrome / FF)
|
||||||
|
// (see https://javascript.info/regexp-lookahead-lookbehind)
|
||||||
|
export var REGEX_INLINE_MATH_DOLLARS_ESCAPED = (function () {
|
||||||
|
// attempt to create regex with negative lookbehind
|
||||||
|
try {
|
||||||
|
return new RegExp("(?<!\\\\)\\$(.+)(?<!\\\\)\\$");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return REGEX_INLINE_MATH_DOLLARS;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// ---- Block Input Rules ------------------------------- //
|
||||||
|
// simple inputrule for block math
|
||||||
|
export var REGEX_BLOCK_MATH_DOLLARS = /\$\$\$\s+$/; //new RegExp("\$\$\s+$", "i");
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
export function makeInlineMathInputRule(pattern, nodeType, getAttrs) {
|
||||||
|
return new InputRule(pattern, function (state, match, start, end) {
|
||||||
|
var $start = state.doc.resolve(start);
|
||||||
|
var index = $start.index();
|
||||||
|
var $end = state.doc.resolve(end);
|
||||||
|
// get attrs
|
||||||
|
var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
|
||||||
|
// check if replacement valid
|
||||||
|
if (!$start.parent.canReplaceWith(index, $end.index(), nodeType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// perform replacement
|
||||||
|
return state.tr.replaceRangeWith(start, end, nodeType.create(attrs, nodeType.schema.text(match[1])));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function makeBlockMathInputRule(pattern, nodeType, getAttrs) {
|
||||||
|
return new InputRule(pattern, function (state, match, start, end) {
|
||||||
|
var $start = state.doc.resolve(start);
|
||||||
|
var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
|
||||||
|
if (!$start
|
||||||
|
.node(-1)
|
||||||
|
.canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType))
|
||||||
|
return null;
|
||||||
|
var tr = state.tr
|
||||||
|
.delete(start, end)
|
||||||
|
.setBlockType(start, start, nodeType, attrs);
|
||||||
|
return tr.setSelection(NodeSelection.create(tr.doc, tr.mapping.map($start.pos - 1)));
|
||||||
|
});
|
||||||
|
}
|
||||||
56
packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.d.ts
vendored
Normal file
56
packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Note that for some of the `ParseRule`s defined below,
|
||||||
|
* we define a `getAttrs` function, which, other than
|
||||||
|
* defining node attributes, can be used to describe complex
|
||||||
|
* match conditions for a rule.
|
||||||
|
|
||||||
|
* Returning `false` from `ParseRule.getAttrs` prevents the
|
||||||
|
* rule from matching, while returning `null` indicates that
|
||||||
|
* the default set of note attributes should be used.
|
||||||
|
*/
|
||||||
|
import { ParseRule } from "prosemirror-model";
|
||||||
|
/**
|
||||||
|
* Wikipedia formats block math inside a <dl>...</dl> element, as below.
|
||||||
|
*
|
||||||
|
* - Evidently no CSS class is used to distinguish inline vs block math
|
||||||
|
* - Sometimes the `\displaystyle` TeX command is present even in inline math
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <dl><dd><span class="mwe-math-element">
|
||||||
|
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
* <semantics>
|
||||||
|
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
|
||||||
|
* <annotation encoding="application/x-tex">...</annotation>
|
||||||
|
* </semantics>
|
||||||
|
* </math>
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
* </span>
|
||||||
|
* </span></dd></dl>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export declare const wikipediaBlockMathParseRule: ParseRule;
|
||||||
|
/**
|
||||||
|
* Parse rule for inline math content on Wikipedia of the following form:
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <span class="mwe-math-element">
|
||||||
|
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
* <semantics>
|
||||||
|
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
|
||||||
|
* <annotation encoding="application/x-tex">...</annotation>
|
||||||
|
* </semantics>
|
||||||
|
* </math>
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
* </span>
|
||||||
|
* </span>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export declare const wikipediaInlineMathParseRule: ParseRule;
|
||||||
|
export declare const defaultInlineMathParseRules: ParseRule[];
|
||||||
|
export declare const defaultBlockMathParseRules: ParseRule[];
|
||||||
193
packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.js
vendored
Normal file
193
packages/editor/dist/extensions/math/plugin/plugins/math-paste-rules.js
vendored
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* Note that for some of the `ParseRule`s defined below,
|
||||||
|
* we define a `getAttrs` function, which, other than
|
||||||
|
* defining node attributes, can be used to describe complex
|
||||||
|
* match conditions for a rule.
|
||||||
|
|
||||||
|
* Returning `false` from `ParseRule.getAttrs` prevents the
|
||||||
|
* rule from matching, while returning `null` indicates that
|
||||||
|
* the default set of note attributes should be used.
|
||||||
|
*/
|
||||||
|
var __values = (this && this.__values) || function(o) {
|
||||||
|
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
||||||
|
if (m) return m.call(o);
|
||||||
|
if (o && typeof o.length === "number") return {
|
||||||
|
next: function () {
|
||||||
|
if (o && i >= o.length) o = void 0;
|
||||||
|
return { value: o && o[i++], done: !o };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
||||||
|
};
|
||||||
|
import { Fragment, } from "prosemirror-model";
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
function getFirstMatch(root, rules) {
|
||||||
|
var e_1, _a;
|
||||||
|
try {
|
||||||
|
for (var rules_1 = __values(rules), rules_1_1 = rules_1.next(); !rules_1_1.done; rules_1_1 = rules_1.next()) {
|
||||||
|
var rule = rules_1_1.value;
|
||||||
|
var match = rule(root);
|
||||||
|
if (match !== false) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
if (rules_1_1 && !rules_1_1.done && (_a = rules_1.return)) _a.call(rules_1);
|
||||||
|
}
|
||||||
|
finally { if (e_1) throw e_1.error; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function makeTextFragment(text, schema) {
|
||||||
|
return Fragment.from(schema.text(text));
|
||||||
|
}
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// -- Wikipedia ----------------------------------------- //
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
*/
|
||||||
|
function texFromMediaWikiFallbackImage(root) {
|
||||||
|
var _a;
|
||||||
|
var match = root.querySelector("img.mwe-math-fallback-image-inline[alt]");
|
||||||
|
return (_a = match === null || match === void 0 ? void 0 : match.getAttribute("alt")) !== null && _a !== void 0 ? _a : false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
*/
|
||||||
|
function texFromMathML_01(root) {
|
||||||
|
var _a;
|
||||||
|
var match = root.querySelector("math[alttext]");
|
||||||
|
return (_a = match === null || match === void 0 ? void 0 : match.getAttribute("alttext")) !== null && _a !== void 0 ? _a : false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
*/
|
||||||
|
function texFromMathML_02(root) {
|
||||||
|
var _a;
|
||||||
|
var match = root.querySelector("math annotation[encoding='application/x-tex'");
|
||||||
|
return (_a = match === null || match === void 0 ? void 0 : match.textContent) !== null && _a !== void 0 ? _a : false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <script type="math/tex"></script>
|
||||||
|
*/
|
||||||
|
function texFromScriptTag(root) {
|
||||||
|
var _a;
|
||||||
|
var match = root.querySelector("script[type*='math/tex']");
|
||||||
|
return (_a = match === null || match === void 0 ? void 0 : match.textContent) !== null && _a !== void 0 ? _a : false;
|
||||||
|
}
|
||||||
|
function matchWikipedia(root) {
|
||||||
|
var match = getFirstMatch(root, [
|
||||||
|
texFromMediaWikiFallbackImage,
|
||||||
|
texFromMathML_01,
|
||||||
|
texFromMathML_02,
|
||||||
|
]);
|
||||||
|
// TODO: if no tex string was found, but we have MathML, try to parse it
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wikipedia formats block math inside a <dl>...</dl> element, as below.
|
||||||
|
*
|
||||||
|
* - Evidently no CSS class is used to distinguish inline vs block math
|
||||||
|
* - Sometimes the `\displaystyle` TeX command is present even in inline math
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <dl><dd><span class="mwe-math-element">
|
||||||
|
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
* <semantics>
|
||||||
|
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
|
||||||
|
* <annotation encoding="application/x-tex">...</annotation>
|
||||||
|
* </semantics>
|
||||||
|
* </math>
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
* </span>
|
||||||
|
* </span></dd></dl>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export var wikipediaBlockMathParseRule = {
|
||||||
|
tag: "dl",
|
||||||
|
getAttrs: function (p) {
|
||||||
|
var dl = p;
|
||||||
|
// <dl> must contain exactly one child
|
||||||
|
if (dl.childElementCount !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var dd = dl.firstChild;
|
||||||
|
if (dd.tagName !== "DD") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// <dd> must contain exactly one child
|
||||||
|
if (dd.childElementCount !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var mweElt = dd.firstChild;
|
||||||
|
if (!mweElt.classList.contains("mwe-math-element")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// success! proceed to `getContent` for further processing
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getContent: function (p, schema) {
|
||||||
|
// search the matched element for a TeX string
|
||||||
|
var match = matchWikipedia(p);
|
||||||
|
// return a fragment representing the math node's children
|
||||||
|
var texString = match || "\\text{\\color{red}(paste error)}";
|
||||||
|
return makeTextFragment(texString, schema);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Parse rule for inline math content on Wikipedia of the following form:
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <span class="mwe-math-element">
|
||||||
|
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
* <semantics>
|
||||||
|
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
|
||||||
|
* <annotation encoding="application/x-tex">...</annotation>
|
||||||
|
* </semantics>
|
||||||
|
* </math>
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
* </span>
|
||||||
|
* </span>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export var wikipediaInlineMathParseRule = {
|
||||||
|
tag: "span",
|
||||||
|
getAttrs: function (p) {
|
||||||
|
var span = p;
|
||||||
|
if (!span.classList.contains("mwe-math-element")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// success! proceed to `getContent` for further processing
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getContent: function (p, schema) {
|
||||||
|
// search the matched element for a TeX string
|
||||||
|
var match = matchWikipedia(p);
|
||||||
|
// return a fragment representing the math node's children
|
||||||
|
var texString = match || "\\text{\\color{red}(paste error)}";
|
||||||
|
return makeTextFragment(texString, schema);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// -- MathJax ------------------------------------------- //
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
export var defaultInlineMathParseRules = [
|
||||||
|
wikipediaInlineMathParseRule,
|
||||||
|
];
|
||||||
|
export var defaultBlockMathParseRules = [
|
||||||
|
wikipediaBlockMathParseRule,
|
||||||
|
];
|
||||||
12
packages/editor/dist/extensions/math/plugin/plugins/math-select.d.ts
vendored
Normal file
12
packages/editor/dist/extensions/math/plugin/plugins/math-select.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Plugin as ProsePlugin } from "prosemirror-state";
|
||||||
|
/**
|
||||||
|
* Due to the internals of KaTeX, by default, selecting rendered
|
||||||
|
* math will put a box around each individual character of a
|
||||||
|
* math expression. This plugin attempts to make math selections
|
||||||
|
* slightly prettier by instead setting a background color on the node.
|
||||||
|
*
|
||||||
|
* (remember to use the included math.css!)
|
||||||
|
*
|
||||||
|
* @todo (6/13/20) math selection rectangles are not quite even with text
|
||||||
|
*/
|
||||||
|
export declare const mathSelectPlugin: ProsePlugin;
|
||||||
65
packages/editor/dist/extensions/math/plugin/plugins/math-select.js
vendored
Normal file
65
packages/editor/dist/extensions/math/plugin/plugins/math-select.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
// prosemirror imports
|
||||||
|
import { Plugin as ProsePlugin, } from "prosemirror-state";
|
||||||
|
import { DecorationSet, Decoration } from "prosemirror-view";
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
/**
|
||||||
|
* Uses the selection to determine which math_select decorations
|
||||||
|
* should be applied to the given document.
|
||||||
|
* @param arg Should be either a Transaction or an EditorState,
|
||||||
|
* although any object with `selection` and `doc` will work.
|
||||||
|
*/
|
||||||
|
var checkSelection = function (arg) {
|
||||||
|
var _a = arg.selection, from = _a.from, to = _a.to;
|
||||||
|
var content = arg.selection.content().content;
|
||||||
|
var result = [];
|
||||||
|
content.descendants(function (node, pos, parent) {
|
||||||
|
if (node.type.name == "text") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.type.name.startsWith("math_")) {
|
||||||
|
result.push({
|
||||||
|
start: Math.max(from + pos - 1, 0),
|
||||||
|
end: from + pos + node.nodeSize - 1,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return DecorationSet.create(arg.doc, result.map(function (_a) {
|
||||||
|
var start = _a.start, end = _a.end;
|
||||||
|
return Decoration.node(start, end, { class: "math-select" });
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Due to the internals of KaTeX, by default, selecting rendered
|
||||||
|
* math will put a box around each individual character of a
|
||||||
|
* math expression. This plugin attempts to make math selections
|
||||||
|
* slightly prettier by instead setting a background color on the node.
|
||||||
|
*
|
||||||
|
* (remember to use the included math.css!)
|
||||||
|
*
|
||||||
|
* @todo (6/13/20) math selection rectangles are not quite even with text
|
||||||
|
*/
|
||||||
|
export var mathSelectPlugin = new ProsePlugin({
|
||||||
|
state: {
|
||||||
|
init: function (config, partialState) {
|
||||||
|
return checkSelection(partialState);
|
||||||
|
},
|
||||||
|
apply: function (tr, value, oldState, newState) {
|
||||||
|
if (!tr.selection || !tr.selectionSet) {
|
||||||
|
return oldState;
|
||||||
|
}
|
||||||
|
var sel = checkSelection(tr);
|
||||||
|
return sel;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations: function (state) {
|
||||||
|
return mathSelectPlugin.getState(state);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
3
packages/editor/dist/extensions/math/plugin/renderers/katex.d.ts
vendored
Normal file
3
packages/editor/dist/extensions/math/plugin/renderers/katex.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { MathRenderer } from "./types";
|
||||||
|
import "katex/contrib/mhchem/mhchem";
|
||||||
|
export declare const KatexRenderer: MathRenderer;
|
||||||
19
packages/editor/dist/extensions/math/plugin/renderers/katex.js
vendored
Normal file
19
packages/editor/dist/extensions/math/plugin/renderers/katex.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import katex from "katex";
|
||||||
|
// Chemistry formulas support
|
||||||
|
import "katex/contrib/mhchem/mhchem";
|
||||||
|
export var KatexRenderer = {
|
||||||
|
inline: function (text, element) {
|
||||||
|
katex.render(text, element, {
|
||||||
|
displayMode: false,
|
||||||
|
globalGroup: true,
|
||||||
|
throwOnError: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
block: function (text, element) {
|
||||||
|
katex.render(text, element, {
|
||||||
|
displayMode: true,
|
||||||
|
globalGroup: true,
|
||||||
|
throwOnError: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
5
packages/editor/dist/extensions/math/plugin/renderers/types.d.ts
vendored
Normal file
5
packages/editor/dist/extensions/math/plugin/renderers/types.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export declare type MathRenderFn = (text: string, element: HTMLElement) => void;
|
||||||
|
export declare type MathRenderer = {
|
||||||
|
inline: MathRenderFn;
|
||||||
|
block: MathRenderFn;
|
||||||
|
};
|
||||||
1
packages/editor/dist/extensions/math/plugin/renderers/types.js
vendored
Normal file
1
packages/editor/dist/extensions/math/plugin/renderers/types.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
34
packages/editor/dist/extensions/math/plugin/utils/text-serializer.d.ts
vendored
Normal file
34
packages/editor/dist/extensions/math/plugin/utils/text-serializer.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Node as ProseNode, Mark, Slice, NodeType, MarkType, Fragment } from "prosemirror-model";
|
||||||
|
declare type TypedNode<T extends string> = ProseNode & {
|
||||||
|
type: NodeType & {
|
||||||
|
name: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
declare type TypedMark<T extends string> = Mark & {
|
||||||
|
type: MarkType & {
|
||||||
|
name: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
declare type NodeSerializer<T extends string> = (node: TypedNode<T>) => string;
|
||||||
|
declare type MarkSerializer<T extends string> = (mark: TypedMark<T>) => string;
|
||||||
|
declare class ProseMirrorTextSerializer {
|
||||||
|
nodes: {
|
||||||
|
[name: string]: NodeSerializer<string> | undefined;
|
||||||
|
};
|
||||||
|
marks: {
|
||||||
|
[name: string]: MarkSerializer<string> | undefined;
|
||||||
|
};
|
||||||
|
constructor(fns: {
|
||||||
|
nodes?: {
|
||||||
|
[name: string]: NodeSerializer<string> | undefined;
|
||||||
|
};
|
||||||
|
marks?: {
|
||||||
|
[name: string]: MarkSerializer<string> | undefined;
|
||||||
|
};
|
||||||
|
}, base?: ProseMirrorTextSerializer);
|
||||||
|
serializeFragment(fragment: Fragment): string;
|
||||||
|
serializeSlice(slice: Slice): string;
|
||||||
|
serializeNode(node: ProseNode): string | null;
|
||||||
|
}
|
||||||
|
export declare const mathSerializer: ProseMirrorTextSerializer;
|
||||||
|
export {};
|
||||||
71
packages/editor/dist/extensions/math/plugin/utils/text-serializer.js
vendored
Normal file
71
packages/editor/dist/extensions/math/plugin/utils/text-serializer.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var ProseMirrorTextSerializer = /** @class */ (function () {
|
||||||
|
function ProseMirrorTextSerializer(fns, base) {
|
||||||
|
// use base serializer as a fallback
|
||||||
|
this.nodes = __assign(__assign({}, base === null || base === void 0 ? void 0 : base.nodes), fns.nodes);
|
||||||
|
this.marks = __assign(__assign({}, base === null || base === void 0 ? void 0 : base.marks), fns.marks);
|
||||||
|
}
|
||||||
|
ProseMirrorTextSerializer.prototype.serializeFragment = function (fragment) {
|
||||||
|
var _this = this;
|
||||||
|
// adapted from the undocumented `Fragment.textBetween` function
|
||||||
|
// https://github.com/ProseMirror/prosemirror-model/blob/eef20c8c6dbf841b1d70859df5d59c21b5108a4f/src/fragment.js#L46
|
||||||
|
var blockSeparator = "\n\n";
|
||||||
|
var leafText = undefined;
|
||||||
|
var text = "";
|
||||||
|
var separated = true;
|
||||||
|
var from = 0;
|
||||||
|
var to = fragment.size;
|
||||||
|
fragment.nodesBetween(from, to, function (node, pos) {
|
||||||
|
var _a;
|
||||||
|
// check if one of our custom serializers handles this node
|
||||||
|
var serialized = _this.serializeNode(node);
|
||||||
|
if (serialized !== null) {
|
||||||
|
text += serialized;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.isText) {
|
||||||
|
text += ((_a = node.text) === null || _a === void 0 ? void 0 : _a.slice(Math.max(from, pos) - pos, to - pos)) || "";
|
||||||
|
separated = !blockSeparator;
|
||||||
|
}
|
||||||
|
else if (node.isLeaf && leafText) {
|
||||||
|
text += leafText;
|
||||||
|
separated = !blockSeparator;
|
||||||
|
}
|
||||||
|
else if (!separated && node.isBlock) {
|
||||||
|
text += blockSeparator;
|
||||||
|
separated = true;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
ProseMirrorTextSerializer.prototype.serializeSlice = function (slice) {
|
||||||
|
return this.serializeFragment(slice.content);
|
||||||
|
};
|
||||||
|
ProseMirrorTextSerializer.prototype.serializeNode = function (node) {
|
||||||
|
// check if one of our custom serializers handles this node
|
||||||
|
var nodeSerializer = this.nodes[node.type.name];
|
||||||
|
if (nodeSerializer !== undefined) {
|
||||||
|
return nodeSerializer(node);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return ProseMirrorTextSerializer;
|
||||||
|
}());
|
||||||
|
export var mathSerializer = new ProseMirrorTextSerializer({
|
||||||
|
nodes: {
|
||||||
|
math_inline: function (node) { return "$".concat(node.textContent, "$"); },
|
||||||
|
math_display: function (node) { return "\n\n$$\n".concat(node.textContent, "\n$$"); },
|
||||||
|
},
|
||||||
|
});
|
||||||
5
packages/editor/dist/extensions/math/plugin/utils/types.d.ts
vendored
Normal file
5
packages/editor/dist/extensions/math/plugin/utils/types.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Schema, SchemaSpec } from "prosemirror-model";
|
||||||
|
export declare type SchemaSpecNodeT<Spec> = Spec extends SchemaSpec<infer N, infer _> ? N : never;
|
||||||
|
export declare type SchemaSpecMarkT<Spec> = Spec extends SchemaSpec<infer _, infer M> ? M : never;
|
||||||
|
export declare type SchemaNodeT<S> = S extends Schema<infer N, infer _> ? N : never;
|
||||||
|
export declare type SchemaMarkT<S> = S extends Schema<infer _, infer M> ? M : never;
|
||||||
1
packages/editor/dist/extensions/math/plugin/utils/types.js
vendored
Normal file
1
packages/editor/dist/extensions/math/plugin/utils/types.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
5
packages/editor/dist/index.js
vendored
5
packages/editor/dist/index.js
vendored
@@ -53,6 +53,8 @@ import { EmbedNode } from "./extensions/embed";
|
|||||||
import { CodeBlock } from "./extensions/code-block";
|
import { CodeBlock } from "./extensions/code-block";
|
||||||
import { ListItem } from "./extensions/list-item";
|
import { ListItem } from "./extensions/list-item";
|
||||||
import { Link } from "@tiptap/extension-link";
|
import { Link } from "@tiptap/extension-link";
|
||||||
|
import { Codemark } from "./extensions/code-mark";
|
||||||
|
import { MathInline, MathBlock } from "./extensions/math";
|
||||||
import { NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react";
|
import { NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react";
|
||||||
import { OutlineList } from "./extensions/outline-list";
|
import { OutlineList } from "./extensions/outline-list";
|
||||||
import { OutlineListItem } from "./extensions/outline-list-item";
|
import { OutlineListItem } from "./extensions/outline-list-item";
|
||||||
@@ -130,6 +132,9 @@ var useTiptap = function (options, deps) {
|
|||||||
OutlineListItem,
|
OutlineListItem,
|
||||||
OutlineList,
|
OutlineList,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
Codemark,
|
||||||
|
MathInline,
|
||||||
|
MathBlock,
|
||||||
],
|
],
|
||||||
onBeforeCreate: function (_a) {
|
onBeforeCreate: function (_a) {
|
||||||
var editor = _a.editor;
|
var editor = _a.editor;
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export declare type ToolButtonProps = ButtonProps & {
|
|||||||
};
|
};
|
||||||
export declare const ToolButton: React.NamedExoticComponent<ButtonProps & {
|
export declare const ToolButton: React.NamedExoticComponent<ButtonProps & {
|
||||||
icon: IconNames;
|
icon: IconNames;
|
||||||
iconColor?: "background" | "border" | "text" | "blue" | "gray" | "green" | "orange" | "purple" | "red" | "yellow" | "checked" | "disabled" | "placeholder" | "icon" | "overlay" | "hover" | keyof import("@notesnook/theme/dist/theme/colorscheme/static").StaticColors | "primary" | "bgTransparent" | "accent" | "bgSecondary" | "bgSecondaryText" | "fontSecondary" | "fontTertiary" | "secondary" | undefined;
|
iconColor?: "background" | "border" | "text" | "blue" | "gray" | "green" | "orange" | "purple" | "red" | "yellow" | "checked" | "disabled" | "placeholder" | "icon" | keyof import("@notesnook/theme/dist/theme/colorscheme/static").StaticColors | "primary" | "bgTransparent" | "accent" | "bgSecondary" | "bgSecondaryText" | "hover" | "fontSecondary" | "fontTertiary" | "overlay" | "secondary" | undefined;
|
||||||
iconSize?: number | "small" | "big" | "medium" | undefined;
|
iconSize?: number | "big" | "small" | "medium" | undefined;
|
||||||
toggled: boolean;
|
toggled: boolean;
|
||||||
buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined> | undefined;
|
buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined> | undefined;
|
||||||
variant?: ToolButtonVariant | undefined;
|
variant?: ToolButtonVariant | undefined;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/// <reference types="react" />
|
|
||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from "@tiptap/core";
|
||||||
import { MenuButton } from "../../components/menu/types";
|
import { MenuButton } from "../../components/menu/types";
|
||||||
import { ToolProps } from "../types";
|
import { ToolProps } from "../types";
|
||||||
|
|||||||
130
packages/editor/package-lock.json
generated
130
packages/editor/package-lock.json
generated
@@ -37,8 +37,10 @@
|
|||||||
"@tiptap/starter-kit": "^2.0.0-beta.185",
|
"@tiptap/starter-kit": "^2.0.0-beta.185",
|
||||||
"detect-indent": "^7.0.0",
|
"detect-indent": "^7.0.0",
|
||||||
"emotion-theming": "^10.0.19",
|
"emotion-theming": "^10.0.19",
|
||||||
|
"katex": "^0.13.24",
|
||||||
"lowlight": "^2.6.1",
|
"lowlight": "^2.6.1",
|
||||||
"prism-themes": "^1.9.0",
|
"prism-themes": "^1.9.0",
|
||||||
|
"prosemirror-codemark": "^0.4.0",
|
||||||
"prosemirror-tables": "^1.1.1",
|
"prosemirror-tables": "^1.1.1",
|
||||||
"prosemirror-utils": "github:atlassian/prosemirror-utils",
|
"prosemirror-utils": "github:atlassian/prosemirror-utils",
|
||||||
"prosemirror-view": "^1.24.1",
|
"prosemirror-view": "^1.24.1",
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/esm": "^3.2.0",
|
"@types/esm": "^3.2.0",
|
||||||
|
"@types/katex": "^0.14.0",
|
||||||
"@types/node": "^16.11.11",
|
"@types/node": "^16.11.11",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
@@ -4564,6 +4567,12 @@
|
|||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/katex": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/minimatch": {
|
"node_modules/@types/minimatch": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||||
@@ -15131,6 +15140,29 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/katex": {
|
||||||
|
"version": "0.13.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.13.24.tgz",
|
||||||
|
"integrity": "sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==",
|
||||||
|
"funding": [
|
||||||
|
"https://opencollective.com/katex",
|
||||||
|
"https://github.com/sponsors/katex"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^8.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"katex": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/katex/node_modules/commander": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/killable": {
|
"node_modules/killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
@@ -16567,9 +16599,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/orderedmap": {
|
"node_modules/orderedmap": {
|
||||||
"version": "1.1.7",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.0.0.tgz",
|
||||||
"integrity": "sha512-B1SuadDDwIRXXutaJQ1xjreGL3hxujpexBG4PquoXbgJD8bjp2k8b8qI/mk7q0LUdIx7T8IALWB8mPbfsjbGCw=="
|
"integrity": "sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ=="
|
||||||
},
|
},
|
||||||
"node_modules/os-browserify": {
|
"node_modules/os-browserify": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
@@ -18709,6 +18741,17 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-codemark": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-codemark/-/prosemirror-codemark-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-bl0UMClJHr7fiWq8LjK5jcvVqX0t4HLLUGhxu5pAbhBwPU8qKLaVUEdZvo4ioMExF5q8WICnqiUaVv5N9TH2uw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"prosemirror-inputrules": "^1.2.0",
|
||||||
|
"prosemirror-model": "^1.18.1",
|
||||||
|
"prosemirror-state": "^1.4.1",
|
||||||
|
"prosemirror-view": "^1.26.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prosemirror-commands": {
|
"node_modules/prosemirror-commands": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz",
|
||||||
@@ -18750,6 +18793,16 @@
|
|||||||
"rope-sequence": "^1.3.0"
|
"rope-sequence": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prosemirror-keymap": {
|
"node_modules/prosemirror-keymap": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz",
|
||||||
@@ -18760,11 +18813,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-model": {
|
"node_modules/prosemirror-model": {
|
||||||
"version": "1.17.0",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.18.1.tgz",
|
||||||
"integrity": "sha512-RJBDgZs/W26yyx1itrk5b3H9FxIro3K7Xjc2QWJI99Gu1nxYAnIggqI3fIOD8Jd/6QZfM+t6elZFJPycVexMTA==",
|
"integrity": "sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^1.1.0"
|
"orderedmap": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-schema-list": {
|
"node_modules/prosemirror-schema-list": {
|
||||||
@@ -18778,9 +18831,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-state": {
|
"node_modules/prosemirror-state": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.1.tgz",
|
||||||
"integrity": "sha512-mVDZdjNX/YT5FvypiwbphJe9psA5h+j9apsSszVRFc6oKFoIInvzdujh8QW9f9lwHtSYajLxNiM1hPhd0Sl1XA==",
|
"integrity": "sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0"
|
"prosemirror-transform": "^1.0.0"
|
||||||
@@ -28787,6 +28840,12 @@
|
|||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/katex": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||||
@@ -37049,6 +37108,21 @@
|
|||||||
"object.assign": "^4.1.2"
|
"object.assign": "^4.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"katex": {
|
||||||
|
"version": "0.13.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.13.24.tgz",
|
||||||
|
"integrity": "sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==",
|
||||||
|
"requires": {
|
||||||
|
"commander": "^8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
@@ -38203,9 +38277,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderedmap": {
|
"orderedmap": {
|
||||||
"version": "1.1.7",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.0.0.tgz",
|
||||||
"integrity": "sha512-B1SuadDDwIRXXutaJQ1xjreGL3hxujpexBG4PquoXbgJD8bjp2k8b8qI/mk7q0LUdIx7T8IALWB8mPbfsjbGCw=="
|
"integrity": "sha512-buf4PoAMlh45b8a8gsGy/X6w279TSqkyAS0C0wdTSJwFSU+ljQFJON5I8NfjLHoCXwpSROIo2wr0g33T+kQshQ=="
|
||||||
},
|
},
|
||||||
"os-browserify": {
|
"os-browserify": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
@@ -39964,6 +40038,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
|
||||||
"integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
|
"integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
|
||||||
},
|
},
|
||||||
|
"prosemirror-codemark": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-codemark/-/prosemirror-codemark-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-bl0UMClJHr7fiWq8LjK5jcvVqX0t4HLLUGhxu5pAbhBwPU8qKLaVUEdZvo4ioMExF5q8WICnqiUaVv5N9TH2uw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"prosemirror-commands": {
|
"prosemirror-commands": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz",
|
||||||
@@ -40005,6 +40085,16 @@
|
|||||||
"rope-sequence": "^1.3.0"
|
"rope-sequence": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"prosemirror-inputrules": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==",
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"prosemirror-keymap": {
|
"prosemirror-keymap": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz",
|
||||||
@@ -40015,11 +40105,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prosemirror-model": {
|
"prosemirror-model": {
|
||||||
"version": "1.17.0",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.18.1.tgz",
|
||||||
"integrity": "sha512-RJBDgZs/W26yyx1itrk5b3H9FxIro3K7Xjc2QWJI99Gu1nxYAnIggqI3fIOD8Jd/6QZfM+t6elZFJPycVexMTA==",
|
"integrity": "sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"orderedmap": "^1.1.0"
|
"orderedmap": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prosemirror-schema-list": {
|
"prosemirror-schema-list": {
|
||||||
@@ -40033,9 +40123,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prosemirror-state": {
|
"prosemirror-state": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.1.tgz",
|
||||||
"integrity": "sha512-mVDZdjNX/YT5FvypiwbphJe9psA5h+j9apsSszVRFc6oKFoIInvzdujh8QW9f9lwHtSYajLxNiM1hPhd0Sl1XA==",
|
"integrity": "sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0"
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
|||||||
@@ -33,8 +33,10 @@
|
|||||||
"@tiptap/starter-kit": "^2.0.0-beta.185",
|
"@tiptap/starter-kit": "^2.0.0-beta.185",
|
||||||
"detect-indent": "^7.0.0",
|
"detect-indent": "^7.0.0",
|
||||||
"emotion-theming": "^10.0.19",
|
"emotion-theming": "^10.0.19",
|
||||||
|
"katex": "^0.13.24",
|
||||||
"lowlight": "^2.6.1",
|
"lowlight": "^2.6.1",
|
||||||
"prism-themes": "^1.9.0",
|
"prism-themes": "^1.9.0",
|
||||||
|
"prosemirror-codemark": "^0.4.0",
|
||||||
"prosemirror-tables": "^1.1.1",
|
"prosemirror-tables": "^1.1.1",
|
||||||
"prosemirror-utils": "github:atlassian/prosemirror-utils",
|
"prosemirror-utils": "github:atlassian/prosemirror-utils",
|
||||||
"prosemirror-view": "^1.24.1",
|
"prosemirror-view": "^1.24.1",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/esm": "^3.2.0",
|
"@types/esm": "^3.2.0",
|
||||||
|
"@types/katex": "^0.14.0",
|
||||||
"@types/node": "^16.11.11",
|
"@types/node": "^16.11.11",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
|
|||||||
10
packages/editor/src/extensions/code-mark/code-mark.ts
Normal file
10
packages/editor/src/extensions/code-mark/code-mark.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
import codemark from "prosemirror-codemark";
|
||||||
|
// import "prosemirror-codemark/dist/codemark.css";
|
||||||
|
|
||||||
|
export const Codemark = Extension.create({
|
||||||
|
name: "codemarkPlugin",
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return codemark({ markType: this.editor.schema.marks.code });
|
||||||
|
},
|
||||||
|
});
|
||||||
2
packages/editor/src/extensions/code-mark/index.ts
Normal file
2
packages/editor/src/extensions/code-mark/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./code-mark";
|
||||||
|
export { Codemark as default } from "./code-mark";
|
||||||
2
packages/editor/src/extensions/math/index.ts
Normal file
2
packages/editor/src/extensions/math/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { MathInline } from "./math-inline";
|
||||||
|
export { MathBlock } from "./math-block";
|
||||||
39
packages/editor/src/extensions/math/math-block.ts
Normal file
39
packages/editor/src/extensions/math/math-block.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { inputRules } from "prosemirror-inputrules";
|
||||||
|
import {
|
||||||
|
mathPlugin,
|
||||||
|
makeBlockMathInputRule,
|
||||||
|
REGEX_BLOCK_MATH_DOLLARS,
|
||||||
|
} from "./plugin";
|
||||||
|
|
||||||
|
export const MathBlock = Node.create({
|
||||||
|
name: "math_display",
|
||||||
|
group: "block math",
|
||||||
|
content: "text*", // important!
|
||||||
|
atom: true, // important!
|
||||||
|
code: true,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: `div[class*='math-display']`, // important!
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"div",
|
||||||
|
mergeAttributes({ class: "math-display math-node" }, HTMLAttributes),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const inputRulePlugin = inputRules({
|
||||||
|
rules: [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, this.type)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [inputRulePlugin];
|
||||||
|
},
|
||||||
|
});
|
||||||
42
packages/editor/src/extensions/math/math-inline.ts
Normal file
42
packages/editor/src/extensions/math/math-inline.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { inputRules } from "prosemirror-inputrules";
|
||||||
|
import {
|
||||||
|
makeInlineMathInputRule,
|
||||||
|
REGEX_INLINE_MATH_DOLLARS,
|
||||||
|
mathPlugin,
|
||||||
|
} from "./plugin";
|
||||||
|
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
|
export const MathInline = Node.create({
|
||||||
|
name: "math_inline",
|
||||||
|
group: "inline math",
|
||||||
|
content: "text*", // important!
|
||||||
|
inline: true, // important!
|
||||||
|
atom: true, // important!
|
||||||
|
code: true,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "span[class*='math-inline']", // important!,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"span",
|
||||||
|
mergeAttributes({ class: "math-inline math-node" }, HTMLAttributes),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const inputRulePlugin = inputRules({
|
||||||
|
rules: [makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, this.type)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [mathPlugin, inputRulePlugin];
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
24
packages/editor/src/extensions/math/plugin/global.d.ts
vendored
Normal file
24
packages/editor/src/extensions/math/plugin/global.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// (https://stackoverflow.com/a/53098695/1444650)
|
||||||
|
// import needed to make this a module
|
||||||
|
import { Fragment, Node as ProseNode } from "prosemirror-model";
|
||||||
|
import { EditorState, Transaction } from "prosemirror-state";
|
||||||
|
import { EditorView } from "prosemirror-view";
|
||||||
|
|
||||||
|
declare module "prosemirror-model" {
|
||||||
|
interface Fragment {
|
||||||
|
// as of (3/31/20) official @types/prosemirror-model
|
||||||
|
// was missing Fragment.content, so we define it here
|
||||||
|
content: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeType {
|
||||||
|
hasRequiredAttrs(): boolean;
|
||||||
|
createAndFill(attrs?:Object, content?: Fragment|ProseNode|ProseNode[], marks?:Mark[]): ProseNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResolvedPos {
|
||||||
|
// missing declaration as of (7/25/20)
|
||||||
|
/** Get the position at the given index in the parent node at the given depth (which defaults to this.depth). */
|
||||||
|
posAtIndex(index:number, depth?:number):number;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/editor/src/extensions/math/plugin/index.ts
Normal file
33
packages/editor/src/extensions/math/plugin/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
|
||||||
|
// core functionality
|
||||||
|
export { MathView, type ICursorPosObserver } from "./math-node-view";
|
||||||
|
export {
|
||||||
|
mathPlugin,
|
||||||
|
createMathView,
|
||||||
|
type IMathPluginState,
|
||||||
|
} from "./math-plugin";
|
||||||
|
export { mathSchemaSpec, createMathSchema } from "./math-schema";
|
||||||
|
|
||||||
|
// recommended plugins
|
||||||
|
export { mathBackspaceCmd } from "./plugins/math-backspace";
|
||||||
|
export {
|
||||||
|
makeBlockMathInputRule,
|
||||||
|
makeInlineMathInputRule,
|
||||||
|
REGEX_BLOCK_MATH_DOLLARS,
|
||||||
|
REGEX_INLINE_MATH_DOLLARS,
|
||||||
|
REGEX_INLINE_MATH_DOLLARS_ESCAPED,
|
||||||
|
} from "./plugins/math-input-rules";
|
||||||
|
|
||||||
|
// optional / experimental plugins
|
||||||
|
export { mathSelectPlugin } from "./plugins/math-select";
|
||||||
|
|
||||||
|
// commands
|
||||||
|
export { insertMathCmd } from "./commands/insert-math-cmd";
|
||||||
|
|
||||||
|
// utilities
|
||||||
|
export { mathSerializer } from "./utils/text-serializer";
|
||||||
|
export * from "./utils/types";
|
||||||
401
packages/editor/src/extensions/math/plugin/math-node-view.ts
Normal file
401
packages/editor/src/extensions/math/plugin/math-node-view.ts
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
|
||||||
|
// prosemirror imports
|
||||||
|
import { Node as ProseNode } from "prosemirror-model";
|
||||||
|
import {
|
||||||
|
EditorState,
|
||||||
|
Transaction,
|
||||||
|
TextSelection,
|
||||||
|
PluginKey,
|
||||||
|
} from "prosemirror-state";
|
||||||
|
import {
|
||||||
|
NodeView,
|
||||||
|
EditorView,
|
||||||
|
Decoration,
|
||||||
|
DecorationSource,
|
||||||
|
} from "prosemirror-view";
|
||||||
|
import { StepMap } from "prosemirror-transform";
|
||||||
|
import { keymap } from "prosemirror-keymap";
|
||||||
|
import {
|
||||||
|
newlineInCode,
|
||||||
|
chainCommands,
|
||||||
|
deleteSelection,
|
||||||
|
} from "prosemirror-commands";
|
||||||
|
|
||||||
|
import { collapseMathCmd } from "./commands/collapse-math-cmd";
|
||||||
|
import { IMathPluginState } from "./math-plugin";
|
||||||
|
import { MathRenderer, MathRenderFn } from "./renderers/types";
|
||||||
|
|
||||||
|
//// INLINE MATH NODEVIEW //////////////////////////////////
|
||||||
|
export interface ICursorPosObserver {
|
||||||
|
/** indicates on which side cursor should appear when this node is selected */
|
||||||
|
cursorSide: "start" | "end";
|
||||||
|
/** */
|
||||||
|
updateCursorPos(state: EditorState): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMathViewOptions {
|
||||||
|
/** Dom element name to use for this NodeView */
|
||||||
|
tagName?: string;
|
||||||
|
|
||||||
|
/** Used to render the Tex input */
|
||||||
|
renderer: MathRenderFn;
|
||||||
|
|
||||||
|
/** Should be true if node is inline */
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MathView implements NodeView, ICursorPosObserver {
|
||||||
|
// nodeview params
|
||||||
|
private _node: ProseNode;
|
||||||
|
private _outerView: EditorView;
|
||||||
|
private _getPos: () => number;
|
||||||
|
|
||||||
|
// nodeview dom
|
||||||
|
dom: HTMLElement;
|
||||||
|
private _mathRenderElt: HTMLElement | undefined;
|
||||||
|
private _mathSrcElt: HTMLElement | undefined;
|
||||||
|
private _innerView: EditorView | undefined;
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
cursorSide: "start" | "end";
|
||||||
|
private _tagName: string;
|
||||||
|
private _isEditing: boolean;
|
||||||
|
private _mathPluginKey: PluginKey<IMathPluginState>;
|
||||||
|
private options: IMathViewOptions;
|
||||||
|
|
||||||
|
// == Lifecycle ===================================== //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onDestroy Callback for when this NodeView is destroyed.
|
||||||
|
* This NodeView should unregister itself from the list of ICursorPosObservers.
|
||||||
|
*
|
||||||
|
* Math Views support the following options:
|
||||||
|
* @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
|
||||||
|
* @option tagName HTML tag name to use for this NodeView. If none is provided,
|
||||||
|
* will use the node name with underscores converted to hyphens.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
node: ProseNode,
|
||||||
|
view: EditorView,
|
||||||
|
getPos: () => number,
|
||||||
|
options: IMathViewOptions,
|
||||||
|
mathPluginKey: PluginKey<IMathPluginState>,
|
||||||
|
onDestroy?: () => void
|
||||||
|
) {
|
||||||
|
// store arguments
|
||||||
|
this.options = options;
|
||||||
|
this._node = node;
|
||||||
|
this._outerView = view;
|
||||||
|
this._getPos = getPos;
|
||||||
|
this._mathPluginKey = mathPluginKey;
|
||||||
|
|
||||||
|
// editing state
|
||||||
|
this.cursorSide = "start";
|
||||||
|
this._isEditing = false;
|
||||||
|
|
||||||
|
// options
|
||||||
|
this._tagName = options.tagName || this._node.type.name.replace("_", "-");
|
||||||
|
|
||||||
|
// create dom representation of nodeview
|
||||||
|
this.dom = document.createElement(this._tagName);
|
||||||
|
if (options.inline) this.dom.classList.add("math-inline");
|
||||||
|
else this.dom.classList.add("math-display");
|
||||||
|
this.dom.classList.add("math-node");
|
||||||
|
|
||||||
|
this._mathRenderElt = document.createElement("span");
|
||||||
|
this._mathRenderElt.textContent = "";
|
||||||
|
this._mathRenderElt.classList.add("math-render");
|
||||||
|
this.dom.appendChild(this._mathRenderElt);
|
||||||
|
|
||||||
|
this._mathSrcElt = document.createElement("span");
|
||||||
|
this._mathSrcElt.classList.add("math-src");
|
||||||
|
this.dom.appendChild(this._mathSrcElt);
|
||||||
|
|
||||||
|
// ensure
|
||||||
|
this.dom.addEventListener("click", () => this.ensureFocus());
|
||||||
|
|
||||||
|
// render initial content
|
||||||
|
this.renderMath();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// close the inner editor without rendering
|
||||||
|
this.closeEditor(false);
|
||||||
|
|
||||||
|
// clean up dom elements
|
||||||
|
if (this._mathRenderElt) {
|
||||||
|
this._mathRenderElt.remove();
|
||||||
|
delete this._mathRenderElt;
|
||||||
|
}
|
||||||
|
if (this._mathSrcElt) {
|
||||||
|
this._mathSrcElt.remove();
|
||||||
|
delete this._mathSrcElt;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dom.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure focus on the inner editor whenever this node has focus.
|
||||||
|
* This helps to prevent accidental deletions of math blocks.
|
||||||
|
*/
|
||||||
|
ensureFocus() {
|
||||||
|
if (this._innerView && this._outerView.hasFocus()) {
|
||||||
|
this._innerView.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Updates ======================================= //
|
||||||
|
|
||||||
|
update(
|
||||||
|
node: ProseNode,
|
||||||
|
_decorations: readonly Decoration[],
|
||||||
|
_innerDecorations: DecorationSource
|
||||||
|
) {
|
||||||
|
if (!node.sameMarkup(this._node)) return false;
|
||||||
|
this._node = node;
|
||||||
|
|
||||||
|
if (this._innerView) {
|
||||||
|
let state = this._innerView.state;
|
||||||
|
|
||||||
|
let start = node.content.findDiffStart(state.doc.content);
|
||||||
|
if (start != null) {
|
||||||
|
let diff = node.content.findDiffEnd(state.doc.content as any);
|
||||||
|
if (diff) {
|
||||||
|
let { a: endA, b: endB } = diff;
|
||||||
|
let overlap = start - Math.min(endA, endB);
|
||||||
|
if (overlap > 0) {
|
||||||
|
endA += overlap;
|
||||||
|
endB += overlap;
|
||||||
|
}
|
||||||
|
this._innerView.dispatch(
|
||||||
|
state.tr
|
||||||
|
.replace(start, endB, node.slice(start, endA))
|
||||||
|
.setMeta("fromOutside", true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isEditing) {
|
||||||
|
this.renderMath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCursorPos(state: EditorState): void {
|
||||||
|
const pos = this._getPos();
|
||||||
|
const size = this._node.nodeSize;
|
||||||
|
const inPmSelection =
|
||||||
|
state.selection.from < pos + size && pos < state.selection.to;
|
||||||
|
|
||||||
|
if (!inPmSelection) {
|
||||||
|
this.cursorSide = pos < state.selection.from ? "end" : "start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Events ===================================== //
|
||||||
|
|
||||||
|
selectNode() {
|
||||||
|
if (!this._outerView.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dom.classList.add("ProseMirror-selectednode");
|
||||||
|
if (!this._isEditing) {
|
||||||
|
this.openEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectNode() {
|
||||||
|
this.dom.classList.remove("ProseMirror-selectednode");
|
||||||
|
if (this._isEditing) {
|
||||||
|
this.closeEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEvent(event: Event): boolean {
|
||||||
|
return (
|
||||||
|
this._innerView !== undefined &&
|
||||||
|
event.target !== undefined &&
|
||||||
|
this._innerView.dom.contains(event.target as Node)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreMutation() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Rendering ===================================== //
|
||||||
|
|
||||||
|
renderMath() {
|
||||||
|
if (!this._mathRenderElt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get tex string to render
|
||||||
|
let content = this._node.content.content;
|
||||||
|
let texString = "";
|
||||||
|
if (content.length > 0 && content[0].textContent !== null) {
|
||||||
|
texString = content[0].textContent.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty math?
|
||||||
|
if (texString.length < 1) {
|
||||||
|
this.dom.classList.add("empty-math");
|
||||||
|
// clear rendered math, since this node is in an invalid state
|
||||||
|
while (this._mathRenderElt.firstChild) {
|
||||||
|
this._mathRenderElt.firstChild.remove();
|
||||||
|
}
|
||||||
|
// do not render empty math
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.dom.classList.remove("empty-math");
|
||||||
|
}
|
||||||
|
|
||||||
|
// render katex, but fail gracefully
|
||||||
|
try {
|
||||||
|
this.options.renderer(texString, this._mathRenderElt);
|
||||||
|
this._mathRenderElt.classList.remove("parse-error");
|
||||||
|
this.dom.setAttribute("title", "");
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
console.error(err);
|
||||||
|
this._mathRenderElt.classList.add("parse-error");
|
||||||
|
this.dom.setAttribute("title", err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Inner Editor ================================== //
|
||||||
|
|
||||||
|
dispatchInner(tr: Transaction) {
|
||||||
|
if (!this._innerView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let { state, transactions } = this._innerView.state.applyTransaction(tr);
|
||||||
|
this._innerView.updateState(state);
|
||||||
|
|
||||||
|
if (!tr.getMeta("fromOutside")) {
|
||||||
|
let outerTr = this._outerView.state.tr,
|
||||||
|
offsetMap = StepMap.offset(this._getPos() + 1);
|
||||||
|
for (let i = 0; i < transactions.length; i++) {
|
||||||
|
let steps = transactions[i].steps;
|
||||||
|
for (let j = 0; j < steps.length; j++) {
|
||||||
|
let mapped = steps[j].map(offsetMap);
|
||||||
|
if (!mapped) {
|
||||||
|
throw Error("step discarded!");
|
||||||
|
}
|
||||||
|
outerTr.step(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outerTr.docChanged) this._outerView.dispatch(outerTr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openEditor() {
|
||||||
|
if (this._innerView) {
|
||||||
|
throw Error("inner view should not exist!");
|
||||||
|
}
|
||||||
|
if (!this._mathSrcElt) throw new Error("_mathSrcElt does not exist!");
|
||||||
|
|
||||||
|
// create a nested ProseMirror view
|
||||||
|
this._innerView = new EditorView(this._mathSrcElt, {
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: this._node,
|
||||||
|
plugins: [
|
||||||
|
keymap({
|
||||||
|
Tab: (state, dispatch) => {
|
||||||
|
if (dispatch) {
|
||||||
|
dispatch(state.tr.insertText("\t"));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
Backspace: chainCommands(
|
||||||
|
deleteSelection,
|
||||||
|
(state, dispatch, tr_inner) => {
|
||||||
|
// default backspace behavior for non-empty selections
|
||||||
|
if (!state.selection.empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// default backspace behavior when math node is non-empty
|
||||||
|
if (this._node.textContent.length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// otherwise, we want to delete the empty math node and focus the outer view
|
||||||
|
this._outerView.dispatch(
|
||||||
|
this._outerView.state.tr.insertText("")
|
||||||
|
);
|
||||||
|
this._outerView.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
// "Ctrl-Backspace": (state, dispatch, tr_inner) => {
|
||||||
|
// // delete math node and focus the outer view
|
||||||
|
// this._outerView.dispatch(this._outerView.state.tr.insertText(""));
|
||||||
|
// this._outerView.focus();
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
Enter: chainCommands(
|
||||||
|
newlineInCode,
|
||||||
|
collapseMathCmd(this._outerView, +1, false)
|
||||||
|
),
|
||||||
|
"Ctrl-Enter": collapseMathCmd(this._outerView, +1, false),
|
||||||
|
ArrowLeft: collapseMathCmd(this._outerView, -1, true),
|
||||||
|
ArrowRight: collapseMathCmd(this._outerView, +1, true),
|
||||||
|
ArrowUp: collapseMathCmd(this._outerView, -1, true),
|
||||||
|
ArrowDown: collapseMathCmd(this._outerView, +1, true),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
dispatchTransaction: this.dispatchInner.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
|
// focus element
|
||||||
|
let innerState = this._innerView.state;
|
||||||
|
this._innerView.focus();
|
||||||
|
|
||||||
|
// request outer cursor position before math node was selected
|
||||||
|
let maybePos = this._mathPluginKey.getState(
|
||||||
|
this._outerView.state
|
||||||
|
)?.prevCursorPos;
|
||||||
|
if (maybePos === null || maybePos === undefined) {
|
||||||
|
console.error(
|
||||||
|
"[prosemirror-math] Error: Unable to fetch math plugin state from key."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let prevCursorPos: number = maybePos ?? 0;
|
||||||
|
|
||||||
|
// compute position that cursor should appear within the expanded math node
|
||||||
|
let innerPos =
|
||||||
|
prevCursorPos <= this._getPos() ? 0 : this._node.nodeSize - 2;
|
||||||
|
|
||||||
|
this._innerView.dispatch(
|
||||||
|
innerState.tr.setSelection(TextSelection.create(innerState.doc, innerPos))
|
||||||
|
);
|
||||||
|
|
||||||
|
this._isEditing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the inner ProseMirror editor should close.
|
||||||
|
*
|
||||||
|
* @param render Optionally update the rendered math after closing. (which
|
||||||
|
* is generally what we want to do, since the user is done editing!)
|
||||||
|
*/
|
||||||
|
closeEditor(render: boolean = true) {
|
||||||
|
if (this._innerView) {
|
||||||
|
this._innerView.destroy();
|
||||||
|
this._innerView = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (render) {
|
||||||
|
this.renderMath();
|
||||||
|
}
|
||||||
|
this._isEditing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
packages/editor/src/extensions/math/plugin/math-plugin.ts
Normal file
111
packages/editor/src/extensions/math/plugin/math-plugin.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
|
||||||
|
// prosemirror imports
|
||||||
|
import { Schema, Node as ProseNode } from "prosemirror-model";
|
||||||
|
import {
|
||||||
|
Plugin as ProsePlugin,
|
||||||
|
PluginKey,
|
||||||
|
PluginSpec,
|
||||||
|
} from "prosemirror-state";
|
||||||
|
import { MathView } from "./math-node-view";
|
||||||
|
import { EditorView } from "prosemirror-view";
|
||||||
|
import { KatexRenderer } from "./renderers/katex";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
export interface IMathPluginState {
|
||||||
|
macros: { [cmd: string]: string };
|
||||||
|
/** A list of currently active `NodeView`s, in insertion order. */
|
||||||
|
activeNodeViews: MathView[];
|
||||||
|
/**
|
||||||
|
* Used to determine whether to place the cursor in the front- or back-most
|
||||||
|
* position when expanding a math node, without overriding the default arrow
|
||||||
|
* key behavior.
|
||||||
|
*/
|
||||||
|
prevCursorPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// uniquely identifies the prosemirror-math plugin
|
||||||
|
const MATH_PLUGIN_KEY = new PluginKey<IMathPluginState>("prosemirror-math");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
|
||||||
|
* @param inline TRUE for block math, FALSE for inline math.
|
||||||
|
* @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
|
||||||
|
*/
|
||||||
|
export function createMathView(inline: boolean) {
|
||||||
|
return (
|
||||||
|
node: ProseNode,
|
||||||
|
view: EditorView,
|
||||||
|
getPos: boolean | (() => number)
|
||||||
|
): MathView => {
|
||||||
|
/** @todo is this necessary?
|
||||||
|
* Docs says that for any function proprs, the current plugin instance
|
||||||
|
* will be bound to `this`. However, the typings don't reflect this.
|
||||||
|
*/
|
||||||
|
let pluginState = MATH_PLUGIN_KEY.getState(view.state);
|
||||||
|
if (!pluginState) {
|
||||||
|
throw new Error("no math plugin!");
|
||||||
|
}
|
||||||
|
let nodeViews = pluginState.activeNodeViews;
|
||||||
|
|
||||||
|
// set up NodeView
|
||||||
|
let nodeView = new MathView(
|
||||||
|
node,
|
||||||
|
view,
|
||||||
|
getPos as () => number,
|
||||||
|
{
|
||||||
|
inline,
|
||||||
|
renderer: inline ? KatexRenderer.inline : KatexRenderer.block,
|
||||||
|
tagName: inline ? "span" : "div",
|
||||||
|
},
|
||||||
|
MATH_PLUGIN_KEY,
|
||||||
|
() => {
|
||||||
|
nodeViews.splice(nodeViews.indexOf(nodeView));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
nodeViews.push(nodeView);
|
||||||
|
return nodeView;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathPluginSpec: PluginSpec<IMathPluginState> = {
|
||||||
|
key: MATH_PLUGIN_KEY,
|
||||||
|
state: {
|
||||||
|
init(config, instance) {
|
||||||
|
return {
|
||||||
|
macros: {},
|
||||||
|
activeNodeViews: [],
|
||||||
|
prevCursorPos: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
apply(tr, value, oldState, newState) {
|
||||||
|
// produce updated state field for this plugin
|
||||||
|
const newPos = newState.selection.from;
|
||||||
|
const oldPos = oldState.selection.from;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// these values are left unchanged
|
||||||
|
activeNodeViews: value.activeNodeViews,
|
||||||
|
macros: value.macros,
|
||||||
|
// update with the second-most recent cursor pos
|
||||||
|
prevCursorPos: oldPos !== newPos ? oldPos : value.prevCursorPos,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** @todo (8/21/20) implement serialization for math plugin */
|
||||||
|
// toJSON(value) { },
|
||||||
|
// fromJSON(config, value, state){ return {}; }
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
nodeViews: {
|
||||||
|
math_inline: createMathView(true),
|
||||||
|
math_display: createMathView(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mathPlugin = new ProsePlugin(mathPluginSpec);
|
||||||
126
packages/editor/src/extensions/math/plugin/math-schema.ts
Normal file
126
packages/editor/src/extensions/math/plugin/math-schema.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
|
||||||
|
// prosemirror imports
|
||||||
|
import {
|
||||||
|
Node as ProseNode,
|
||||||
|
Fragment,
|
||||||
|
MarkSpec,
|
||||||
|
NodeSpec,
|
||||||
|
Schema,
|
||||||
|
SchemaSpec,
|
||||||
|
NodeType,
|
||||||
|
} from "prosemirror-model";
|
||||||
|
import {
|
||||||
|
defaultBlockMathParseRules,
|
||||||
|
defaultInlineMathParseRules,
|
||||||
|
} from "./plugins/math-paste-rules";
|
||||||
|
import { SchemaSpecMarkT, SchemaSpecNodeT } from "./utils/types";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Borrowed from ProseMirror typings, modified to exclude OrderedMaps in spec,
|
||||||
|
* in order to help with the schema-building functions below.
|
||||||
|
*
|
||||||
|
* NOTE: TypeScript's typings for the spread operator { ...a, ...b } are only
|
||||||
|
* an approximation to the true type, and have difficulty with optional fields.
|
||||||
|
* So, unlike the SchemaSpec type, the `marks` field is NOT optional here.
|
||||||
|
*
|
||||||
|
* function example<T extends string>(x: { [name in T]: string; } | null) {
|
||||||
|
* const s = { ...x }; // inferred to have type `{}`.
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @see https://github.com/microsoft/TypeScript/issues/10727
|
||||||
|
*/
|
||||||
|
interface SchemaSpecJson<N extends string = any, M extends string = any>
|
||||||
|
extends SchemaSpec<N, M> {
|
||||||
|
nodes: { [name in N]: NodeSpec };
|
||||||
|
marks: { [name in M]: MarkSpec };
|
||||||
|
topNode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MathSpecNodeT = SchemaSpecNodeT<typeof mathSchemaSpec>;
|
||||||
|
type MathSpecMarkT = SchemaSpecMarkT<typeof mathSchemaSpec>;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// force typescript to infer generic type arguments for SchemaSpec
|
||||||
|
function createSchemaSpec<N extends string = any, M extends string = any>(
|
||||||
|
spec: SchemaSpecJson<N, M>
|
||||||
|
): SchemaSpecJson<N, M> {
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bare minimum ProseMirror schema for working with math nodes
|
||||||
|
export const mathSchemaSpec = createSchemaSpec({
|
||||||
|
nodes: {
|
||||||
|
// :: NodeSpec top-level document node
|
||||||
|
doc: {
|
||||||
|
content: "block+",
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
content: "inline*",
|
||||||
|
group: "block",
|
||||||
|
parseDOM: [{ tag: "p" }],
|
||||||
|
toDOM() {
|
||||||
|
return ["p", 0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
math_inline: {
|
||||||
|
group: "inline math",
|
||||||
|
content: "text*",
|
||||||
|
inline: true,
|
||||||
|
atom: true,
|
||||||
|
toDOM: () => ["math-inline", { class: "math-node" }, 0],
|
||||||
|
parseDOM: [{ tag: "math-inline" }, ...defaultInlineMathParseRules],
|
||||||
|
},
|
||||||
|
math_display: {
|
||||||
|
group: "block math",
|
||||||
|
content: "text*",
|
||||||
|
atom: true,
|
||||||
|
code: true,
|
||||||
|
toDOM: () => ["math-display", { class: "math-node" }, 0],
|
||||||
|
parseDOM: [{ tag: "math-display" }, ...defaultBlockMathParseRules],
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
group: "inline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marks: {
|
||||||
|
math_select: {
|
||||||
|
toDOM() {
|
||||||
|
return ["math-select", 0];
|
||||||
|
},
|
||||||
|
parseDOM: [{ tag: "math-select" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the prosemirror-math default SchemaSpec to create a new Schema.
|
||||||
|
*/
|
||||||
|
export function createMathSchema() {
|
||||||
|
return new Schema(mathSchemaSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SchemaSpec by adding math nodes to an existing spec.
|
||||||
|
|
||||||
|
* @deprecated This function is included for demonstration/testing only. For the
|
||||||
|
* time being, I highly recommend adding the math nodes manually to your own
|
||||||
|
* ProseMirror spec to avoid unexpected interactions between the math nodes
|
||||||
|
* and your own spec. Use the example spec for reference.
|
||||||
|
*
|
||||||
|
* @param baseSpec The SchemaSpec to extend. Must specify a `marks` field, and
|
||||||
|
* must be a raw object (not an OrderedMap).
|
||||||
|
*/
|
||||||
|
export function extendMathSchemaSpec<N extends string, M extends string>(
|
||||||
|
baseSpec: SchemaSpecJson<N, M>
|
||||||
|
): SchemaSpecJson<N | MathSpecNodeT, M | MathSpecMarkT> {
|
||||||
|
let nodes = { ...baseSpec.nodes, ...mathSchemaSpec.nodes };
|
||||||
|
let marks = { ...baseSpec.marks, ...mathSchemaSpec.marks };
|
||||||
|
return { nodes, marks, topNode: baseSpec.topNode };
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { InputRule } from "prosemirror-inputrules";
|
||||||
|
import { NodeType } from "prosemirror-model";
|
||||||
|
import { NodeSelection } from "prosemirror-state";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// ---- Inline Input Rules ------------------------------ //
|
||||||
|
|
||||||
|
// simple input rule for inline math
|
||||||
|
export const REGEX_INLINE_MATH_DOLLARS: RegExp = /\$\$(.+)\$\$/; //new RegExp("\$(.+)\$", "i");
|
||||||
|
|
||||||
|
// negative lookbehind regex notation allows for escaped \$ delimiters
|
||||||
|
// (requires browser supporting ECMA2018 standard -- currently only Chrome / FF)
|
||||||
|
// (see https://javascript.info/regexp-lookahead-lookbehind)
|
||||||
|
export const REGEX_INLINE_MATH_DOLLARS_ESCAPED: RegExp = (() => {
|
||||||
|
// attempt to create regex with negative lookbehind
|
||||||
|
try {
|
||||||
|
return new RegExp("(?<!\\\\)\\$(.+)(?<!\\\\)\\$");
|
||||||
|
} catch (e) {
|
||||||
|
return REGEX_INLINE_MATH_DOLLARS;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ---- Block Input Rules ------------------------------- //
|
||||||
|
|
||||||
|
// simple inputrule for block math
|
||||||
|
export const REGEX_BLOCK_MATH_DOLLARS: RegExp = /\$\$\$\s+$/; //new RegExp("\$\$\s+$", "i");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
export function makeInlineMathInputRule(
|
||||||
|
pattern: RegExp,
|
||||||
|
nodeType: NodeType,
|
||||||
|
getAttrs?: (match: string[]) => any
|
||||||
|
) {
|
||||||
|
return new InputRule(pattern, (state, match, start, end) => {
|
||||||
|
let $start = state.doc.resolve(start);
|
||||||
|
let index = $start.index();
|
||||||
|
let $end = state.doc.resolve(end);
|
||||||
|
// get attrs
|
||||||
|
let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
|
||||||
|
// check if replacement valid
|
||||||
|
if (!$start.parent.canReplaceWith(index, $end.index(), nodeType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform replacement
|
||||||
|
return state.tr.replaceRangeWith(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
nodeType.create(attrs, nodeType.schema.text(match[1]))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeBlockMathInputRule(
|
||||||
|
pattern: RegExp,
|
||||||
|
nodeType: NodeType,
|
||||||
|
getAttrs?: (match: string[]) => any
|
||||||
|
) {
|
||||||
|
return new InputRule(pattern, (state, match, start, end) => {
|
||||||
|
let $start = state.doc.resolve(start);
|
||||||
|
let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
|
||||||
|
if (
|
||||||
|
!$start
|
||||||
|
.node(-1)
|
||||||
|
.canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
let tr = state.tr
|
||||||
|
.delete(start, end)
|
||||||
|
.setBlockType(start, start, nodeType, attrs);
|
||||||
|
|
||||||
|
return tr.setSelection(
|
||||||
|
NodeSelection.create(tr.doc, tr.mapping.map($start.pos - 1))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* Note that for some of the `ParseRule`s defined below,
|
||||||
|
* we define a `getAttrs` function, which, other than
|
||||||
|
* defining node attributes, can be used to describe complex
|
||||||
|
* match conditions for a rule.
|
||||||
|
|
||||||
|
* Returning `false` from `ParseRule.getAttrs` prevents the
|
||||||
|
* rule from matching, while returning `null` indicates that
|
||||||
|
* the default set of note attributes should be used.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Node as ProseNode,
|
||||||
|
Fragment,
|
||||||
|
ParseRule,
|
||||||
|
Schema,
|
||||||
|
NodeType,
|
||||||
|
} from "prosemirror-model";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function getFirstMatch(
|
||||||
|
root: Element,
|
||||||
|
rules: ((root: Element) => false | string)[]
|
||||||
|
): false | string {
|
||||||
|
for (let rule of rules) {
|
||||||
|
let match: false | string = rule(root);
|
||||||
|
if (match !== false) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTextFragment<S extends Schema<any, any>>(
|
||||||
|
text: string,
|
||||||
|
schema: S
|
||||||
|
): Fragment {
|
||||||
|
return Fragment.from(schema.text(text) as ProseNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// -- Wikipedia ----------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
*/
|
||||||
|
function texFromMediaWikiFallbackImage(root: Element): false | string {
|
||||||
|
let match = root.querySelector("img.mwe-math-fallback-image-inline[alt]");
|
||||||
|
return match?.getAttribute("alt") ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
*/
|
||||||
|
function texFromMathML_01(root: Element): false | string {
|
||||||
|
let match = root.querySelector("math[alttext]");
|
||||||
|
return match?.getAttribute("alttext") ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
*/
|
||||||
|
function texFromMathML_02(root: Element): false | string {
|
||||||
|
let match = root.querySelector(
|
||||||
|
"math annotation[encoding='application/x-tex'"
|
||||||
|
);
|
||||||
|
return match?.textContent ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for a child node that matches the following template:
|
||||||
|
* <script type="math/tex"></script>
|
||||||
|
*/
|
||||||
|
function texFromScriptTag(root: Element): false | string {
|
||||||
|
let match = root.querySelector("script[type*='math/tex']");
|
||||||
|
return match?.textContent ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchWikipedia(root: Element): false | string {
|
||||||
|
let match: false | string = getFirstMatch(root, [
|
||||||
|
texFromMediaWikiFallbackImage,
|
||||||
|
texFromMathML_01,
|
||||||
|
texFromMathML_02,
|
||||||
|
]);
|
||||||
|
// TODO: if no tex string was found, but we have MathML, try to parse it
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wikipedia formats block math inside a <dl>...</dl> element, as below.
|
||||||
|
*
|
||||||
|
* - Evidently no CSS class is used to distinguish inline vs block math
|
||||||
|
* - Sometimes the `\displaystyle` TeX command is present even in inline math
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <dl><dd><span class="mwe-math-element">
|
||||||
|
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
* <semantics>
|
||||||
|
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
|
||||||
|
* <annotation encoding="application/x-tex">...</annotation>
|
||||||
|
* </semantics>
|
||||||
|
* </math>
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
* </span>
|
||||||
|
* </span></dd></dl>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const wikipediaBlockMathParseRule: ParseRule = {
|
||||||
|
tag: "dl",
|
||||||
|
getAttrs(p: Node | string): false | null {
|
||||||
|
let dl = p as HTMLDListElement;
|
||||||
|
|
||||||
|
// <dl> must contain exactly one child
|
||||||
|
if (dl.childElementCount !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let dd = dl.firstChild as Element;
|
||||||
|
if (dd.tagName !== "DD") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <dd> must contain exactly one child
|
||||||
|
if (dd.childElementCount !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mweElt = dd.firstChild as Element;
|
||||||
|
if (!mweElt.classList.contains("mwe-math-element")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// success! proceed to `getContent` for further processing
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getContent<S extends Schema<any, any>>(p: Node, schema: S): Fragment {
|
||||||
|
// search the matched element for a TeX string
|
||||||
|
let match: false | string = matchWikipedia(p as Element);
|
||||||
|
// return a fragment representing the math node's children
|
||||||
|
let texString: string = match || "\\text{\\color{red}(paste error)}";
|
||||||
|
return makeTextFragment(texString, schema);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse rule for inline math content on Wikipedia of the following form:
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <span class="mwe-math-element">
|
||||||
|
* <span class="mwe-math-mathml-inline mwe-math-mathml-ally" style="...">
|
||||||
|
* <math xmlns="http://www.w3.org/1998/Math/MathML" alttext="...">
|
||||||
|
* <semantics>
|
||||||
|
* <mrow class="MJX-TeXAtom-ORD">...</mrow>
|
||||||
|
* <annotation encoding="application/x-tex">...</annotation>
|
||||||
|
* </semantics>
|
||||||
|
* </math>
|
||||||
|
* <img src="https://wikimedia.org/api/rest_v1/media/math/render/svg/..."
|
||||||
|
* class="mwe-math-fallback-image-inline"
|
||||||
|
* alt="..." />
|
||||||
|
* </span>
|
||||||
|
* </span>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const wikipediaInlineMathParseRule: ParseRule = {
|
||||||
|
tag: "span",
|
||||||
|
getAttrs(p: Node | string): false | null {
|
||||||
|
let span = p as HTMLSpanElement;
|
||||||
|
if (!span.classList.contains("mwe-math-element")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// success! proceed to `getContent` for further processing
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getContent<S extends Schema<any, any>>(p: Node, schema: S): Fragment {
|
||||||
|
// search the matched element for a TeX string
|
||||||
|
let match: false | string = matchWikipedia(p as Element);
|
||||||
|
// return a fragment representing the math node's children
|
||||||
|
let texString: string = match || "\\text{\\color{red}(paste error)}";
|
||||||
|
return makeTextFragment(texString, schema);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- MathJax ------------------------------------------- //
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
export const defaultInlineMathParseRules: ParseRule[] = [
|
||||||
|
wikipediaInlineMathParseRule,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultBlockMathParseRules: ParseRule[] = [
|
||||||
|
wikipediaBlockMathParseRule,
|
||||||
|
];
|
||||||
@@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type MathRenderFn = (text: string, element: HTMLElement) => void;
|
||||||
|
export type MathRenderer = {
|
||||||
|
inline: MathRenderFn;
|
||||||
|
block: MathRenderFn;
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Node as ProseNode,
|
||||||
|
Mark,
|
||||||
|
Slice,
|
||||||
|
NodeType,
|
||||||
|
MarkType,
|
||||||
|
Fragment,
|
||||||
|
} from "prosemirror-model";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type TypedNode<T extends string> = ProseNode & {
|
||||||
|
type: NodeType & { name: T };
|
||||||
|
};
|
||||||
|
type TypedMark<T extends string> = Mark & {
|
||||||
|
type: MarkType & { name: T };
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeSerializer<T extends string> = (node: TypedNode<T>) => string;
|
||||||
|
type MarkSerializer<T extends string> = (mark: TypedMark<T>) => string;
|
||||||
|
|
||||||
|
class ProseMirrorTextSerializer {
|
||||||
|
public nodes: { [name: string]: NodeSerializer<string> | undefined };
|
||||||
|
public marks: { [name: string]: MarkSerializer<string> | undefined };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
fns: {
|
||||||
|
nodes?: { [name: string]: NodeSerializer<string> | undefined };
|
||||||
|
marks?: { [name: string]: MarkSerializer<string> | undefined };
|
||||||
|
},
|
||||||
|
base?: ProseMirrorTextSerializer
|
||||||
|
) {
|
||||||
|
// use base serializer as a fallback
|
||||||
|
this.nodes = { ...base?.nodes, ...fns.nodes };
|
||||||
|
this.marks = { ...base?.marks, ...fns.marks };
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeFragment(fragment: Fragment): string {
|
||||||
|
// adapted from the undocumented `Fragment.textBetween` function
|
||||||
|
// https://github.com/ProseMirror/prosemirror-model/blob/eef20c8c6dbf841b1d70859df5d59c21b5108a4f/src/fragment.js#L46
|
||||||
|
let blockSeparator = "\n\n";
|
||||||
|
let leafText: string | undefined = undefined;
|
||||||
|
let text: string = "";
|
||||||
|
let separated: boolean = true;
|
||||||
|
|
||||||
|
let from = 0;
|
||||||
|
let to = fragment.size;
|
||||||
|
|
||||||
|
fragment.nodesBetween(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
(node, pos) => {
|
||||||
|
// check if one of our custom serializers handles this node
|
||||||
|
let serialized: string | null = this.serializeNode(node);
|
||||||
|
if (serialized !== null) {
|
||||||
|
text += serialized;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.isText) {
|
||||||
|
text += node.text?.slice(Math.max(from, pos) - pos, to - pos) || "";
|
||||||
|
separated = !blockSeparator;
|
||||||
|
} else if (node.isLeaf && leafText) {
|
||||||
|
text += leafText;
|
||||||
|
separated = !blockSeparator;
|
||||||
|
} else if (!separated && node.isBlock) {
|
||||||
|
text += blockSeparator;
|
||||||
|
separated = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeSlice(slice: Slice): string {
|
||||||
|
return this.serializeFragment(slice.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeNode(node: ProseNode): string | null {
|
||||||
|
// check if one of our custom serializers handles this node
|
||||||
|
let nodeSerializer = this.nodes[node.type.name];
|
||||||
|
if (nodeSerializer !== undefined) {
|
||||||
|
return nodeSerializer(node);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mathSerializer = new ProseMirrorTextSerializer({
|
||||||
|
nodes: {
|
||||||
|
math_inline: (node) => `$${node.textContent}$`,
|
||||||
|
math_display: (node) => `\n\n$$\n${node.textContent}\n$$`,
|
||||||
|
},
|
||||||
|
});
|
||||||
10
packages/editor/src/extensions/math/plugin/utils/types.ts
Normal file
10
packages/editor/src/extensions/math/plugin/utils/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Schema, SchemaSpec } from "prosemirror-model";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// infer generic `Nodes` and `Marks` type parameters for a SchemaSpec
|
||||||
|
export type SchemaSpecNodeT<Spec> = Spec extends SchemaSpec<infer N, infer _> ? N : never;
|
||||||
|
export type SchemaSpecMarkT<Spec> = Spec extends SchemaSpec<infer _, infer M> ? M : never;
|
||||||
|
|
||||||
|
export type SchemaNodeT<S> = S extends Schema<infer N, infer _> ? N : never;
|
||||||
|
export type SchemaMarkT<S> = S extends Schema<infer _, infer M> ? M : never;
|
||||||
@@ -32,6 +32,8 @@ import { EmbedNode } from "./extensions/embed";
|
|||||||
import { CodeBlock } from "./extensions/code-block";
|
import { CodeBlock } from "./extensions/code-block";
|
||||||
import { ListItem } from "./extensions/list-item";
|
import { ListItem } from "./extensions/list-item";
|
||||||
import { Link } from "@tiptap/extension-link";
|
import { Link } from "@tiptap/extension-link";
|
||||||
|
import { Codemark } from "./extensions/code-mark";
|
||||||
|
import { MathInline, MathBlock } from "./extensions/math";
|
||||||
import {
|
import {
|
||||||
NodeViewSelectionNotifier,
|
NodeViewSelectionNotifier,
|
||||||
usePortalProvider,
|
usePortalProvider,
|
||||||
@@ -123,6 +125,9 @@ const useTiptap = (
|
|||||||
OutlineListItem,
|
OutlineListItem,
|
||||||
OutlineList,
|
OutlineList,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
Codemark,
|
||||||
|
MathInline,
|
||||||
|
MathBlock,
|
||||||
],
|
],
|
||||||
onBeforeCreate: ({ editor }) => {
|
onBeforeCreate: ({ editor }) => {
|
||||||
if (theme) {
|
if (theme) {
|
||||||
|
|||||||
@@ -221,3 +221,168 @@ pre *::selection {
|
|||||||
border-left: 5px solid var(--border);
|
border-left: 5px solid var(--border);
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****************************************************************/
|
||||||
|
/* Styles taken from https://github.com/curvenote/prosemirror-codemark/blob/main/src/codemark.css */
|
||||||
|
/****************************************************************/
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
49% {
|
||||||
|
border-color: unset;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.no-cursor {
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
|
div:focus .fake-cursor,
|
||||||
|
span:focus .fake-cursor {
|
||||||
|
margin-right: -1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-left-style: solid;
|
||||||
|
animation: blink 1s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************/
|
||||||
|
/* MATH STYLES */
|
||||||
|
/*******************************************/
|
||||||
|
|
||||||
|
/*---------------------------------------------------------
|
||||||
|
* Author: Benjamin R. Bray
|
||||||
|
* License: MIT (see LICENSE in project root for details)
|
||||||
|
*--------------------------------------------------------*/
|
||||||
|
|
||||||
|
/* == Math Nodes ======================================== */
|
||||||
|
|
||||||
|
.math-node {
|
||||||
|
min-width: 1em;
|
||||||
|
min-height: 1em;
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-family: "Consolas", "Ubuntu Mono", monospace;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-node.empty-math .math-render::before {
|
||||||
|
content: "(empty)";
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-node .math-render.parse-error::before {
|
||||||
|
content: "(math error)";
|
||||||
|
color: red;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-node.ProseMirror-selectednode {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-node .math-src {
|
||||||
|
display: none;
|
||||||
|
color: var(--text);
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-node.ProseMirror-selectednode .math-src {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.math-node.ProseMirror-selectednode .math-render {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Inline Math --------------------------------------- */
|
||||||
|
|
||||||
|
.math-inline {
|
||||||
|
display: inline;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-inline .math-render {
|
||||||
|
display: inline-block;
|
||||||
|
/* font-size: 0.85em; */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-inline .math-src .ProseMirror {
|
||||||
|
display: inline;
|
||||||
|
/* Necessary to fix FireFox bug with contenteditable, https://bugzilla.mozilla.org/show_bug.cgi?id=1252108 */
|
||||||
|
border-right: 1px solid transparent;
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||||
|
Liberation Mono, monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-inline.ProseMirror-selectednode {
|
||||||
|
background-color: var(--bgSecondary);
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-inline .math-src::after,
|
||||||
|
.math-inline .math-src::before {
|
||||||
|
content: "$$";
|
||||||
|
color: var(--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Block Math ---------------------------------------- */
|
||||||
|
|
||||||
|
.math-display {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-display .math-render {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-display.ProseMirror-selectednode {
|
||||||
|
background-color: var(--bgSecondary);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-display .math-src .ProseMirror {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||||
|
Liberation Mono, monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .math-display .math-src::after, */
|
||||||
|
.math-display .math-src::before {
|
||||||
|
content: "Math";
|
||||||
|
text-align: left;
|
||||||
|
color: var(--disabled);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-display .katex-display {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Selection Plugin ---------------------------------- */
|
||||||
|
|
||||||
|
/* p::selection,
|
||||||
|
p > *::selection {
|
||||||
|
background-color: #c0c0c0;
|
||||||
|
}
|
||||||
|
.katex-html *::selection {
|
||||||
|
background-color: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-node.math-select .math-render {
|
||||||
|
background-color: #c0c0c0ff;
|
||||||
|
}
|
||||||
|
.math-inline.math-select .math-render {
|
||||||
|
padding-top: 2px;
|
||||||
|
} */
|
||||||
|
|||||||
Reference in New Issue
Block a user