@tiptap/extension-mathematics 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/MathematicsPlugin.d.ts +13 -0
- package/dist/MathematicsPlugin.d.ts.map +1 -0
- package/dist/index.cjs +177 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +174 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/mathematics.d.ts +7 -0
- package/dist/mathematics.d.ts.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +50 -0
- package/src/MathematicsPlugin.ts +208 -0
- package/src/index.ts +7 -0
- package/src/mathematics.ts +31 -0
- package/src/types.ts +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @tiptap/extension-mathematics
|
|
2
|
+
[](https://www.npmjs.com/package/@tiptap/extension-mathematics)
|
|
3
|
+
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
|
4
|
+
[](https://www.npmjs.com/package/@tiptap/extension-mathematics)
|
|
5
|
+
[](https://github.com/sponsors/ueberdosis)
|
|
6
|
+
|
|
7
|
+
## Introduction
|
|
8
|
+
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
|
|
9
|
+
|
|
10
|
+
## Official Documentation
|
|
11
|
+
Documentation can be found on the [Tiptap website](https://tiptap.dev).
|
|
12
|
+
|
|
13
|
+
## License
|
|
14
|
+
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Plugin } from '@tiptap/pm/state';
|
|
2
|
+
import { DecorationSet } from '@tiptap/pm/view';
|
|
3
|
+
import { MathematicsOptionsWithEditor } from './types.js';
|
|
4
|
+
type PluginState = {
|
|
5
|
+
decorations: DecorationSet;
|
|
6
|
+
isEditable: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
decorations: undefined;
|
|
9
|
+
isEditable: undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare const MathematicsPlugin: (options: MathematicsOptionsWithEditor) => Plugin<PluginState>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=MathematicsPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MathematicsPlugin.d.ts","sourceRoot":"","sources":["../src/MathematicsPlugin.ts"],"names":[],"mappings":"AACA,OAAO,EACQ,MAAM,EACpB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAc,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAG3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAA;AAWzD,KAAK,WAAW,GACd;IAAE,WAAW,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GACnD;IAAE,WAAW,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,SAAS,CAAA;CAAE,CAAA;AA8CnD,eAAO,MAAM,iBAAiB,YAAa,4BAA4B,wBA6ItE,CAAA"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var core = require('@tiptap/core');
|
|
6
|
+
var state = require('@tiptap/pm/state');
|
|
7
|
+
var view = require('@tiptap/pm/view');
|
|
8
|
+
var katex = require('katex');
|
|
9
|
+
|
|
10
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var katex__default = /*#__PURE__*/_interopDefaultCompat(katex);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the range of positions that have been affected by a transaction
|
|
16
|
+
*/
|
|
17
|
+
function getAffectedRange(newState, previousPluginState, isEditable, tr, state) {
|
|
18
|
+
const docSize = newState.doc.nodeSize - 2;
|
|
19
|
+
let minFrom = 0;
|
|
20
|
+
let maxTo = docSize;
|
|
21
|
+
if (previousPluginState.isEditable !== isEditable) {
|
|
22
|
+
// When the editable state changes, run on all nodes just to be safe
|
|
23
|
+
minFrom = 0;
|
|
24
|
+
maxTo = docSize;
|
|
25
|
+
}
|
|
26
|
+
else if (tr.docChanged) {
|
|
27
|
+
// When the document changes, only run on the nodes that have changed
|
|
28
|
+
minFrom = docSize;
|
|
29
|
+
maxTo = 0;
|
|
30
|
+
core.getChangedRanges(tr).forEach(range => {
|
|
31
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
32
|
+
minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1);
|
|
33
|
+
maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (tr.selectionSet) {
|
|
37
|
+
const { $from, $to } = state.selection;
|
|
38
|
+
const { $from: $newFrom, $to: $newTo } = newState.selection;
|
|
39
|
+
// When the selection changes, run on all the nodes between the old and new selection
|
|
40
|
+
minFrom = Math.min(
|
|
41
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
42
|
+
$from.depth === 0 ? 0 : $from.before(), $newFrom.depth === 0 ? 0 : $newFrom.before());
|
|
43
|
+
maxTo = Math.max($to.depth === 0 ? maxTo : $to.after(), $newTo.depth === 0 ? maxTo : $newTo.after());
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
minFrom: Math.max(minFrom, 0),
|
|
47
|
+
maxTo: Math.min(maxTo, docSize),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const MathematicsPlugin = (options) => {
|
|
51
|
+
const { regex, katexOptions = {}, editor, shouldRender, } = options;
|
|
52
|
+
return new state.Plugin({
|
|
53
|
+
key: new state.PluginKey('mathematics'),
|
|
54
|
+
state: {
|
|
55
|
+
init() {
|
|
56
|
+
return { decorations: undefined, isEditable: undefined };
|
|
57
|
+
},
|
|
58
|
+
apply(tr, previousPluginState, state, newState) {
|
|
59
|
+
if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {
|
|
60
|
+
// Just reuse the existing decorations, since nothing should have changed
|
|
61
|
+
return previousPluginState;
|
|
62
|
+
}
|
|
63
|
+
const nextDecorationSet = (previousPluginState.decorations || view.DecorationSet.empty).map(tr.mapping, tr.doc);
|
|
64
|
+
const { selection } = newState;
|
|
65
|
+
const isEditable = editor.isEditable;
|
|
66
|
+
const decorationsToAdd = [];
|
|
67
|
+
const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state);
|
|
68
|
+
newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {
|
|
69
|
+
const enabled = shouldRender(newState, pos, node);
|
|
70
|
+
if (node.isText && node.text && enabled) {
|
|
71
|
+
let match;
|
|
72
|
+
// eslint-disable-next-line no-cond-assign
|
|
73
|
+
while ((match = regex.exec(node.text))) {
|
|
74
|
+
const from = pos + match.index;
|
|
75
|
+
const to = from + match[0].length;
|
|
76
|
+
const content = match.slice(1).find(Boolean);
|
|
77
|
+
if (content) {
|
|
78
|
+
const selectionSize = selection.from - selection.to;
|
|
79
|
+
const anchorIsInside = selection.anchor >= from && selection.anchor <= to;
|
|
80
|
+
const rangeIsInside = selection.from >= from && selection.to <= to;
|
|
81
|
+
const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside;
|
|
82
|
+
if (
|
|
83
|
+
// Are the decorations already present?
|
|
84
|
+
nextDecorationSet.find(from, to, (deco) => isEditing === deco.isEditing
|
|
85
|
+
&& content === deco.content
|
|
86
|
+
&& isEditable === deco.isEditable
|
|
87
|
+
&& katexOptions === deco.katexOptions).length) {
|
|
88
|
+
// Decoration exists in set, no need to add it again
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Use an inline decoration to either hide original (preview is showing) or show it (editing "mode")
|
|
92
|
+
decorationsToAdd.push(view.Decoration.inline(from, to, {
|
|
93
|
+
class: isEditing && isEditable
|
|
94
|
+
? 'Tiptap-mathematics-editor'
|
|
95
|
+
: 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',
|
|
96
|
+
style: !isEditing || !isEditable
|
|
97
|
+
? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'
|
|
98
|
+
: undefined,
|
|
99
|
+
}, {
|
|
100
|
+
content,
|
|
101
|
+
isEditable,
|
|
102
|
+
isEditing,
|
|
103
|
+
katexOptions,
|
|
104
|
+
}));
|
|
105
|
+
if (!isEditable || !isEditing) {
|
|
106
|
+
// Create decoration widget and add KaTeX preview if selection is not within the math-editor
|
|
107
|
+
decorationsToAdd.push(view.Decoration.widget(from, () => {
|
|
108
|
+
const element = document.createElement('span');
|
|
109
|
+
// TODO: changeable class names
|
|
110
|
+
element.classList.add('Tiptap-mathematics-render');
|
|
111
|
+
if (isEditable) {
|
|
112
|
+
element.classList.add('Tiptap-mathematics-render--editable');
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
katex__default.default.render(content, element, katexOptions);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
element.innerHTML = content;
|
|
119
|
+
}
|
|
120
|
+
return element;
|
|
121
|
+
}, {
|
|
122
|
+
content,
|
|
123
|
+
isEditable,
|
|
124
|
+
isEditing,
|
|
125
|
+
katexOptions,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// Remove any decorations that exist at the same position, they will be replaced by the new decorations
|
|
133
|
+
const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to));
|
|
134
|
+
return {
|
|
135
|
+
decorations: nextDecorationSet
|
|
136
|
+
// Remove existing decorations that are going to be replaced
|
|
137
|
+
.remove(decorationsToRemove)
|
|
138
|
+
// Add any new decorations
|
|
139
|
+
.add(tr.doc, decorationsToAdd),
|
|
140
|
+
isEditable,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
props: {
|
|
145
|
+
decorations(state) {
|
|
146
|
+
var _a, _b;
|
|
147
|
+
return (_b = (_a = this.getState(state)) === null || _a === void 0 ? void 0 : _a.decorations) !== null && _b !== void 0 ? _b : view.DecorationSet.empty;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const defaultShouldRender = (state, pos) => {
|
|
154
|
+
const $pos = state.doc.resolve(pos);
|
|
155
|
+
const isInCodeBlock = $pos.parent.type.name === 'codeBlock';
|
|
156
|
+
return !isInCodeBlock;
|
|
157
|
+
};
|
|
158
|
+
const Mathematics = core.Extension.create({
|
|
159
|
+
name: 'Mathematics',
|
|
160
|
+
addOptions() {
|
|
161
|
+
return {
|
|
162
|
+
// eslint-disable-next-line no-useless-escape
|
|
163
|
+
regex: /\$([^\$]*)\$/gi,
|
|
164
|
+
katexOptions: undefined,
|
|
165
|
+
shouldRender: defaultShouldRender,
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
addProseMirrorPlugins() {
|
|
169
|
+
return [MathematicsPlugin({ ...this.options, editor: this.editor })];
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
exports.Mathematics = Mathematics;
|
|
174
|
+
exports.MathematicsPlugin = MathematicsPlugin;
|
|
175
|
+
exports.default = Mathematics;
|
|
176
|
+
exports.defaultShouldRender = defaultShouldRender;
|
|
177
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/MathematicsPlugin.ts","../src/mathematics.ts"],"sourcesContent":["import { getChangedRanges } from '@tiptap/core'\nimport {\n EditorState, Plugin, PluginKey, Transaction,\n} from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\nimport katex from 'katex'\n\nimport { MathematicsOptionsWithEditor } from './types.js'\n\ntype DecoSpec = {\n isEditable: boolean;\n isEditing: boolean;\n katexOptions: MathematicsOptionsWithEditor['katexOptions'];\n content: string;\n};\n\ntype Deco = Omit<Decoration, 'spec'> & { spec: DecoSpec };\n\ntype PluginState =\n| { decorations: DecorationSet; isEditable: boolean }\n| { decorations: undefined; isEditable: undefined }\n\n/**\n * Get the range of positions that have been affected by a transaction\n */\nfunction getAffectedRange(newState: EditorState, previousPluginState: PluginState, isEditable: boolean, tr: Transaction, state: EditorState) {\n const docSize = newState.doc.nodeSize - 2\n let minFrom = 0\n let maxTo = docSize\n\n if (previousPluginState.isEditable !== isEditable) {\n // When the editable state changes, run on all nodes just to be safe\n minFrom = 0\n maxTo = docSize\n } else if (tr.docChanged) {\n // When the document changes, only run on the nodes that have changed\n minFrom = docSize\n maxTo = 0\n\n getChangedRanges(tr).forEach(range => {\n // Purposefully over scan the range to ensure we catch all decorations\n minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1)\n maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1)\n })\n } else if (tr.selectionSet) {\n const { $from, $to } = state.selection\n const { $from: $newFrom, $to: $newTo } = newState.selection\n\n // When the selection changes, run on all the nodes between the old and new selection\n minFrom = Math.min(\n // Purposefully over scan the range to ensure we catch all decorations\n $from.depth === 0 ? 0 : $from.before(),\n $newFrom.depth === 0 ? 0 : $newFrom.before(),\n )\n maxTo = Math.max(\n $to.depth === 0 ? maxTo : $to.after(),\n $newTo.depth === 0 ? maxTo : $newTo.after(),\n )\n }\n\n return {\n minFrom: Math.max(minFrom, 0),\n maxTo: Math.min(maxTo, docSize),\n }\n}\n\nexport const MathematicsPlugin = (options: MathematicsOptionsWithEditor) => {\n const {\n regex, katexOptions = {}, editor, shouldRender,\n } = options\n\n return new Plugin<PluginState>({\n key: new PluginKey('mathematics'),\n\n state: {\n init() {\n return { decorations: undefined, isEditable: undefined }\n },\n apply(tr, previousPluginState, state, newState) {\n\n if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {\n // Just reuse the existing decorations, since nothing should have changed\n return previousPluginState\n }\n\n const nextDecorationSet = (previousPluginState.decorations || DecorationSet.empty).map(\n tr.mapping,\n tr.doc,\n )\n const { selection } = newState\n const isEditable = editor.isEditable\n const decorationsToAdd = [] as Deco[]\n const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state)\n\n newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {\n const enabled = shouldRender(newState, pos, node)\n\n if (node.isText && node.text && enabled) {\n let match: RegExpExecArray | null\n\n // eslint-disable-next-line no-cond-assign\n while ((match = regex.exec(node.text))) {\n const from = pos + match.index\n const to = from + match[0].length\n const content = match.slice(1).find(Boolean)\n\n if (content) {\n const selectionSize = selection.from - selection.to\n const anchorIsInside = selection.anchor >= from && selection.anchor <= to\n const rangeIsInside = selection.from >= from && selection.to <= to\n const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside\n\n if (\n // Are the decorations already present?\n nextDecorationSet.find(\n from,\n to,\n (deco: DecoSpec) => isEditing === deco.isEditing\n && content === deco.content\n && isEditable === deco.isEditable\n && katexOptions === deco.katexOptions,\n ).length\n ) {\n // Decoration exists in set, no need to add it again\n continue\n }\n // Use an inline decoration to either hide original (preview is showing) or show it (editing \"mode\")\n decorationsToAdd.push(\n Decoration.inline(\n from,\n to,\n {\n class:\n isEditing && isEditable\n ? 'Tiptap-mathematics-editor'\n : 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',\n style:\n !isEditing || !isEditable\n ? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'\n : undefined,\n },\n {\n content,\n isEditable,\n isEditing,\n katexOptions,\n } satisfies DecoSpec,\n ),\n )\n\n if (!isEditable || !isEditing) {\n // Create decoration widget and add KaTeX preview if selection is not within the math-editor\n decorationsToAdd.push(\n Decoration.widget(\n from,\n () => {\n const element = document.createElement('span')\n\n // TODO: changeable class names\n element.classList.add('Tiptap-mathematics-render')\n\n if (isEditable) {\n element.classList.add('Tiptap-mathematics-render--editable')\n }\n\n try {\n katex.render(content!, element, katexOptions)\n } catch {\n element.innerHTML = content!\n }\n\n return element\n },\n {\n content,\n isEditable,\n isEditing,\n katexOptions,\n } satisfies DecoSpec,\n ),\n )\n }\n }\n }\n }\n })\n\n // Remove any decorations that exist at the same position, they will be replaced by the new decorations\n const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to))\n\n return {\n decorations: nextDecorationSet\n // Remove existing decorations that are going to be replaced\n .remove(decorationsToRemove)\n // Add any new decorations\n .add(tr.doc, decorationsToAdd),\n isEditable,\n }\n },\n },\n\n props: {\n decorations(state) {\n return this.getState(state)?.decorations ?? DecorationSet.empty\n },\n },\n })\n}\n","import { Extension } from '@tiptap/core'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport { MathematicsPlugin } from './MathematicsPlugin.js'\nimport { MathematicsOptions } from './types.js'\n\nexport const defaultShouldRender = (state: EditorState, pos: number) => {\n const $pos = state.doc.resolve(pos)\n const isInCodeBlock = $pos.parent.type.name === 'codeBlock'\n\n return !isInCodeBlock\n}\n\nexport const Mathematics = Extension.create<MathematicsOptions>({\n name: 'Mathematics',\n\n addOptions() {\n return {\n // eslint-disable-next-line no-useless-escape\n regex: /\\$([^\\$]*)\\$/gi,\n katexOptions: undefined,\n shouldRender: defaultShouldRender,\n }\n },\n\n addProseMirrorPlugins() {\n return [MathematicsPlugin({ ...this.options, editor: this.editor })]\n },\n})\n\nexport default Mathematics\n"],"names":["getChangedRanges","Plugin","PluginKey","DecorationSet","Decoration","katex","Extension"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,YAAY,CAAA;AAE1B,eAAe,WAAW,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { getChangedRanges, Extension } from '@tiptap/core';
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
import { DecorationSet, Decoration } from '@tiptap/pm/view';
|
|
4
|
+
import katex from 'katex';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the range of positions that have been affected by a transaction
|
|
8
|
+
*/
|
|
9
|
+
function getAffectedRange(newState, previousPluginState, isEditable, tr, state) {
|
|
10
|
+
const docSize = newState.doc.nodeSize - 2;
|
|
11
|
+
let minFrom = 0;
|
|
12
|
+
let maxTo = docSize;
|
|
13
|
+
if (previousPluginState.isEditable !== isEditable) {
|
|
14
|
+
// When the editable state changes, run on all nodes just to be safe
|
|
15
|
+
minFrom = 0;
|
|
16
|
+
maxTo = docSize;
|
|
17
|
+
}
|
|
18
|
+
else if (tr.docChanged) {
|
|
19
|
+
// When the document changes, only run on the nodes that have changed
|
|
20
|
+
minFrom = docSize;
|
|
21
|
+
maxTo = 0;
|
|
22
|
+
getChangedRanges(tr).forEach(range => {
|
|
23
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
24
|
+
minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1);
|
|
25
|
+
maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else if (tr.selectionSet) {
|
|
29
|
+
const { $from, $to } = state.selection;
|
|
30
|
+
const { $from: $newFrom, $to: $newTo } = newState.selection;
|
|
31
|
+
// When the selection changes, run on all the nodes between the old and new selection
|
|
32
|
+
minFrom = Math.min(
|
|
33
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
34
|
+
$from.depth === 0 ? 0 : $from.before(), $newFrom.depth === 0 ? 0 : $newFrom.before());
|
|
35
|
+
maxTo = Math.max($to.depth === 0 ? maxTo : $to.after(), $newTo.depth === 0 ? maxTo : $newTo.after());
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
minFrom: Math.max(minFrom, 0),
|
|
39
|
+
maxTo: Math.min(maxTo, docSize),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const MathematicsPlugin = (options) => {
|
|
43
|
+
const { regex, katexOptions = {}, editor, shouldRender, } = options;
|
|
44
|
+
return new Plugin({
|
|
45
|
+
key: new PluginKey('mathematics'),
|
|
46
|
+
state: {
|
|
47
|
+
init() {
|
|
48
|
+
return { decorations: undefined, isEditable: undefined };
|
|
49
|
+
},
|
|
50
|
+
apply(tr, previousPluginState, state, newState) {
|
|
51
|
+
if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {
|
|
52
|
+
// Just reuse the existing decorations, since nothing should have changed
|
|
53
|
+
return previousPluginState;
|
|
54
|
+
}
|
|
55
|
+
const nextDecorationSet = (previousPluginState.decorations || DecorationSet.empty).map(tr.mapping, tr.doc);
|
|
56
|
+
const { selection } = newState;
|
|
57
|
+
const isEditable = editor.isEditable;
|
|
58
|
+
const decorationsToAdd = [];
|
|
59
|
+
const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state);
|
|
60
|
+
newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {
|
|
61
|
+
const enabled = shouldRender(newState, pos, node);
|
|
62
|
+
if (node.isText && node.text && enabled) {
|
|
63
|
+
let match;
|
|
64
|
+
// eslint-disable-next-line no-cond-assign
|
|
65
|
+
while ((match = regex.exec(node.text))) {
|
|
66
|
+
const from = pos + match.index;
|
|
67
|
+
const to = from + match[0].length;
|
|
68
|
+
const content = match.slice(1).find(Boolean);
|
|
69
|
+
if (content) {
|
|
70
|
+
const selectionSize = selection.from - selection.to;
|
|
71
|
+
const anchorIsInside = selection.anchor >= from && selection.anchor <= to;
|
|
72
|
+
const rangeIsInside = selection.from >= from && selection.to <= to;
|
|
73
|
+
const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside;
|
|
74
|
+
if (
|
|
75
|
+
// Are the decorations already present?
|
|
76
|
+
nextDecorationSet.find(from, to, (deco) => isEditing === deco.isEditing
|
|
77
|
+
&& content === deco.content
|
|
78
|
+
&& isEditable === deco.isEditable
|
|
79
|
+
&& katexOptions === deco.katexOptions).length) {
|
|
80
|
+
// Decoration exists in set, no need to add it again
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Use an inline decoration to either hide original (preview is showing) or show it (editing "mode")
|
|
84
|
+
decorationsToAdd.push(Decoration.inline(from, to, {
|
|
85
|
+
class: isEditing && isEditable
|
|
86
|
+
? 'Tiptap-mathematics-editor'
|
|
87
|
+
: 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',
|
|
88
|
+
style: !isEditing || !isEditable
|
|
89
|
+
? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'
|
|
90
|
+
: undefined,
|
|
91
|
+
}, {
|
|
92
|
+
content,
|
|
93
|
+
isEditable,
|
|
94
|
+
isEditing,
|
|
95
|
+
katexOptions,
|
|
96
|
+
}));
|
|
97
|
+
if (!isEditable || !isEditing) {
|
|
98
|
+
// Create decoration widget and add KaTeX preview if selection is not within the math-editor
|
|
99
|
+
decorationsToAdd.push(Decoration.widget(from, () => {
|
|
100
|
+
const element = document.createElement('span');
|
|
101
|
+
// TODO: changeable class names
|
|
102
|
+
element.classList.add('Tiptap-mathematics-render');
|
|
103
|
+
if (isEditable) {
|
|
104
|
+
element.classList.add('Tiptap-mathematics-render--editable');
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
katex.render(content, element, katexOptions);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
element.innerHTML = content;
|
|
111
|
+
}
|
|
112
|
+
return element;
|
|
113
|
+
}, {
|
|
114
|
+
content,
|
|
115
|
+
isEditable,
|
|
116
|
+
isEditing,
|
|
117
|
+
katexOptions,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Remove any decorations that exist at the same position, they will be replaced by the new decorations
|
|
125
|
+
const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to));
|
|
126
|
+
return {
|
|
127
|
+
decorations: nextDecorationSet
|
|
128
|
+
// Remove existing decorations that are going to be replaced
|
|
129
|
+
.remove(decorationsToRemove)
|
|
130
|
+
// Add any new decorations
|
|
131
|
+
.add(tr.doc, decorationsToAdd),
|
|
132
|
+
isEditable,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
props: {
|
|
137
|
+
decorations(state) {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
return (_b = (_a = this.getState(state)) === null || _a === void 0 ? void 0 : _a.decorations) !== null && _b !== void 0 ? _b : DecorationSet.empty;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const defaultShouldRender = (state, pos) => {
|
|
146
|
+
const $pos = state.doc.resolve(pos);
|
|
147
|
+
const isInCodeBlock = $pos.parent.type.name === 'codeBlock';
|
|
148
|
+
return !isInCodeBlock;
|
|
149
|
+
};
|
|
150
|
+
const Mathematics = Extension.create({
|
|
151
|
+
name: 'Mathematics',
|
|
152
|
+
addOptions() {
|
|
153
|
+
return {
|
|
154
|
+
// eslint-disable-next-line no-useless-escape
|
|
155
|
+
regex: /\$([^\$]*)\$/gi,
|
|
156
|
+
katexOptions: undefined,
|
|
157
|
+
shouldRender: defaultShouldRender,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
addProseMirrorPlugins() {
|
|
161
|
+
return [MathematicsPlugin({ ...this.options, editor: this.editor })];
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export { Mathematics, MathematicsPlugin, Mathematics as default, defaultShouldRender };
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/MathematicsPlugin.ts","../src/mathematics.ts"],"sourcesContent":["import { getChangedRanges } from '@tiptap/core'\nimport {\n EditorState, Plugin, PluginKey, Transaction,\n} from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\nimport katex from 'katex'\n\nimport { MathematicsOptionsWithEditor } from './types.js'\n\ntype DecoSpec = {\n isEditable: boolean;\n isEditing: boolean;\n katexOptions: MathematicsOptionsWithEditor['katexOptions'];\n content: string;\n};\n\ntype Deco = Omit<Decoration, 'spec'> & { spec: DecoSpec };\n\ntype PluginState =\n| { decorations: DecorationSet; isEditable: boolean }\n| { decorations: undefined; isEditable: undefined }\n\n/**\n * Get the range of positions that have been affected by a transaction\n */\nfunction getAffectedRange(newState: EditorState, previousPluginState: PluginState, isEditable: boolean, tr: Transaction, state: EditorState) {\n const docSize = newState.doc.nodeSize - 2\n let minFrom = 0\n let maxTo = docSize\n\n if (previousPluginState.isEditable !== isEditable) {\n // When the editable state changes, run on all nodes just to be safe\n minFrom = 0\n maxTo = docSize\n } else if (tr.docChanged) {\n // When the document changes, only run on the nodes that have changed\n minFrom = docSize\n maxTo = 0\n\n getChangedRanges(tr).forEach(range => {\n // Purposefully over scan the range to ensure we catch all decorations\n minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1)\n maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1)\n })\n } else if (tr.selectionSet) {\n const { $from, $to } = state.selection\n const { $from: $newFrom, $to: $newTo } = newState.selection\n\n // When the selection changes, run on all the nodes between the old and new selection\n minFrom = Math.min(\n // Purposefully over scan the range to ensure we catch all decorations\n $from.depth === 0 ? 0 : $from.before(),\n $newFrom.depth === 0 ? 0 : $newFrom.before(),\n )\n maxTo = Math.max(\n $to.depth === 0 ? maxTo : $to.after(),\n $newTo.depth === 0 ? maxTo : $newTo.after(),\n )\n }\n\n return {\n minFrom: Math.max(minFrom, 0),\n maxTo: Math.min(maxTo, docSize),\n }\n}\n\nexport const MathematicsPlugin = (options: MathematicsOptionsWithEditor) => {\n const {\n regex, katexOptions = {}, editor, shouldRender,\n } = options\n\n return new Plugin<PluginState>({\n key: new PluginKey('mathematics'),\n\n state: {\n init() {\n return { decorations: undefined, isEditable: undefined }\n },\n apply(tr, previousPluginState, state, newState) {\n\n if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {\n // Just reuse the existing decorations, since nothing should have changed\n return previousPluginState\n }\n\n const nextDecorationSet = (previousPluginState.decorations || DecorationSet.empty).map(\n tr.mapping,\n tr.doc,\n )\n const { selection } = newState\n const isEditable = editor.isEditable\n const decorationsToAdd = [] as Deco[]\n const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state)\n\n newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {\n const enabled = shouldRender(newState, pos, node)\n\n if (node.isText && node.text && enabled) {\n let match: RegExpExecArray | null\n\n // eslint-disable-next-line no-cond-assign\n while ((match = regex.exec(node.text))) {\n const from = pos + match.index\n const to = from + match[0].length\n const content = match.slice(1).find(Boolean)\n\n if (content) {\n const selectionSize = selection.from - selection.to\n const anchorIsInside = selection.anchor >= from && selection.anchor <= to\n const rangeIsInside = selection.from >= from && selection.to <= to\n const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside\n\n if (\n // Are the decorations already present?\n nextDecorationSet.find(\n from,\n to,\n (deco: DecoSpec) => isEditing === deco.isEditing\n && content === deco.content\n && isEditable === deco.isEditable\n && katexOptions === deco.katexOptions,\n ).length\n ) {\n // Decoration exists in set, no need to add it again\n continue\n }\n // Use an inline decoration to either hide original (preview is showing) or show it (editing \"mode\")\n decorationsToAdd.push(\n Decoration.inline(\n from,\n to,\n {\n class:\n isEditing && isEditable\n ? 'Tiptap-mathematics-editor'\n : 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',\n style:\n !isEditing || !isEditable\n ? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'\n : undefined,\n },\n {\n content,\n isEditable,\n isEditing,\n katexOptions,\n } satisfies DecoSpec,\n ),\n )\n\n if (!isEditable || !isEditing) {\n // Create decoration widget and add KaTeX preview if selection is not within the math-editor\n decorationsToAdd.push(\n Decoration.widget(\n from,\n () => {\n const element = document.createElement('span')\n\n // TODO: changeable class names\n element.classList.add('Tiptap-mathematics-render')\n\n if (isEditable) {\n element.classList.add('Tiptap-mathematics-render--editable')\n }\n\n try {\n katex.render(content!, element, katexOptions)\n } catch {\n element.innerHTML = content!\n }\n\n return element\n },\n {\n content,\n isEditable,\n isEditing,\n katexOptions,\n } satisfies DecoSpec,\n ),\n )\n }\n }\n }\n }\n })\n\n // Remove any decorations that exist at the same position, they will be replaced by the new decorations\n const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to))\n\n return {\n decorations: nextDecorationSet\n // Remove existing decorations that are going to be replaced\n .remove(decorationsToRemove)\n // Add any new decorations\n .add(tr.doc, decorationsToAdd),\n isEditable,\n }\n },\n },\n\n props: {\n decorations(state) {\n return this.getState(state)?.decorations ?? DecorationSet.empty\n },\n },\n })\n}\n","import { Extension } from '@tiptap/core'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport { MathematicsPlugin } from './MathematicsPlugin.js'\nimport { MathematicsOptions } from './types.js'\n\nexport const defaultShouldRender = (state: EditorState, pos: number) => {\n const $pos = state.doc.resolve(pos)\n const isInCodeBlock = $pos.parent.type.name === 'codeBlock'\n\n return !isInCodeBlock\n}\n\nexport const Mathematics = Extension.create<MathematicsOptions>({\n name: 'Mathematics',\n\n addOptions() {\n return {\n // eslint-disable-next-line no-useless-escape\n regex: /\\$([^\\$]*)\\$/gi,\n katexOptions: undefined,\n shouldRender: defaultShouldRender,\n }\n },\n\n addProseMirrorPlugins() {\n return [MathematicsPlugin({ ...this.options, editor: this.editor })]\n },\n})\n\nexport default Mathematics\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('@tiptap/pm/state'), require('@tiptap/pm/view'), require('katex')) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', '@tiptap/pm/state', '@tiptap/pm/view', 'katex'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-mathematics"] = {}, global.core, global.state, global.view, global.katex));
|
|
5
|
+
})(this, (function (exports, core, state, view, katex) { 'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the range of positions that have been affected by a transaction
|
|
9
|
+
*/
|
|
10
|
+
function getAffectedRange(newState, previousPluginState, isEditable, tr, state) {
|
|
11
|
+
const docSize = newState.doc.nodeSize - 2;
|
|
12
|
+
let minFrom = 0;
|
|
13
|
+
let maxTo = docSize;
|
|
14
|
+
if (previousPluginState.isEditable !== isEditable) {
|
|
15
|
+
// When the editable state changes, run on all nodes just to be safe
|
|
16
|
+
minFrom = 0;
|
|
17
|
+
maxTo = docSize;
|
|
18
|
+
}
|
|
19
|
+
else if (tr.docChanged) {
|
|
20
|
+
// When the document changes, only run on the nodes that have changed
|
|
21
|
+
minFrom = docSize;
|
|
22
|
+
maxTo = 0;
|
|
23
|
+
core.getChangedRanges(tr).forEach(range => {
|
|
24
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
25
|
+
minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1);
|
|
26
|
+
maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (tr.selectionSet) {
|
|
30
|
+
const { $from, $to } = state.selection;
|
|
31
|
+
const { $from: $newFrom, $to: $newTo } = newState.selection;
|
|
32
|
+
// When the selection changes, run on all the nodes between the old and new selection
|
|
33
|
+
minFrom = Math.min(
|
|
34
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
35
|
+
$from.depth === 0 ? 0 : $from.before(), $newFrom.depth === 0 ? 0 : $newFrom.before());
|
|
36
|
+
maxTo = Math.max($to.depth === 0 ? maxTo : $to.after(), $newTo.depth === 0 ? maxTo : $newTo.after());
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
minFrom: Math.max(minFrom, 0),
|
|
40
|
+
maxTo: Math.min(maxTo, docSize),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const MathematicsPlugin = (options) => {
|
|
44
|
+
const { regex, katexOptions = {}, editor, shouldRender, } = options;
|
|
45
|
+
return new state.Plugin({
|
|
46
|
+
key: new state.PluginKey('mathematics'),
|
|
47
|
+
state: {
|
|
48
|
+
init() {
|
|
49
|
+
return { decorations: undefined, isEditable: undefined };
|
|
50
|
+
},
|
|
51
|
+
apply(tr, previousPluginState, state, newState) {
|
|
52
|
+
if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {
|
|
53
|
+
// Just reuse the existing decorations, since nothing should have changed
|
|
54
|
+
return previousPluginState;
|
|
55
|
+
}
|
|
56
|
+
const nextDecorationSet = (previousPluginState.decorations || view.DecorationSet.empty).map(tr.mapping, tr.doc);
|
|
57
|
+
const { selection } = newState;
|
|
58
|
+
const isEditable = editor.isEditable;
|
|
59
|
+
const decorationsToAdd = [];
|
|
60
|
+
const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state);
|
|
61
|
+
newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {
|
|
62
|
+
const enabled = shouldRender(newState, pos, node);
|
|
63
|
+
if (node.isText && node.text && enabled) {
|
|
64
|
+
let match;
|
|
65
|
+
// eslint-disable-next-line no-cond-assign
|
|
66
|
+
while ((match = regex.exec(node.text))) {
|
|
67
|
+
const from = pos + match.index;
|
|
68
|
+
const to = from + match[0].length;
|
|
69
|
+
const content = match.slice(1).find(Boolean);
|
|
70
|
+
if (content) {
|
|
71
|
+
const selectionSize = selection.from - selection.to;
|
|
72
|
+
const anchorIsInside = selection.anchor >= from && selection.anchor <= to;
|
|
73
|
+
const rangeIsInside = selection.from >= from && selection.to <= to;
|
|
74
|
+
const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside;
|
|
75
|
+
if (
|
|
76
|
+
// Are the decorations already present?
|
|
77
|
+
nextDecorationSet.find(from, to, (deco) => isEditing === deco.isEditing
|
|
78
|
+
&& content === deco.content
|
|
79
|
+
&& isEditable === deco.isEditable
|
|
80
|
+
&& katexOptions === deco.katexOptions).length) {
|
|
81
|
+
// Decoration exists in set, no need to add it again
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Use an inline decoration to either hide original (preview is showing) or show it (editing "mode")
|
|
85
|
+
decorationsToAdd.push(view.Decoration.inline(from, to, {
|
|
86
|
+
class: isEditing && isEditable
|
|
87
|
+
? 'Tiptap-mathematics-editor'
|
|
88
|
+
: 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',
|
|
89
|
+
style: !isEditing || !isEditable
|
|
90
|
+
? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'
|
|
91
|
+
: undefined,
|
|
92
|
+
}, {
|
|
93
|
+
content,
|
|
94
|
+
isEditable,
|
|
95
|
+
isEditing,
|
|
96
|
+
katexOptions,
|
|
97
|
+
}));
|
|
98
|
+
if (!isEditable || !isEditing) {
|
|
99
|
+
// Create decoration widget and add KaTeX preview if selection is not within the math-editor
|
|
100
|
+
decorationsToAdd.push(view.Decoration.widget(from, () => {
|
|
101
|
+
const element = document.createElement('span');
|
|
102
|
+
// TODO: changeable class names
|
|
103
|
+
element.classList.add('Tiptap-mathematics-render');
|
|
104
|
+
if (isEditable) {
|
|
105
|
+
element.classList.add('Tiptap-mathematics-render--editable');
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
katex.render(content, element, katexOptions);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
element.innerHTML = content;
|
|
112
|
+
}
|
|
113
|
+
return element;
|
|
114
|
+
}, {
|
|
115
|
+
content,
|
|
116
|
+
isEditable,
|
|
117
|
+
isEditing,
|
|
118
|
+
katexOptions,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Remove any decorations that exist at the same position, they will be replaced by the new decorations
|
|
126
|
+
const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to));
|
|
127
|
+
return {
|
|
128
|
+
decorations: nextDecorationSet
|
|
129
|
+
// Remove existing decorations that are going to be replaced
|
|
130
|
+
.remove(decorationsToRemove)
|
|
131
|
+
// Add any new decorations
|
|
132
|
+
.add(tr.doc, decorationsToAdd),
|
|
133
|
+
isEditable,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
props: {
|
|
138
|
+
decorations(state) {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
return (_b = (_a = this.getState(state)) === null || _a === void 0 ? void 0 : _a.decorations) !== null && _b !== void 0 ? _b : view.DecorationSet.empty;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const defaultShouldRender = (state, pos) => {
|
|
147
|
+
const $pos = state.doc.resolve(pos);
|
|
148
|
+
const isInCodeBlock = $pos.parent.type.name === 'codeBlock';
|
|
149
|
+
return !isInCodeBlock;
|
|
150
|
+
};
|
|
151
|
+
const Mathematics = core.Extension.create({
|
|
152
|
+
name: 'Mathematics',
|
|
153
|
+
addOptions() {
|
|
154
|
+
return {
|
|
155
|
+
// eslint-disable-next-line no-useless-escape
|
|
156
|
+
regex: /\$([^\$]*)\$/gi,
|
|
157
|
+
katexOptions: undefined,
|
|
158
|
+
shouldRender: defaultShouldRender,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
addProseMirrorPlugins() {
|
|
162
|
+
return [MathematicsPlugin({ ...this.options, editor: this.editor })];
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
exports.Mathematics = Mathematics;
|
|
167
|
+
exports.MathematicsPlugin = MathematicsPlugin;
|
|
168
|
+
exports.default = Mathematics;
|
|
169
|
+
exports.defaultShouldRender = defaultShouldRender;
|
|
170
|
+
|
|
171
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
172
|
+
|
|
173
|
+
}));
|
|
174
|
+
//# sourceMappingURL=index.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/MathematicsPlugin.ts","../src/mathematics.ts"],"sourcesContent":["import { getChangedRanges } from '@tiptap/core'\nimport {\n EditorState, Plugin, PluginKey, Transaction,\n} from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\nimport katex from 'katex'\n\nimport { MathematicsOptionsWithEditor } from './types.js'\n\ntype DecoSpec = {\n isEditable: boolean;\n isEditing: boolean;\n katexOptions: MathematicsOptionsWithEditor['katexOptions'];\n content: string;\n};\n\ntype Deco = Omit<Decoration, 'spec'> & { spec: DecoSpec };\n\ntype PluginState =\n| { decorations: DecorationSet; isEditable: boolean }\n| { decorations: undefined; isEditable: undefined }\n\n/**\n * Get the range of positions that have been affected by a transaction\n */\nfunction getAffectedRange(newState: EditorState, previousPluginState: PluginState, isEditable: boolean, tr: Transaction, state: EditorState) {\n const docSize = newState.doc.nodeSize - 2\n let minFrom = 0\n let maxTo = docSize\n\n if (previousPluginState.isEditable !== isEditable) {\n // When the editable state changes, run on all nodes just to be safe\n minFrom = 0\n maxTo = docSize\n } else if (tr.docChanged) {\n // When the document changes, only run on the nodes that have changed\n minFrom = docSize\n maxTo = 0\n\n getChangedRanges(tr).forEach(range => {\n // Purposefully over scan the range to ensure we catch all decorations\n minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1)\n maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1)\n })\n } else if (tr.selectionSet) {\n const { $from, $to } = state.selection\n const { $from: $newFrom, $to: $newTo } = newState.selection\n\n // When the selection changes, run on all the nodes between the old and new selection\n minFrom = Math.min(\n // Purposefully over scan the range to ensure we catch all decorations\n $from.depth === 0 ? 0 : $from.before(),\n $newFrom.depth === 0 ? 0 : $newFrom.before(),\n )\n maxTo = Math.max(\n $to.depth === 0 ? maxTo : $to.after(),\n $newTo.depth === 0 ? maxTo : $newTo.after(),\n )\n }\n\n return {\n minFrom: Math.max(minFrom, 0),\n maxTo: Math.min(maxTo, docSize),\n }\n}\n\nexport const MathematicsPlugin = (options: MathematicsOptionsWithEditor) => {\n const {\n regex, katexOptions = {}, editor, shouldRender,\n } = options\n\n return new Plugin<PluginState>({\n key: new PluginKey('mathematics'),\n\n state: {\n init() {\n return { decorations: undefined, isEditable: undefined }\n },\n apply(tr, previousPluginState, state, newState) {\n\n if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {\n // Just reuse the existing decorations, since nothing should have changed\n return previousPluginState\n }\n\n const nextDecorationSet = (previousPluginState.decorations || DecorationSet.empty).map(\n tr.mapping,\n tr.doc,\n )\n const { selection } = newState\n const isEditable = editor.isEditable\n const decorationsToAdd = [] as Deco[]\n const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state)\n\n newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {\n const enabled = shouldRender(newState, pos, node)\n\n if (node.isText && node.text && enabled) {\n let match: RegExpExecArray | null\n\n // eslint-disable-next-line no-cond-assign\n while ((match = regex.exec(node.text))) {\n const from = pos + match.index\n const to = from + match[0].length\n const content = match.slice(1).find(Boolean)\n\n if (content) {\n const selectionSize = selection.from - selection.to\n const anchorIsInside = selection.anchor >= from && selection.anchor <= to\n const rangeIsInside = selection.from >= from && selection.to <= to\n const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside\n\n if (\n // Are the decorations already present?\n nextDecorationSet.find(\n from,\n to,\n (deco: DecoSpec) => isEditing === deco.isEditing\n && content === deco.content\n && isEditable === deco.isEditable\n && katexOptions === deco.katexOptions,\n ).length\n ) {\n // Decoration exists in set, no need to add it again\n continue\n }\n // Use an inline decoration to either hide original (preview is showing) or show it (editing \"mode\")\n decorationsToAdd.push(\n Decoration.inline(\n from,\n to,\n {\n class:\n isEditing && isEditable\n ? 'Tiptap-mathematics-editor'\n : 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',\n style:\n !isEditing || !isEditable\n ? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'\n : undefined,\n },\n {\n content,\n isEditable,\n isEditing,\n katexOptions,\n } satisfies DecoSpec,\n ),\n )\n\n if (!isEditable || !isEditing) {\n // Create decoration widget and add KaTeX preview if selection is not within the math-editor\n decorationsToAdd.push(\n Decoration.widget(\n from,\n () => {\n const element = document.createElement('span')\n\n // TODO: changeable class names\n element.classList.add('Tiptap-mathematics-render')\n\n if (isEditable) {\n element.classList.add('Tiptap-mathematics-render--editable')\n }\n\n try {\n katex.render(content!, element, katexOptions)\n } catch {\n element.innerHTML = content!\n }\n\n return element\n },\n {\n content,\n isEditable,\n isEditing,\n katexOptions,\n } satisfies DecoSpec,\n ),\n )\n }\n }\n }\n }\n })\n\n // Remove any decorations that exist at the same position, they will be replaced by the new decorations\n const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to))\n\n return {\n decorations: nextDecorationSet\n // Remove existing decorations that are going to be replaced\n .remove(decorationsToRemove)\n // Add any new decorations\n .add(tr.doc, decorationsToAdd),\n isEditable,\n }\n },\n },\n\n props: {\n decorations(state) {\n return this.getState(state)?.decorations ?? DecorationSet.empty\n },\n },\n })\n}\n","import { Extension } from '@tiptap/core'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport { MathematicsPlugin } from './MathematicsPlugin.js'\nimport { MathematicsOptions } from './types.js'\n\nexport const defaultShouldRender = (state: EditorState, pos: number) => {\n const $pos = state.doc.resolve(pos)\n const isInCodeBlock = $pos.parent.type.name === 'codeBlock'\n\n return !isInCodeBlock\n}\n\nexport const Mathematics = Extension.create<MathematicsOptions>({\n name: 'Mathematics',\n\n addOptions() {\n return {\n // eslint-disable-next-line no-useless-escape\n regex: /\\$([^\\$]*)\\$/gi,\n katexOptions: undefined,\n shouldRender: defaultShouldRender,\n }\n },\n\n addProseMirrorPlugins() {\n return [MathematicsPlugin({ ...this.options, editor: this.editor })]\n },\n})\n\nexport default Mathematics\n"],"names":["getChangedRanges","Plugin","PluginKey","DecorationSet","Decoration","Extension"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { EditorState } from '@tiptap/pm/state';
|
|
3
|
+
import { MathematicsOptions } from './types.js';
|
|
4
|
+
export declare const defaultShouldRender: (state: EditorState, pos: number) => boolean;
|
|
5
|
+
export declare const Mathematics: Extension<MathematicsOptions, any>;
|
|
6
|
+
export default Mathematics;
|
|
7
|
+
//# sourceMappingURL=mathematics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mathematics.d.ts","sourceRoot":"","sources":["../src/mathematics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAG9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,mBAAmB,UAAW,WAAW,OAAO,MAAM,YAKlE,CAAA;AAED,eAAO,MAAM,WAAW,oCAetB,CAAA;AAEF,eAAe,WAAW,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import { Node } from '@tiptap/pm/model';
|
|
3
|
+
import { EditorState } from '@tiptap/pm/state';
|
|
4
|
+
import { KatexOptions } from 'katex';
|
|
5
|
+
export type MathematicsOptions = {
|
|
6
|
+
regex: RegExp;
|
|
7
|
+
katexOptions?: KatexOptions;
|
|
8
|
+
shouldRender: (state: EditorState, pos: number, node: Node) => boolean;
|
|
9
|
+
};
|
|
10
|
+
export type MathematicsOptionsWithEditor = MathematicsOptions & {
|
|
11
|
+
editor: Editor;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAEpC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,YAAY,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC;CACxE,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,kBAAkB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tiptap/extension-mathematics",
|
|
3
|
+
"description": "latex math extension for tiptap",
|
|
4
|
+
"version": "2.22.0",
|
|
5
|
+
"homepage": "https://tiptap.dev/api/extensions/mathematics",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"tiptap",
|
|
8
|
+
"tiptap extension"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"funding": {
|
|
12
|
+
"type": "github",
|
|
13
|
+
"url": "https://github.com/sponsors/ueberdosis"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/ueberdosis/tiptap",
|
|
18
|
+
"directory": "packages/extension-mathematics"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"main": "dist/index.cjs",
|
|
29
|
+
"module": "dist/index.js",
|
|
30
|
+
"umd": "dist/index.umd.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@tiptap/core": "^2.22.0",
|
|
38
|
+
"@tiptap/pm": "^2.22.0",
|
|
39
|
+
"@types/katex": "^0.16.7"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@tiptap/core": "^2.7.0",
|
|
43
|
+
"@tiptap/pm": "^2.7.0",
|
|
44
|
+
"katex": "^0.16.4"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"clean": "rm -rf dist",
|
|
48
|
+
"build": "npm run clean && rollup -c"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { getChangedRanges } from '@tiptap/core'
|
|
2
|
+
import {
|
|
3
|
+
EditorState, Plugin, PluginKey, Transaction,
|
|
4
|
+
} from '@tiptap/pm/state'
|
|
5
|
+
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
|
6
|
+
import katex from 'katex'
|
|
7
|
+
|
|
8
|
+
import { MathematicsOptionsWithEditor } from './types.js'
|
|
9
|
+
|
|
10
|
+
type DecoSpec = {
|
|
11
|
+
isEditable: boolean;
|
|
12
|
+
isEditing: boolean;
|
|
13
|
+
katexOptions: MathematicsOptionsWithEditor['katexOptions'];
|
|
14
|
+
content: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Deco = Omit<Decoration, 'spec'> & { spec: DecoSpec };
|
|
18
|
+
|
|
19
|
+
type PluginState =
|
|
20
|
+
| { decorations: DecorationSet; isEditable: boolean }
|
|
21
|
+
| { decorations: undefined; isEditable: undefined }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the range of positions that have been affected by a transaction
|
|
25
|
+
*/
|
|
26
|
+
function getAffectedRange(newState: EditorState, previousPluginState: PluginState, isEditable: boolean, tr: Transaction, state: EditorState) {
|
|
27
|
+
const docSize = newState.doc.nodeSize - 2
|
|
28
|
+
let minFrom = 0
|
|
29
|
+
let maxTo = docSize
|
|
30
|
+
|
|
31
|
+
if (previousPluginState.isEditable !== isEditable) {
|
|
32
|
+
// When the editable state changes, run on all nodes just to be safe
|
|
33
|
+
minFrom = 0
|
|
34
|
+
maxTo = docSize
|
|
35
|
+
} else if (tr.docChanged) {
|
|
36
|
+
// When the document changes, only run on the nodes that have changed
|
|
37
|
+
minFrom = docSize
|
|
38
|
+
maxTo = 0
|
|
39
|
+
|
|
40
|
+
getChangedRanges(tr).forEach(range => {
|
|
41
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
42
|
+
minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1)
|
|
43
|
+
maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1)
|
|
44
|
+
})
|
|
45
|
+
} else if (tr.selectionSet) {
|
|
46
|
+
const { $from, $to } = state.selection
|
|
47
|
+
const { $from: $newFrom, $to: $newTo } = newState.selection
|
|
48
|
+
|
|
49
|
+
// When the selection changes, run on all the nodes between the old and new selection
|
|
50
|
+
minFrom = Math.min(
|
|
51
|
+
// Purposefully over scan the range to ensure we catch all decorations
|
|
52
|
+
$from.depth === 0 ? 0 : $from.before(),
|
|
53
|
+
$newFrom.depth === 0 ? 0 : $newFrom.before(),
|
|
54
|
+
)
|
|
55
|
+
maxTo = Math.max(
|
|
56
|
+
$to.depth === 0 ? maxTo : $to.after(),
|
|
57
|
+
$newTo.depth === 0 ? maxTo : $newTo.after(),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
minFrom: Math.max(minFrom, 0),
|
|
63
|
+
maxTo: Math.min(maxTo, docSize),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const MathematicsPlugin = (options: MathematicsOptionsWithEditor) => {
|
|
68
|
+
const {
|
|
69
|
+
regex, katexOptions = {}, editor, shouldRender,
|
|
70
|
+
} = options
|
|
71
|
+
|
|
72
|
+
return new Plugin<PluginState>({
|
|
73
|
+
key: new PluginKey('mathematics'),
|
|
74
|
+
|
|
75
|
+
state: {
|
|
76
|
+
init() {
|
|
77
|
+
return { decorations: undefined, isEditable: undefined }
|
|
78
|
+
},
|
|
79
|
+
apply(tr, previousPluginState, state, newState) {
|
|
80
|
+
|
|
81
|
+
if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {
|
|
82
|
+
// Just reuse the existing decorations, since nothing should have changed
|
|
83
|
+
return previousPluginState
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const nextDecorationSet = (previousPluginState.decorations || DecorationSet.empty).map(
|
|
87
|
+
tr.mapping,
|
|
88
|
+
tr.doc,
|
|
89
|
+
)
|
|
90
|
+
const { selection } = newState
|
|
91
|
+
const isEditable = editor.isEditable
|
|
92
|
+
const decorationsToAdd = [] as Deco[]
|
|
93
|
+
const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state)
|
|
94
|
+
|
|
95
|
+
newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {
|
|
96
|
+
const enabled = shouldRender(newState, pos, node)
|
|
97
|
+
|
|
98
|
+
if (node.isText && node.text && enabled) {
|
|
99
|
+
let match: RegExpExecArray | null
|
|
100
|
+
|
|
101
|
+
// eslint-disable-next-line no-cond-assign
|
|
102
|
+
while ((match = regex.exec(node.text))) {
|
|
103
|
+
const from = pos + match.index
|
|
104
|
+
const to = from + match[0].length
|
|
105
|
+
const content = match.slice(1).find(Boolean)
|
|
106
|
+
|
|
107
|
+
if (content) {
|
|
108
|
+
const selectionSize = selection.from - selection.to
|
|
109
|
+
const anchorIsInside = selection.anchor >= from && selection.anchor <= to
|
|
110
|
+
const rangeIsInside = selection.from >= from && selection.to <= to
|
|
111
|
+
const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
// Are the decorations already present?
|
|
115
|
+
nextDecorationSet.find(
|
|
116
|
+
from,
|
|
117
|
+
to,
|
|
118
|
+
(deco: DecoSpec) => isEditing === deco.isEditing
|
|
119
|
+
&& content === deco.content
|
|
120
|
+
&& isEditable === deco.isEditable
|
|
121
|
+
&& katexOptions === deco.katexOptions,
|
|
122
|
+
).length
|
|
123
|
+
) {
|
|
124
|
+
// Decoration exists in set, no need to add it again
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
// Use an inline decoration to either hide original (preview is showing) or show it (editing "mode")
|
|
128
|
+
decorationsToAdd.push(
|
|
129
|
+
Decoration.inline(
|
|
130
|
+
from,
|
|
131
|
+
to,
|
|
132
|
+
{
|
|
133
|
+
class:
|
|
134
|
+
isEditing && isEditable
|
|
135
|
+
? 'Tiptap-mathematics-editor'
|
|
136
|
+
: 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',
|
|
137
|
+
style:
|
|
138
|
+
!isEditing || !isEditable
|
|
139
|
+
? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'
|
|
140
|
+
: undefined,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
content,
|
|
144
|
+
isEditable,
|
|
145
|
+
isEditing,
|
|
146
|
+
katexOptions,
|
|
147
|
+
} satisfies DecoSpec,
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (!isEditable || !isEditing) {
|
|
152
|
+
// Create decoration widget and add KaTeX preview if selection is not within the math-editor
|
|
153
|
+
decorationsToAdd.push(
|
|
154
|
+
Decoration.widget(
|
|
155
|
+
from,
|
|
156
|
+
() => {
|
|
157
|
+
const element = document.createElement('span')
|
|
158
|
+
|
|
159
|
+
// TODO: changeable class names
|
|
160
|
+
element.classList.add('Tiptap-mathematics-render')
|
|
161
|
+
|
|
162
|
+
if (isEditable) {
|
|
163
|
+
element.classList.add('Tiptap-mathematics-render--editable')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
katex.render(content!, element, katexOptions)
|
|
168
|
+
} catch {
|
|
169
|
+
element.innerHTML = content!
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return element
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
content,
|
|
176
|
+
isEditable,
|
|
177
|
+
isEditing,
|
|
178
|
+
katexOptions,
|
|
179
|
+
} satisfies DecoSpec,
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Remove any decorations that exist at the same position, they will be replaced by the new decorations
|
|
189
|
+
const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to))
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
decorations: nextDecorationSet
|
|
193
|
+
// Remove existing decorations that are going to be replaced
|
|
194
|
+
.remove(decorationsToRemove)
|
|
195
|
+
// Add any new decorations
|
|
196
|
+
.add(tr.doc, decorationsToAdd),
|
|
197
|
+
isEditable,
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
props: {
|
|
203
|
+
decorations(state) {
|
|
204
|
+
return this.getState(state)?.decorations ?? DecorationSet.empty
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core'
|
|
2
|
+
import { EditorState } from '@tiptap/pm/state'
|
|
3
|
+
|
|
4
|
+
import { MathematicsPlugin } from './MathematicsPlugin.js'
|
|
5
|
+
import { MathematicsOptions } from './types.js'
|
|
6
|
+
|
|
7
|
+
export const defaultShouldRender = (state: EditorState, pos: number) => {
|
|
8
|
+
const $pos = state.doc.resolve(pos)
|
|
9
|
+
const isInCodeBlock = $pos.parent.type.name === 'codeBlock'
|
|
10
|
+
|
|
11
|
+
return !isInCodeBlock
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Mathematics = Extension.create<MathematicsOptions>({
|
|
15
|
+
name: 'Mathematics',
|
|
16
|
+
|
|
17
|
+
addOptions() {
|
|
18
|
+
return {
|
|
19
|
+
// eslint-disable-next-line no-useless-escape
|
|
20
|
+
regex: /\$([^\$]*)\$/gi,
|
|
21
|
+
katexOptions: undefined,
|
|
22
|
+
shouldRender: defaultShouldRender,
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
addProseMirrorPlugins() {
|
|
27
|
+
return [MathematicsPlugin({ ...this.options, editor: this.editor })]
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export default Mathematics
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core'
|
|
2
|
+
import { Node } from '@tiptap/pm/model'
|
|
3
|
+
import { EditorState } from '@tiptap/pm/state'
|
|
4
|
+
import { KatexOptions } from 'katex'
|
|
5
|
+
|
|
6
|
+
export type MathematicsOptions = {
|
|
7
|
+
regex: RegExp,
|
|
8
|
+
katexOptions?: KatexOptions,
|
|
9
|
+
shouldRender: (state: EditorState, pos: number, node: Node) => boolean,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type MathematicsOptionsWithEditor = MathematicsOptions & { editor: Editor }
|