prosemirror-math 0.1.0 → 0.2.2
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/LICENSE +1 -1
- package/README.md +26 -31
- package/dist/{prosemirror-math.d.ts → index.d.ts} +50 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/{prosemirror-math.js → index.js} +83 -16
- package/dist/index.js.map +1 -0
- package/package.json +43 -44
- package/src/create-element.spec.ts +1 -1
- package/src/cursor-inside-plugin.spec.ts +16 -16
- package/src/cursor-inside-plugin.ts +6 -4
- package/src/index.ts +10 -7
- package/src/math-block-enter-rule.spec.ts +3 -5
- package/src/math-block-enter-rule.ts +15 -5
- package/src/math-block-spec.spec.ts +2 -2
- package/src/math-block-spec.ts +7 -2
- package/src/math-block-view.spec.ts +14 -8
- package/src/math-block-view.ts +23 -7
- package/src/math-inline-input-rule.spec.ts +17 -16
- package/src/math-inline-input-rule.ts +28 -20
- package/src/math-inline-spec.spec.ts +5 -3
- package/src/math-inline-spec.ts +7 -2
- package/src/math-inline-view.spec.ts +14 -8
- package/src/math-inline-view.ts +20 -7
- package/src/math-view-render.ts +31 -2
- package/src/temml.ts +10 -2
- package/src/testing.ts +15 -21
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# prosemirror-math
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/prosemirror-math)
|
|
4
|
+
|
|
5
|
+
Rendering math expressions in [ProseMirror](https://prosemirror.net/).
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -23,7 +25,12 @@ import { Schema } from 'prosemirror-model'
|
|
|
23
25
|
const schema = new Schema({
|
|
24
26
|
nodes: {
|
|
25
27
|
doc: { content: 'block+' },
|
|
26
|
-
paragraph: {
|
|
28
|
+
paragraph: {
|
|
29
|
+
content: 'inline*',
|
|
30
|
+
group: 'block',
|
|
31
|
+
parseDOM: [{ tag: 'p' }],
|
|
32
|
+
toDOM: () => ['p', 0],
|
|
33
|
+
},
|
|
27
34
|
text: { group: 'inline' },
|
|
28
35
|
mathBlock: { ...mathBlockSpec },
|
|
29
36
|
mathInline: { ...mathInlineSpec },
|
|
@@ -43,12 +50,22 @@ import Temml from 'temml'
|
|
|
43
50
|
const view = new EditorView(document.body, {
|
|
44
51
|
state,
|
|
45
52
|
nodeViews: {
|
|
46
|
-
mathBlock: (node, view, getPos, decorations) =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
mathBlock: (node, view, getPos, decorations) =>
|
|
54
|
+
createMathBlockView(
|
|
55
|
+
(text, element) => {
|
|
56
|
+
Temml.render(text, element, { displayMode: true })
|
|
57
|
+
},
|
|
58
|
+
node,
|
|
59
|
+
decorations,
|
|
60
|
+
),
|
|
61
|
+
mathInline: (node, view, getPos, decorations) =>
|
|
62
|
+
createMathInlineView(
|
|
63
|
+
(text, element) => {
|
|
64
|
+
Temml.render(text, element, { displayMode: false })
|
|
65
|
+
},
|
|
66
|
+
node,
|
|
67
|
+
decorations,
|
|
68
|
+
),
|
|
52
69
|
},
|
|
53
70
|
})
|
|
54
71
|
```
|
|
@@ -91,29 +108,7 @@ const plugin = createCursorInsidePlugin()
|
|
|
91
108
|
|
|
92
109
|
## API
|
|
93
110
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- **`mathBlockSpec`** — NodeSpec for block math (`div.prosekit-math-block > pre > code`)
|
|
97
|
-
- **`mathInlineSpec`** — NodeSpec for inline math (`span.prosekit-math-inline > code`)
|
|
98
|
-
|
|
99
|
-
### Node views
|
|
100
|
-
|
|
101
|
-
- **`createMathBlockView(renderMath, node, decorations)`** — Creates a block math NodeView
|
|
102
|
-
- **`createMathInlineView(renderMath, node, decorations)`** — Creates an inline math NodeView
|
|
103
|
-
|
|
104
|
-
The `renderMath` callback receives `(text: string, element: HTMLElement)` and should render the math into the element.
|
|
105
|
-
|
|
106
|
-
### Input rules
|
|
107
|
-
|
|
108
|
-
- **`createMathInlineInputRule(nodeType)`** — Creates an InputRule for inline math
|
|
109
|
-
|
|
110
|
-
### Enter rules
|
|
111
|
-
|
|
112
|
-
- **`mathBlockEnterRule`** — EnterRule for converting `$$` into a math block
|
|
113
|
-
|
|
114
|
-
### Plugins
|
|
115
|
-
|
|
116
|
-
- **`createCursorInsidePlugin()`** — Plugin that decorates math nodes containing the cursor
|
|
111
|
+
[API Reference](https://npmx.dev/package-docs/prosemirror-math)
|
|
117
112
|
|
|
118
113
|
## License
|
|
119
114
|
|
|
@@ -17,9 +17,20 @@ import { Node, NodeSpec } from "prosemirror-model";
|
|
|
17
17
|
declare function createCursorInsidePlugin(): Plugin;
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/math-block-enter-rule.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* An {@link EnterRule} that converts a textblock node that only contains `$$` into a math
|
|
22
|
+
* block node when Enter is pressed.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
20
26
|
declare const mathBlockEnterRule: EnterRule;
|
|
21
27
|
//#endregion
|
|
22
28
|
//#region src/math-block-spec.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* A {@link NodeSpec} for a block-level math node.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
23
34
|
declare const mathBlockSpec: NodeSpec;
|
|
24
35
|
//#endregion
|
|
25
36
|
//#region src/math-block-view.d.ts
|
|
@@ -30,12 +41,37 @@ declare const mathBlockSpec: NodeSpec;
|
|
|
30
41
|
* @param element - A `<div>` element to render the math block.
|
|
31
42
|
*/
|
|
32
43
|
type RenderMathBlock = (text: string, element: HTMLElement) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a {@link NodeView} for a block-level math node. The view will show a
|
|
46
|
+
* source editor or a rendered display area based on the text cursor position.
|
|
47
|
+
*
|
|
48
|
+
* @param renderMathBlock - A function that renders math text (e.g. TeX) into
|
|
49
|
+
* the display element. You can use libraries like
|
|
50
|
+
* [Temml](https://temml.org/) or [KaTeX](https://katex.org/).
|
|
51
|
+
* @param node - The ProseMirror node to render.
|
|
52
|
+
* @param decorations - The decorations applied to the node.
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
33
56
|
declare function createMathBlockView(renderMathBlock: RenderMathBlock, node: Node, decorations: readonly Decoration[]): NodeView;
|
|
34
57
|
//#endregion
|
|
35
58
|
//#region src/math-inline-input-rule.d.ts
|
|
59
|
+
/**
|
|
60
|
+
* Creates a ProseMirror {@link InputRule} that converts text wrapped in `$` or
|
|
61
|
+
* `$$` (e.g. `$x^2$`) into an inline math node.
|
|
62
|
+
*
|
|
63
|
+
* @param nodeType - The name of the inline math node type in your schema.
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
36
67
|
declare function createMathInlineInputRule(nodeType: string): InputRule;
|
|
37
68
|
//#endregion
|
|
38
69
|
//#region src/math-inline-spec.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* A {@link NodeSpec} for an inline math node.
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
39
75
|
declare const mathInlineSpec: NodeSpec;
|
|
40
76
|
//#endregion
|
|
41
77
|
//#region src/math-inline-view.d.ts
|
|
@@ -46,6 +82,19 @@ declare const mathInlineSpec: NodeSpec;
|
|
|
46
82
|
* @param element - A `<span>` element to render the math inline.
|
|
47
83
|
*/
|
|
48
84
|
type RenderMathInline = (text: string, element: HTMLElement) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Creates a {@link NodeView} for an inline math node. The view will show a
|
|
87
|
+
* source editor or a rendered display area based on the text cursor position.
|
|
88
|
+
*
|
|
89
|
+
* @param renderMathInline - A function that renders math text (e.g. TeX) into
|
|
90
|
+
* the display element. You can use libraries like [Temml](https://temml.org/)
|
|
91
|
+
* or [KaTeX](https://katex.org/).
|
|
92
|
+
* @param node - The ProseMirror node to render.
|
|
93
|
+
* @param decorations - The decorations applied to the node.
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
49
97
|
declare function createMathInlineView(renderMathInline: RenderMathInline, node: Node, decorations: readonly Decoration[]): NodeView;
|
|
50
98
|
//#endregion
|
|
51
|
-
export { type RenderMathBlock, type RenderMathInline, createCursorInsidePlugin, createMathBlockView, createMathInlineInputRule, createMathInlineView, mathBlockEnterRule, mathBlockSpec, mathInlineSpec };
|
|
99
|
+
export { type RenderMathBlock, type RenderMathInline, createCursorInsidePlugin, createMathBlockView, createMathInlineInputRule, createMathInlineView, mathBlockEnterRule, mathBlockSpec, mathInlineSpec };
|
|
100
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/cursor-inside-plugin.ts","../src/math-block-enter-rule.ts","../src/math-block-spec.ts","../src/math-block-view.ts","../src/math-inline-input-rule.ts","../src/math-inline-spec.ts","../src/math-inline-view.ts"],"mappings":";;;;;;;;AA2CA;;;;;;;;iBAAgB,wBAAA,CAAA,GAA4B,MAAA;;;;;;;;AAA5C;cC9Ba,kBAAA,EAAoB,SAAA;;;;;;;;cCNpB,aAAA,EAAe,QAAA;;;;;;;;AFoC5B;KG/BY,eAAA,IAAmB,IAAA,UAAc,OAAA,EAAS,WAAA;;;;;;;AFCtD;;;;;;iBEagB,mBAAA,CACd,eAAA,EAAiB,eAAA,EACjB,IAAA,EAAM,IAAA,EACN,WAAA,WAAsB,UAAA,KACrB,QAAA;;;;;;;;AHaH;;;iBIfgB,yBAAA,CAA0B,QAAA,WAAmB,SAAA;;;;;;;;cCrBhD,cAAA,EAAgB,QAAA;;;;;;;;ALoC7B;KM/BY,gBAAA,IAAoB,IAAA,UAAc,OAAA,EAAS,WAAA;;;;;;;ALCvD;;;;;;iBKagB,oBAAA,CACd,gBAAA,EAAkB,gBAAA,EAClB,IAAA,EAAM,IAAA,EACN,WAAA,WAAsB,UAAA,KACrB,QAAA"}
|
|
@@ -49,6 +49,12 @@ function createCursorInsidePlugin() {
|
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/math-block-enter-rule.ts
|
|
51
51
|
const MATH_BLOCK_ENTER_REGEXP = /^\$\$$/;
|
|
52
|
+
/**
|
|
53
|
+
* An {@link EnterRule} that converts a textblock node that only contains `$$` into a math
|
|
54
|
+
* block node when Enter is pressed.
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
52
58
|
const mathBlockEnterRule = /* @__PURE__ */ createTextBlockEnterRule({
|
|
53
59
|
regex: MATH_BLOCK_ENTER_REGEXP,
|
|
54
60
|
type: "mathBlock"
|
|
@@ -56,6 +62,11 @@ const mathBlockEnterRule = /* @__PURE__ */ createTextBlockEnterRule({
|
|
|
56
62
|
|
|
57
63
|
//#endregion
|
|
58
64
|
//#region src/math-block-spec.ts
|
|
65
|
+
/**
|
|
66
|
+
* A {@link NodeSpec} for a block-level math node.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
59
70
|
const mathBlockSpec = {
|
|
60
71
|
atom: false,
|
|
61
72
|
group: "block math",
|
|
@@ -64,12 +75,12 @@ const mathBlockSpec = {
|
|
|
64
75
|
toDOM() {
|
|
65
76
|
return [
|
|
66
77
|
"div",
|
|
67
|
-
{ class: "
|
|
78
|
+
{ class: "prosemirror-math-block" },
|
|
68
79
|
["pre", ["code", 0]]
|
|
69
80
|
];
|
|
70
81
|
},
|
|
71
82
|
parseDOM: [{
|
|
72
|
-
tag: "div.
|
|
83
|
+
tag: "div.prosemirror-math-block",
|
|
73
84
|
contentElement: "code"
|
|
74
85
|
}]
|
|
75
86
|
};
|
|
@@ -85,7 +96,7 @@ function createElement(tag, className, ...children) {
|
|
|
85
96
|
|
|
86
97
|
//#endregion
|
|
87
98
|
//#region src/math-view-render.ts
|
|
88
|
-
function createMathViewRender(renderMath, source, display) {
|
|
99
|
+
function createMathViewRender(renderMath, source, display, inline) {
|
|
89
100
|
let prevNode;
|
|
90
101
|
let prevText;
|
|
91
102
|
let prevSelected;
|
|
@@ -101,23 +112,52 @@ function createMathViewRender(renderMath, source, display) {
|
|
|
101
112
|
const selected = hasCursorInsideDecoration(decorations);
|
|
102
113
|
if (selected === prevSelected) return;
|
|
103
114
|
prevSelected = selected;
|
|
104
|
-
source.style.display = selected ? "" : "none";
|
|
105
115
|
display.style.display = selected ? "none" : "";
|
|
116
|
+
if (!inline) source.style.display = selected ? "" : "none";
|
|
117
|
+
else Object.assign(source.style, selected ? visibleInlineSourceStyle : hiddenInlineSourceStyle);
|
|
106
118
|
}
|
|
107
119
|
return function updateMathView(node, decorations) {
|
|
108
120
|
updateDisplay(node);
|
|
109
121
|
updateStyle(decorations);
|
|
110
122
|
};
|
|
111
123
|
}
|
|
124
|
+
const hiddenInlineSourceStyle = {
|
|
125
|
+
display: "inline-flex",
|
|
126
|
+
opacity: "0",
|
|
127
|
+
pointerEvents: "none",
|
|
128
|
+
maxWidth: "0",
|
|
129
|
+
maxHeight: "0",
|
|
130
|
+
overflow: "hidden"
|
|
131
|
+
};
|
|
132
|
+
const visibleInlineSourceStyle = {
|
|
133
|
+
display: "inline-flex",
|
|
134
|
+
opacity: "1",
|
|
135
|
+
pointerEvents: "",
|
|
136
|
+
maxWidth: "",
|
|
137
|
+
maxHeight: "",
|
|
138
|
+
overflow: ""
|
|
139
|
+
};
|
|
112
140
|
|
|
113
141
|
//#endregion
|
|
114
142
|
//#region src/math-block-view.ts
|
|
143
|
+
/**
|
|
144
|
+
* Creates a {@link NodeView} for a block-level math node. The view will show a
|
|
145
|
+
* source editor or a rendered display area based on the text cursor position.
|
|
146
|
+
*
|
|
147
|
+
* @param renderMathBlock - A function that renders math text (e.g. TeX) into
|
|
148
|
+
* the display element. You can use libraries like
|
|
149
|
+
* [Temml](https://temml.org/) or [KaTeX](https://katex.org/).
|
|
150
|
+
* @param node - The ProseMirror node to render.
|
|
151
|
+
* @param decorations - The decorations applied to the node.
|
|
152
|
+
*
|
|
153
|
+
* @public
|
|
154
|
+
*/
|
|
115
155
|
function createMathBlockView(renderMathBlock, node, decorations) {
|
|
116
156
|
const code = createElement("code");
|
|
117
|
-
const source = createElement("pre", "
|
|
118
|
-
const display = createElement("div", "
|
|
119
|
-
const dom = createElement("div", "
|
|
120
|
-
const render = createMathViewRender(renderMathBlock, source, display);
|
|
157
|
+
const source = createElement("pre", "prosemirror-math-source", code);
|
|
158
|
+
const display = createElement("div", "prosemirror-math-display");
|
|
159
|
+
const dom = createElement("div", "prosemirror-math-block", source, display);
|
|
160
|
+
const render = createMathViewRender(renderMathBlock, source, display, false);
|
|
121
161
|
render(node, decorations);
|
|
122
162
|
return {
|
|
123
163
|
dom,
|
|
@@ -132,6 +172,14 @@ function createMathBlockView(renderMathBlock, node, decorations) {
|
|
|
132
172
|
//#endregion
|
|
133
173
|
//#region src/math-inline-input-rule.ts
|
|
134
174
|
const MATH_INPUT_REGEXP = (supportsRegexLookbehind() ? "(?<!\\$)" : "") + "(\\$\\$?)([^\\s$](?:[^$]*[^\\s$])?)\\1$";
|
|
175
|
+
/**
|
|
176
|
+
* Creates a ProseMirror {@link InputRule} that converts text wrapped in `$` or
|
|
177
|
+
* `$$` (e.g. `$x^2$`) into an inline math node.
|
|
178
|
+
*
|
|
179
|
+
* @param nodeType - The name of the inline math node type in your schema.
|
|
180
|
+
*
|
|
181
|
+
* @public
|
|
182
|
+
*/
|
|
135
183
|
function createMathInlineInputRule(nodeType) {
|
|
136
184
|
return new InputRule(new RegExp(MATH_INPUT_REGEXP), (state, match, start, end) => {
|
|
137
185
|
const { tr, schema } = state;
|
|
@@ -147,6 +195,11 @@ function createMathInlineInputRule(nodeType) {
|
|
|
147
195
|
|
|
148
196
|
//#endregion
|
|
149
197
|
//#region src/math-inline-spec.ts
|
|
198
|
+
/**
|
|
199
|
+
* A {@link NodeSpec} for an inline math node.
|
|
200
|
+
*
|
|
201
|
+
* @public
|
|
202
|
+
*/
|
|
150
203
|
const mathInlineSpec = {
|
|
151
204
|
atom: false,
|
|
152
205
|
inline: true,
|
|
@@ -157,27 +210,40 @@ const mathInlineSpec = {
|
|
|
157
210
|
toDOM() {
|
|
158
211
|
return [
|
|
159
212
|
"span",
|
|
160
|
-
{ class: "
|
|
213
|
+
{ class: "prosemirror-math-inline" },
|
|
161
214
|
["code", 0]
|
|
162
215
|
];
|
|
163
216
|
},
|
|
164
217
|
parseDOM: [{
|
|
165
|
-
tag: "span.
|
|
218
|
+
tag: "span.prosemirror-math-inline",
|
|
166
219
|
contentElement: "code"
|
|
167
220
|
}]
|
|
168
221
|
};
|
|
169
222
|
|
|
170
223
|
//#endregion
|
|
171
224
|
//#region src/math-inline-view.ts
|
|
225
|
+
/**
|
|
226
|
+
* Creates a {@link NodeView} for an inline math node. The view will show a
|
|
227
|
+
* source editor or a rendered display area based on the text cursor position.
|
|
228
|
+
*
|
|
229
|
+
* @param renderMathInline - A function that renders math text (e.g. TeX) into
|
|
230
|
+
* the display element. You can use libraries like [Temml](https://temml.org/)
|
|
231
|
+
* or [KaTeX](https://katex.org/).
|
|
232
|
+
* @param node - The ProseMirror node to render.
|
|
233
|
+
* @param decorations - The decorations applied to the node.
|
|
234
|
+
*
|
|
235
|
+
* @public
|
|
236
|
+
*/
|
|
172
237
|
function createMathInlineView(renderMathInline, node, decorations) {
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
238
|
+
const code = createElement("code");
|
|
239
|
+
const source = createElement("span", "prosemirror-math-source", code);
|
|
240
|
+
const display = createElement("span", "prosemirror-math-display");
|
|
241
|
+
const dom = createElement("span", "prosemirror-math-inline", source, display);
|
|
242
|
+
const render = createMathViewRender(renderMathInline, source, display, true);
|
|
177
243
|
render(node, decorations);
|
|
178
244
|
return {
|
|
179
245
|
dom,
|
|
180
|
-
contentDOM:
|
|
246
|
+
contentDOM: code,
|
|
181
247
|
update: (node, decorations) => {
|
|
182
248
|
render(node, decorations);
|
|
183
249
|
return true;
|
|
@@ -186,4 +252,5 @@ function createMathInlineView(renderMathInline, node, decorations) {
|
|
|
186
252
|
}
|
|
187
253
|
|
|
188
254
|
//#endregion
|
|
189
|
-
export { createCursorInsidePlugin, createMathBlockView, createMathInlineInputRule, createMathInlineView, mathBlockEnterRule, mathBlockSpec, mathInlineSpec };
|
|
255
|
+
export { createCursorInsidePlugin, createMathBlockView, createMathInlineInputRule, createMathInlineView, mathBlockEnterRule, mathBlockSpec, mathInlineSpec };
|
|
256
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/cursor-inside-plugin.ts","../src/math-block-enter-rule.ts","../src/math-block-spec.ts","../src/create-element.ts","../src/math-view-render.ts","../src/math-block-view.ts","../src/math-inline-input-rule.ts","../src/math-inline-spec.ts","../src/math-inline-view.ts"],"sourcesContent":["import { Plugin, PluginKey, type EditorState } from 'prosemirror-state'\nimport { Decoration, DecorationSet } from 'prosemirror-view'\n\nconst DECORATION_SPEC = 'MATH_CURSOR_INSIDE'\n\nfunction createCursorInsideDecoration(\n state: EditorState,\n): DecorationSet | undefined {\n const { $head } = state.selection\n const node = $head.parent\n\n if (!node.type.isInGroup('math')) return\n\n const before = $head.before()\n const deco = Decoration.node(\n before,\n before + node.nodeSize,\n { class: 'prosemirror-math-head-inside' },\n DECORATION_SPEC,\n )\n return DecorationSet.create(state.doc, [deco])\n}\n\n/**\n * @internal\n */\nexport function hasCursorInsideDecoration(\n decorations: readonly Decoration[],\n): boolean {\n return decorations.some((deco) => deco.spec === DECORATION_SPEC)\n}\n\ntype PluginState = DecorationSet | undefined\n\n/**\n * Creates a plugin that adds a `prosemirror-math-head-inside` CSS class to math\n * nodes when the text selection head is inside them. This is useful for styling\n * math nodes differently while they are being edited.\n *\n * The plugin automatically detects nodes in the `math` group.\n *\n * @public\n */\nexport function createCursorInsidePlugin(): Plugin {\n const key = new PluginKey<PluginState>('prosemirror-math-cursor-inside')\n return new Plugin<PluginState>({\n key,\n state: {\n init(): PluginState {\n return undefined\n },\n apply(tr, oldValue, oldState, newState): PluginState {\n if (\n oldState.selection.head === newState.selection.head &&\n !tr.docChanged\n ) {\n return oldValue\n }\n return createCursorInsideDecoration(newState)\n },\n },\n props: {\n decorations(state: EditorState): PluginState | undefined {\n return key.getState(state)\n },\n },\n })\n}\n","import {\n createTextBlockEnterRule,\n type EnterRule,\n} from 'prosemirror-enter-rules'\n\nexport const MATH_BLOCK_ENTER_REGEXP: RegExp = /^\\$\\$$/\n\n/**\n * An {@link EnterRule} that converts a textblock node that only contains `$$` into a math\n * block node when Enter is pressed.\n *\n * @public\n */\nexport const mathBlockEnterRule: EnterRule =\n /* @__PURE__ */ createTextBlockEnterRule({\n regex: MATH_BLOCK_ENTER_REGEXP,\n type: 'mathBlock',\n })\n","import type { NodeSpec } from 'prosemirror-model'\n\n/**\n * A {@link NodeSpec} for a block-level math node.\n *\n * @public\n */\nexport const mathBlockSpec: NodeSpec = {\n atom: false,\n group: 'block math',\n content: 'text*',\n code: true,\n toDOM() {\n return [\n 'div',\n {\n class: 'prosemirror-math-block',\n },\n ['pre', ['code', 0]],\n ]\n },\n parseDOM: [\n {\n tag: 'div.prosemirror-math-block',\n\n // skip the `<pre>` wrapper so that the node `codeBlock` won't match the content.\n // TODO: add test to verify it\n contentElement: 'code',\n },\n ],\n}\n","export function createElement<Tag extends keyof HTMLElementTagNameMap>(\n tag: Tag,\n className?: string,\n ...children: HTMLElement[]\n): HTMLElementTagNameMap[Tag] {\n const element: HTMLElementTagNameMap[Tag] = document.createElement(tag)\n if (className) {\n element.className = className\n }\n if (children.length > 0) {\n element.append(...children)\n }\n return element\n}\n","import type { Node } from 'prosemirror-model'\nimport type { Decoration } from 'prosemirror-view'\n\nimport { hasCursorInsideDecoration } from './cursor-inside-plugin.ts'\n\ntype RenderMath = (text: string, element: HTMLElement) => void\n\nexport function createMathViewRender(\n renderMath: RenderMath,\n source: HTMLElement,\n display: HTMLElement,\n inline: boolean,\n) {\n let prevNode: Node | undefined\n let prevText: string | undefined\n let prevSelected: boolean | undefined\n\n function updateDisplay(node: Node) {\n if (node === prevNode) return\n prevNode = node\n\n const text = node.textContent\n if (text === prevText) return\n prevText = text\n\n renderMath(text, display)\n }\n\n function updateStyle(decorations: readonly Decoration[]): void {\n const selected = hasCursorInsideDecoration(decorations)\n if (selected === prevSelected) return\n prevSelected = selected\n\n // When the math node is selected, show the source code.\n // Otherwise, show the rendered result.\n display.style.display = selected ? 'none' : ''\n if (!inline) {\n source.style.display = selected ? '' : 'none'\n } else {\n // For inline source code, we don't use `display: none` because we need\n // the source text rendered in the DOM to ensure the text cursor can be\n // placed correctly.\n Object.assign(\n source.style,\n selected ? visibleInlineSourceStyle : hiddenInlineSourceStyle,\n )\n }\n }\n\n return function updateMathView(\n node: Node,\n decorations: readonly Decoration[],\n ): void {\n updateDisplay(node)\n updateStyle(decorations)\n }\n}\n\nconst hiddenInlineSourceStyle: Partial<CSSStyleDeclaration> = {\n display: 'inline-flex',\n opacity: '0',\n pointerEvents: 'none',\n maxWidth: '0',\n maxHeight: '0',\n overflow: 'hidden',\n}\n\nconst visibleInlineSourceStyle: Partial<CSSStyleDeclaration> = {\n display: 'inline-flex',\n opacity: '1',\n pointerEvents: '',\n maxWidth: '',\n maxHeight: '',\n overflow: '',\n}\n","import type { Node as ProseMirrorNode } from 'prosemirror-model'\nimport type { Decoration, NodeView } from 'prosemirror-view'\n\nimport { createElement } from './create-element.ts'\nimport { createMathViewRender } from './math-view-render.ts'\n\n/**\n * The function to render a math block.\n *\n * @param text - The text of the math block. For example, a TeX expression.\n * @param element - A `<div>` element to render the math block.\n */\nexport type RenderMathBlock = (text: string, element: HTMLElement) => void\n\n/**\n * Creates a {@link NodeView} for a block-level math node. The view will show a\n * source editor or a rendered display area based on the text cursor position.\n *\n * @param renderMathBlock - A function that renders math text (e.g. TeX) into\n * the display element. You can use libraries like\n * [Temml](https://temml.org/) or [KaTeX](https://katex.org/).\n * @param node - The ProseMirror node to render.\n * @param decorations - The decorations applied to the node.\n *\n * @public\n */\nexport function createMathBlockView(\n renderMathBlock: RenderMathBlock,\n node: ProseMirrorNode,\n decorations: readonly Decoration[],\n): NodeView {\n const code = createElement('code')\n const source = createElement('pre', 'prosemirror-math-source', code)\n const display = createElement('div', 'prosemirror-math-display')\n const dom = createElement('div', 'prosemirror-math-block', source, display)\n\n const render = createMathViewRender(renderMathBlock, source, display, false)\n\n render(node, decorations)\n\n return {\n dom,\n contentDOM: code,\n update: (node, decorations) => {\n render(node, decorations)\n return true\n },\n }\n}\n","/* eslint-disable unicorn/prefer-string-raw -- Don't use String.raw here for better bundler minification */\n\nimport { supportsRegexLookbehind } from '@ocavue/utils'\nimport { InputRule } from 'prosemirror-inputrules'\n\n// Matches text wrapped in `$` or `$$`, e.g. `$x^2$` or `$$x^2$$`.\nexport const MATH_INPUT_REGEXP: string =\n // Don't allow $ before the opening delimiter\n (supportsRegexLookbehind() ? '(?<!\\\\$)' : '') +\n // capture group 1: opening delimiter (`$` or `$$`)\n '(\\\\$\\\\$?)' +\n // capture group 2: the math content\n // - a single non-whitespace, non-`$` character\n // - optionally followed by any non-`$` characters and a final non-whitespace, non-`$` character\n '([^\\\\s$](?:[^$]*[^\\\\s$])?)' +\n // backreference: closing delimiter must match the opening\n '\\\\1' +\n // end of input (required by ProseMirror InputRule)\n '$'\n\n/**\n * Creates a ProseMirror {@link InputRule} that converts text wrapped in `$` or\n * `$$` (e.g. `$x^2$`) into an inline math node.\n *\n * @param nodeType - The name of the inline math node type in your schema.\n *\n * @public\n */\nexport function createMathInlineInputRule(nodeType: string): InputRule {\n return new InputRule(\n new RegExp(MATH_INPUT_REGEXP),\n (state, match, start, end) => {\n const { tr, schema } = state\n const mathText = match[2]\n if (!mathText) return null\n\n const type = schema.nodes[nodeType]\n if (!type) return null\n\n const node = type.create(null, schema.text(mathText))\n tr.replaceWith(start, end, node)\n return tr\n },\n )\n}\n","import type { NodeSpec } from 'prosemirror-model'\n\n/**\n * A {@link NodeSpec} for an inline math node.\n *\n * @public\n */\nexport const mathInlineSpec: NodeSpec = {\n atom: false,\n inline: true,\n group: 'inline math',\n content: 'text*',\n selectable: false,\n code: true,\n toDOM() {\n return [\n 'span',\n {\n class: 'prosemirror-math-inline',\n },\n ['code', 0],\n ]\n },\n parseDOM: [\n {\n tag: 'span.prosemirror-math-inline',\n contentElement: 'code',\n },\n ],\n}\n","import type { Node as ProseMirrorNode } from 'prosemirror-model'\nimport type { Decoration, NodeView } from 'prosemirror-view'\n\nimport { createElement } from './create-element.ts'\nimport { createMathViewRender } from './math-view-render.ts'\n\n/**\n * The function to render a math inline.\n *\n * @param text - The text of the math inline. For example, a TeX expression.\n * @param element - A `<span>` element to render the math inline.\n */\nexport type RenderMathInline = (text: string, element: HTMLElement) => void\n\n/**\n * Creates a {@link NodeView} for an inline math node. The view will show a\n * source editor or a rendered display area based on the text cursor position.\n *\n * @param renderMathInline - A function that renders math text (e.g. TeX) into\n * the display element. You can use libraries like [Temml](https://temml.org/)\n * or [KaTeX](https://katex.org/).\n * @param node - The ProseMirror node to render.\n * @param decorations - The decorations applied to the node.\n *\n * @public\n */\nexport function createMathInlineView(\n renderMathInline: RenderMathInline,\n node: ProseMirrorNode,\n decorations: readonly Decoration[],\n): NodeView {\n const code = createElement('code')\n const source = createElement('span', 'prosemirror-math-source', code)\n const display = createElement('span', 'prosemirror-math-display')\n const dom = createElement('span', 'prosemirror-math-inline', source, display)\n\n const render = createMathViewRender(renderMathInline, source, display, true)\n\n render(node, decorations)\n\n return {\n dom,\n contentDOM: code,\n update: (node, decorations) => {\n render(node, decorations)\n return true\n },\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prosemirror-math",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"author": {
|
|
8
|
-
"name": "ocavue",
|
|
9
|
-
"email": "ocavue@gmail.com"
|
|
10
|
-
},
|
|
4
|
+
"version": "0.2.2",
|
|
5
|
+
"description": "Rendering math expressions in ProseMirror",
|
|
6
|
+
"author": "ocavue <ocavue@gmail.com>",
|
|
11
7
|
"license": "MIT",
|
|
12
8
|
"funding": "https://github.com/sponsors/ocavue",
|
|
13
|
-
"homepage": "https://github.com/
|
|
9
|
+
"homepage": "https://github.com/ocavue/prosemirror-math#readme",
|
|
14
10
|
"repository": {
|
|
15
11
|
"type": "git",
|
|
16
|
-
"url": "
|
|
17
|
-
"directory": "packages/prosemirror-math"
|
|
18
|
-
},
|
|
19
|
-
"bugs": {
|
|
20
|
-
"url": "https://github.com/prosekit/prosekit/issues"
|
|
12
|
+
"url": "https://github.com/ocavue/prosemirror-math.git"
|
|
21
13
|
},
|
|
14
|
+
"bugs": "https://github.com/ocavue/prosemirror-math/issues",
|
|
22
15
|
"keywords": [
|
|
23
16
|
"ProseMirror"
|
|
24
17
|
],
|
|
25
18
|
"sideEffects": false,
|
|
26
|
-
"main": "./dist/
|
|
27
|
-
"module": "./dist/
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
28
22
|
"exports": {
|
|
29
23
|
".": {
|
|
30
|
-
"types": "./dist/
|
|
31
|
-
"
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"typesVersions": {
|
|
29
|
+
"*": {
|
|
30
|
+
"*": [
|
|
31
|
+
"./dist/*",
|
|
32
|
+
"./dist/index.d.ts"
|
|
33
|
+
]
|
|
32
34
|
}
|
|
33
35
|
},
|
|
34
36
|
"files": [
|
|
@@ -37,42 +39,39 @@
|
|
|
37
39
|
],
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@ocavue/utils": "^1.5.0",
|
|
40
|
-
"prosemirror-
|
|
41
|
-
"prosemirror-
|
|
42
|
-
"prosemirror-
|
|
43
|
-
"prosemirror-
|
|
44
|
-
"prosemirror-
|
|
42
|
+
"prosemirror-enter-rules": "^0.1.5",
|
|
43
|
+
"prosemirror-inputrules": "^1.0.0",
|
|
44
|
+
"prosemirror-model": "^1.0.0",
|
|
45
|
+
"prosemirror-state": "^1.0.0",
|
|
46
|
+
"prosemirror-view": "^1.0.0"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@mathjax/src": "^4.1.0",
|
|
50
|
+
"@ocavue/eslint-config": "^4.1.0",
|
|
51
|
+
"@ocavue/tsconfig": "^0.6.3",
|
|
52
|
+
"@prosekit/core": "^0.10.0",
|
|
53
|
+
"@types/node": "^20.19.10",
|
|
54
|
+
"@vitest/browser": "^4.0.18",
|
|
55
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
48
56
|
"diffable-html-snapshot": "^0.2.0",
|
|
57
|
+
"eslint": "^10.0.1",
|
|
49
58
|
"katex": "^0.16.28",
|
|
59
|
+
"playwright": "^1.58.2",
|
|
60
|
+
"prettier": "^3.8.1",
|
|
50
61
|
"temml": "^0.13.1",
|
|
51
62
|
"tsdown": "^0.20.3",
|
|
52
|
-
"typescript": "
|
|
63
|
+
"typescript": "^5.9.3",
|
|
64
|
+
"vite": "^7.3.1",
|
|
53
65
|
"vitest": "^4.0.18",
|
|
54
|
-
"vitest-browser-commands": "^0.2.0"
|
|
55
|
-
"@prosekit/core": "^0.10.0",
|
|
56
|
-
"@prosekit/config-vitest": "0.0.0"
|
|
57
|
-
},
|
|
58
|
-
"publishConfig": {
|
|
59
|
-
"dev": {}
|
|
60
|
-
},
|
|
61
|
-
"dev": {
|
|
62
|
-
"entry": {
|
|
63
|
-
"prosemirror-math": "./src/index.ts"
|
|
64
|
-
}
|
|
66
|
+
"vitest-browser-commands": "^0.2.0"
|
|
65
67
|
},
|
|
66
68
|
"scripts": {
|
|
67
|
-
"build
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
"./dist/prosemirror-math.d.ts"
|
|
75
|
-
]
|
|
76
|
-
}
|
|
69
|
+
"build": "tsdown",
|
|
70
|
+
"dev": "tsdown --watch",
|
|
71
|
+
"lint": "eslint .",
|
|
72
|
+
"fix": "eslint --fix . && prettier --write .",
|
|
73
|
+
"test:install": "playwright install chromium",
|
|
74
|
+
"test": "vitest",
|
|
75
|
+
"typecheck": "tsc -b"
|
|
77
76
|
}
|
|
78
77
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TextSelection } from 'prosemirror-state'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import { setupTest } from './testing'
|
|
4
|
+
import { setupTest } from './testing.ts'
|
|
5
5
|
|
|
6
6
|
describe('cursorInsidePlugin', () => {
|
|
7
7
|
it('applies decoration when cursor is inside mathBlock', () => {
|
|
@@ -10,13 +10,13 @@ describe('cursorInsidePlugin', () => {
|
|
|
10
10
|
|
|
11
11
|
// Place cursor inside the math block
|
|
12
12
|
const { state } = editor.view
|
|
13
|
-
const tr = state.tr.setSelection(
|
|
14
|
-
TextSelection.near(state.doc.resolve(2)),
|
|
15
|
-
)
|
|
13
|
+
const tr = state.tr.setSelection(TextSelection.near(state.doc.resolve(2)))
|
|
16
14
|
editor.view.dispatch(tr)
|
|
17
15
|
|
|
18
|
-
const mathBlock = editor.view.dom.querySelector('.
|
|
19
|
-
expect(mathBlock?.classList.contains('prosemirror-math-head-inside')).toBe(
|
|
16
|
+
const mathBlock = editor.view.dom.querySelector('.prosemirror-math-block')
|
|
17
|
+
expect(mathBlock?.classList.contains('prosemirror-math-head-inside')).toBe(
|
|
18
|
+
true,
|
|
19
|
+
)
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
it('applies decoration when cursor is inside mathInline', () => {
|
|
@@ -25,13 +25,13 @@ describe('cursorInsidePlugin', () => {
|
|
|
25
25
|
|
|
26
26
|
// Place cursor inside the math inline node
|
|
27
27
|
const { state } = editor.view
|
|
28
|
-
const tr = state.tr.setSelection(
|
|
29
|
-
TextSelection.near(state.doc.resolve(2)),
|
|
30
|
-
)
|
|
28
|
+
const tr = state.tr.setSelection(TextSelection.near(state.doc.resolve(2)))
|
|
31
29
|
editor.view.dispatch(tr)
|
|
32
30
|
|
|
33
|
-
const mathInline = editor.view.dom.querySelector('.
|
|
34
|
-
expect(mathInline?.classList.contains('prosemirror-math-head-inside')).toBe(
|
|
31
|
+
const mathInline = editor.view.dom.querySelector('.prosemirror-math-inline')
|
|
32
|
+
expect(mathInline?.classList.contains('prosemirror-math-head-inside')).toBe(
|
|
33
|
+
true,
|
|
34
|
+
)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
it('does not apply decoration when cursor is outside math nodes', () => {
|
|
@@ -40,12 +40,12 @@ describe('cursorInsidePlugin', () => {
|
|
|
40
40
|
|
|
41
41
|
// Place cursor inside the paragraph (position 2 should be inside "hello")
|
|
42
42
|
const { state } = editor.view
|
|
43
|
-
const tr = state.tr.setSelection(
|
|
44
|
-
TextSelection.near(state.doc.resolve(2)),
|
|
45
|
-
)
|
|
43
|
+
const tr = state.tr.setSelection(TextSelection.near(state.doc.resolve(2)))
|
|
46
44
|
editor.view.dispatch(tr)
|
|
47
45
|
|
|
48
|
-
const mathBlock = editor.view.dom.querySelector('.
|
|
49
|
-
expect(mathBlock?.classList.contains('prosemirror-math-head-inside')).toBe(
|
|
46
|
+
const mathBlock = editor.view.dom.querySelector('.prosemirror-math-block')
|
|
47
|
+
expect(mathBlock?.classList.contains('prosemirror-math-head-inside')).toBe(
|
|
48
|
+
false,
|
|
49
|
+
)
|
|
50
50
|
})
|
|
51
51
|
})
|
|
@@ -24,8 +24,10 @@ function createCursorInsideDecoration(
|
|
|
24
24
|
/**
|
|
25
25
|
* @internal
|
|
26
26
|
*/
|
|
27
|
-
export function hasCursorInsideDecoration(
|
|
28
|
-
|
|
27
|
+
export function hasCursorInsideDecoration(
|
|
28
|
+
decorations: readonly Decoration[],
|
|
29
|
+
): boolean {
|
|
30
|
+
return decorations.some((deco) => deco.spec === DECORATION_SPEC)
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
type PluginState = DecorationSet | undefined
|
|
@@ -49,8 +51,8 @@ export function createCursorInsidePlugin(): Plugin {
|
|
|
49
51
|
},
|
|
50
52
|
apply(tr, oldValue, oldState, newState): PluginState {
|
|
51
53
|
if (
|
|
52
|
-
oldState.selection.head === newState.selection.head
|
|
53
|
-
|
|
54
|
+
oldState.selection.head === newState.selection.head &&
|
|
55
|
+
!tr.docChanged
|
|
54
56
|
) {
|
|
55
57
|
return oldValue
|
|
56
58
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
export { createCursorInsidePlugin } from './cursor-inside-plugin'
|
|
2
|
-
export { mathBlockEnterRule } from './math-block-enter-rule'
|
|
3
|
-
export { mathBlockSpec } from './math-block-spec'
|
|
4
|
-
export { createMathBlockView, type RenderMathBlock } from './math-block-view'
|
|
5
|
-
export { createMathInlineInputRule } from './math-inline-input-rule'
|
|
6
|
-
export { mathInlineSpec } from './math-inline-spec'
|
|
7
|
-
export {
|
|
1
|
+
export { createCursorInsidePlugin } from './cursor-inside-plugin.ts'
|
|
2
|
+
export { mathBlockEnterRule } from './math-block-enter-rule.ts'
|
|
3
|
+
export { mathBlockSpec } from './math-block-spec.ts'
|
|
4
|
+
export { createMathBlockView, type RenderMathBlock } from './math-block-view.ts'
|
|
5
|
+
export { createMathInlineInputRule } from './math-inline-input-rule.ts'
|
|
6
|
+
export { mathInlineSpec } from './math-inline-spec.ts'
|
|
7
|
+
export {
|
|
8
|
+
createMathInlineView,
|
|
9
|
+
type RenderMathInline,
|
|
10
|
+
} from './math-inline-view.ts'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { userEvent } from 'vitest/browser'
|
|
3
3
|
|
|
4
|
-
import { MATH_BLOCK_ENTER_REGEXP } from './math-block-enter-rule'
|
|
5
|
-
import { setupTest } from './testing'
|
|
4
|
+
import { MATH_BLOCK_ENTER_REGEXP } from './math-block-enter-rule.ts'
|
|
5
|
+
import { setupTest } from './testing.ts'
|
|
6
6
|
|
|
7
7
|
describe('MATH_BLOCK_ENTER_REGEXP', () => {
|
|
8
8
|
const cases: Array<[input: string, matched: boolean]> = [
|
|
@@ -26,9 +26,7 @@ describe('defineMathBlockEnterRule', () => {
|
|
|
26
26
|
editor.set(n.doc(n.p('<a>')))
|
|
27
27
|
|
|
28
28
|
await userEvent.keyboard('$$')
|
|
29
|
-
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
30
|
-
n.doc(n.p('$$')).toJSON(),
|
|
31
|
-
)
|
|
29
|
+
expect(editor.view.state.doc.toJSON()).toEqual(n.doc(n.p('$$')).toJSON())
|
|
32
30
|
|
|
33
31
|
await userEvent.keyboard('{Enter}')
|
|
34
32
|
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createTextBlockEnterRule,
|
|
3
|
+
type EnterRule,
|
|
4
|
+
} from 'prosemirror-enter-rules'
|
|
2
5
|
|
|
3
6
|
export const MATH_BLOCK_ENTER_REGEXP: RegExp = /^\$\$$/
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* An {@link EnterRule} that converts a textblock node that only contains `$$` into a math
|
|
10
|
+
* block node when Enter is pressed.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export const mathBlockEnterRule: EnterRule =
|
|
15
|
+
/* @__PURE__ */ createTextBlockEnterRule({
|
|
16
|
+
regex: MATH_BLOCK_ENTER_REGEXP,
|
|
17
|
+
type: 'mathBlock',
|
|
18
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import { setupTest } from './testing'
|
|
4
|
+
import { setupTest } from './testing.ts'
|
|
5
5
|
|
|
6
6
|
describe('mathBlockSpec', () => {
|
|
7
7
|
it('can serialize to HTML', () => {
|
|
@@ -12,7 +12,7 @@ describe('mathBlockSpec', () => {
|
|
|
12
12
|
`
|
|
13
13
|
"
|
|
14
14
|
<div>
|
|
15
|
-
<div class="
|
|
15
|
+
<div class="prosemirror-math-block">
|
|
16
16
|
<pre>
|
|
17
17
|
<code>
|
|
18
18
|
E = mc^2
|
package/src/math-block-spec.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { NodeSpec } from 'prosemirror-model'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A {@link NodeSpec} for a block-level math node.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
3
8
|
export const mathBlockSpec: NodeSpec = {
|
|
4
9
|
atom: false,
|
|
5
10
|
group: 'block math',
|
|
@@ -9,14 +14,14 @@ export const mathBlockSpec: NodeSpec = {
|
|
|
9
14
|
return [
|
|
10
15
|
'div',
|
|
11
16
|
{
|
|
12
|
-
class: '
|
|
17
|
+
class: 'prosemirror-math-block',
|
|
13
18
|
},
|
|
14
19
|
['pre', ['code', 0]],
|
|
15
20
|
]
|
|
16
21
|
},
|
|
17
22
|
parseDOM: [
|
|
18
23
|
{
|
|
19
|
-
tag: 'div.
|
|
24
|
+
tag: 'div.prosemirror-math-block',
|
|
20
25
|
|
|
21
26
|
// skip the `<pre>` wrapper so that the node `codeBlock` won't match the content.
|
|
22
27
|
// TODO: add test to verify it
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
katexRenderer,
|
|
6
|
+
mathjaxRenderer,
|
|
7
|
+
renderers,
|
|
8
|
+
setupTest,
|
|
9
|
+
temmlRenderer,
|
|
10
|
+
} from './testing.ts'
|
|
5
11
|
|
|
6
12
|
describe.each(Object.keys(renderers))('createMathBlockView (%s)', (name) => {
|
|
7
13
|
const renderer = renderers[name as keyof typeof renderers]
|
|
@@ -11,17 +17,17 @@ describe.each(Object.keys(renderers))('createMathBlockView (%s)', (name) => {
|
|
|
11
17
|
editor.set(n.doc(n.mathBlock('x^2')))
|
|
12
18
|
|
|
13
19
|
const dom = editor.view.dom
|
|
14
|
-
const mathBlock = dom.querySelector('.
|
|
20
|
+
const mathBlock = dom.querySelector('.prosemirror-math-block')
|
|
15
21
|
expect(mathBlock).toBeTruthy()
|
|
16
|
-
expect(mathBlock?.querySelector('.
|
|
17
|
-
expect(mathBlock?.querySelector('.
|
|
22
|
+
expect(mathBlock?.querySelector('.prosemirror-math-source')).toBeTruthy()
|
|
23
|
+
expect(mathBlock?.querySelector('.prosemirror-math-display')).toBeTruthy()
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
it('updates display when content changes', () => {
|
|
21
27
|
const { editor, n } = setupTest(renderer)
|
|
22
28
|
editor.set(n.doc(n.mathBlock('x^2')))
|
|
23
29
|
|
|
24
|
-
const display = editor.view.dom.querySelector('.
|
|
30
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
25
31
|
const initialHTML = display?.innerHTML
|
|
26
32
|
|
|
27
33
|
// Dispatch a transaction to change content
|
|
@@ -39,7 +45,7 @@ describe('createMathBlockView (temml snapshot)', () => {
|
|
|
39
45
|
const { editor, n } = setupTest(temmlRenderer)
|
|
40
46
|
editor.set(n.doc(n.mathBlock('x^2')))
|
|
41
47
|
|
|
42
|
-
const display = editor.view.dom.querySelector('.
|
|
48
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
43
49
|
const html = formatHTML(display?.innerHTML || '')
|
|
44
50
|
expect(html).toMatchInlineSnapshot(`
|
|
45
51
|
"
|
|
@@ -72,7 +78,7 @@ describe('createMathBlockView (katex snapshot)', () => {
|
|
|
72
78
|
const { editor, n } = setupTest(katexRenderer)
|
|
73
79
|
editor.set(n.doc(n.mathBlock('x^2')))
|
|
74
80
|
|
|
75
|
-
const display = editor.view.dom.querySelector('.
|
|
81
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
76
82
|
const html = formatHTML(display?.innerHTML || '')
|
|
77
83
|
expect(html).toMatchInlineSnapshot(`
|
|
78
84
|
"
|
|
@@ -155,7 +161,7 @@ describe('createMathBlockView (mathjax snapshot)', () => {
|
|
|
155
161
|
const { editor, n } = setupTest(mathjaxRenderer)
|
|
156
162
|
editor.set(n.doc(n.mathBlock('x^2')))
|
|
157
163
|
|
|
158
|
-
const display = editor.view.dom.querySelector('.
|
|
164
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
159
165
|
const html = formatHTML(display?.innerHTML || '')
|
|
160
166
|
expect(html).toMatchInlineSnapshot(`
|
|
161
167
|
"
|
package/src/math-block-view.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Node as ProseMirrorNode } from 'prosemirror-model'
|
|
2
2
|
import type { Decoration, NodeView } from 'prosemirror-view'
|
|
3
3
|
|
|
4
|
-
import { createElement } from './create-element'
|
|
5
|
-
import { createMathViewRender } from './math-view-render'
|
|
4
|
+
import { createElement } from './create-element.ts'
|
|
5
|
+
import { createMathViewRender } from './math-view-render.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The function to render a math block.
|
|
@@ -12,13 +12,29 @@ import { createMathViewRender } from './math-view-render'
|
|
|
12
12
|
*/
|
|
13
13
|
export type RenderMathBlock = (text: string, element: HTMLElement) => void
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Creates a {@link NodeView} for a block-level math node. The view will show a
|
|
17
|
+
* source editor or a rendered display area based on the text cursor position.
|
|
18
|
+
*
|
|
19
|
+
* @param renderMathBlock - A function that renders math text (e.g. TeX) into
|
|
20
|
+
* the display element. You can use libraries like
|
|
21
|
+
* [Temml](https://temml.org/) or [KaTeX](https://katex.org/).
|
|
22
|
+
* @param node - The ProseMirror node to render.
|
|
23
|
+
* @param decorations - The decorations applied to the node.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export function createMathBlockView(
|
|
28
|
+
renderMathBlock: RenderMathBlock,
|
|
29
|
+
node: ProseMirrorNode,
|
|
30
|
+
decorations: readonly Decoration[],
|
|
31
|
+
): NodeView {
|
|
16
32
|
const code = createElement('code')
|
|
17
|
-
const source = createElement('pre', '
|
|
18
|
-
const display = createElement('div', '
|
|
19
|
-
const dom = createElement('div', '
|
|
33
|
+
const source = createElement('pre', 'prosemirror-math-source', code)
|
|
34
|
+
const display = createElement('div', 'prosemirror-math-display')
|
|
35
|
+
const dom = createElement('div', 'prosemirror-math-block', source, display)
|
|
20
36
|
|
|
21
|
-
const render = createMathViewRender(renderMathBlock, source, display)
|
|
37
|
+
const render = createMathViewRender(renderMathBlock, source, display, false)
|
|
22
38
|
|
|
23
39
|
render(node, decorations)
|
|
24
40
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { userEvent } from 'vitest/browser'
|
|
3
3
|
|
|
4
|
-
import { MATH_INPUT_REGEXP } from './math-inline-input-rule'
|
|
5
|
-
import { setupTest } from './testing'
|
|
4
|
+
import { MATH_INPUT_REGEXP } from './math-inline-input-rule.ts'
|
|
5
|
+
import { setupTest } from './testing.ts'
|
|
6
6
|
|
|
7
7
|
describe('MATH_INPUT_REGEXP', () => {
|
|
8
8
|
const regexp = new RegExp(MATH_INPUT_REGEXP)
|
|
9
9
|
|
|
10
|
-
const cases: Array<
|
|
10
|
+
const cases: Array<
|
|
11
|
+
[input: string, delimiter: string | null, captured: string | null]
|
|
12
|
+
> = [
|
|
11
13
|
// Single dollar: inline math
|
|
12
14
|
['$x$', '$', 'x'],
|
|
13
15
|
['$x^2$', '$', 'x^2'],
|
|
@@ -46,13 +48,16 @@ describe('MATH_INPUT_REGEXP', () => {
|
|
|
46
48
|
['hello', null, null],
|
|
47
49
|
]
|
|
48
50
|
|
|
49
|
-
it.each(cases)(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
it.each(cases)(
|
|
52
|
+
'should handle %s',
|
|
53
|
+
(input, expectedDelimiter, expectedContent) => {
|
|
54
|
+
const match = regexp.exec(input)
|
|
55
|
+
const delimiter = match?.[1] ?? null
|
|
56
|
+
const captured = match?.[2] ?? null
|
|
57
|
+
expect(delimiter).toEqual(expectedDelimiter)
|
|
58
|
+
expect(captured).toEqual(expectedContent)
|
|
59
|
+
},
|
|
60
|
+
)
|
|
56
61
|
})
|
|
57
62
|
|
|
58
63
|
describe('defineMathInlineInputRule', () => {
|
|
@@ -62,9 +67,7 @@ describe('defineMathInlineInputRule', () => {
|
|
|
62
67
|
editor.set(n.doc(n.p('<a>')))
|
|
63
68
|
|
|
64
69
|
await userEvent.keyboard('$x^2')
|
|
65
|
-
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
66
|
-
n.doc(n.p('$x^2')).toJSON(),
|
|
67
|
-
)
|
|
70
|
+
expect(editor.view.state.doc.toJSON()).toEqual(n.doc(n.p('$x^2')).toJSON())
|
|
68
71
|
|
|
69
72
|
await userEvent.keyboard('$')
|
|
70
73
|
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
@@ -99,9 +102,7 @@ describe('defineMathInlineInputRule', () => {
|
|
|
99
102
|
editor.set(n.doc(n.p('<a>')))
|
|
100
103
|
|
|
101
104
|
await userEvent.keyboard('$$')
|
|
102
|
-
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
103
|
-
n.doc(n.p('$$')).toJSON(),
|
|
104
|
-
)
|
|
105
|
+
expect(editor.view.state.doc.toJSON()).toEqual(n.doc(n.p('$$')).toJSON())
|
|
105
106
|
})
|
|
106
107
|
|
|
107
108
|
it('should not trigger with spaces at boundaries', async () => {
|
|
@@ -4,34 +4,42 @@ import { supportsRegexLookbehind } from '@ocavue/utils'
|
|
|
4
4
|
import { InputRule } from 'prosemirror-inputrules'
|
|
5
5
|
|
|
6
6
|
// Matches text wrapped in `$` or `$$`, e.g. `$x^2$` or `$$x^2$$`.
|
|
7
|
-
export const MATH_INPUT_REGEXP: string =
|
|
7
|
+
export const MATH_INPUT_REGEXP: string =
|
|
8
8
|
// Don't allow $ before the opening delimiter
|
|
9
|
-
(supportsRegexLookbehind() ? '(?<!\\$)' : '')
|
|
9
|
+
(supportsRegexLookbehind() ? '(?<!\\$)' : '') +
|
|
10
10
|
// capture group 1: opening delimiter (`$` or `$$`)
|
|
11
|
-
|
|
11
|
+
'(\\$\\$?)' +
|
|
12
12
|
// capture group 2: the math content
|
|
13
13
|
// - a single non-whitespace, non-`$` character
|
|
14
14
|
// - optionally followed by any non-`$` characters and a final non-whitespace, non-`$` character
|
|
15
|
-
|
|
15
|
+
'([^\\s$](?:[^$]*[^\\s$])?)' +
|
|
16
16
|
// backreference: closing delimiter must match the opening
|
|
17
|
-
|
|
17
|
+
'\\1' +
|
|
18
18
|
// end of input (required by ProseMirror InputRule)
|
|
19
|
-
|
|
20
|
-
)
|
|
19
|
+
'$'
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Creates a ProseMirror {@link InputRule} that converts text wrapped in `$` or
|
|
23
|
+
* `$$` (e.g. `$x^2$`) into an inline math node.
|
|
24
|
+
*
|
|
25
|
+
* @param nodeType - The name of the inline math node type in your schema.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export function createMathInlineInputRule(nodeType: string): InputRule {
|
|
30
|
+
return new InputRule(
|
|
31
|
+
new RegExp(MATH_INPUT_REGEXP),
|
|
32
|
+
(state, match, start, end) => {
|
|
33
|
+
const { tr, schema } = state
|
|
34
|
+
const mathText = match[2]
|
|
35
|
+
if (!mathText) return null
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
const type = schema.nodes[nodeType]
|
|
38
|
+
if (!type) return null
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
const node = type.create(null, schema.text(mathText))
|
|
41
|
+
tr.replaceWith(start, end, node)
|
|
42
|
+
return tr
|
|
43
|
+
},
|
|
44
|
+
)
|
|
37
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import { setupTest } from './testing'
|
|
4
|
+
import { setupTest } from './testing.ts'
|
|
5
5
|
|
|
6
6
|
describe('mathInlineSpec', () => {
|
|
7
7
|
it('can serialize to HTML', () => {
|
|
@@ -13,7 +13,7 @@ describe('mathInlineSpec', () => {
|
|
|
13
13
|
"
|
|
14
14
|
<div>
|
|
15
15
|
<p>
|
|
16
|
-
<span class="
|
|
16
|
+
<span class="prosemirror-math-inline">
|
|
17
17
|
<code>
|
|
18
18
|
x^2
|
|
19
19
|
</code>
|
|
@@ -75,7 +75,9 @@ describe('mathInlineSpec', () => {
|
|
|
75
75
|
|
|
76
76
|
it('can be mixed with regular text', () => {
|
|
77
77
|
const { editor, n } = setupTest()
|
|
78
|
-
editor.set(
|
|
78
|
+
editor.set(
|
|
79
|
+
n.doc(n.paragraph('The formula ', n.mathInline('E=mc^2'), ' is famous.')),
|
|
80
|
+
)
|
|
79
81
|
const json = editor.getDocJSON()
|
|
80
82
|
expect(json).toMatchInlineSnapshot(`
|
|
81
83
|
{
|
package/src/math-inline-spec.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { NodeSpec } from 'prosemirror-model'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A {@link NodeSpec} for an inline math node.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
3
8
|
export const mathInlineSpec: NodeSpec = {
|
|
4
9
|
atom: false,
|
|
5
10
|
inline: true,
|
|
@@ -11,14 +16,14 @@ export const mathInlineSpec: NodeSpec = {
|
|
|
11
16
|
return [
|
|
12
17
|
'span',
|
|
13
18
|
{
|
|
14
|
-
class: '
|
|
19
|
+
class: 'prosemirror-math-inline',
|
|
15
20
|
},
|
|
16
21
|
['code', 0],
|
|
17
22
|
]
|
|
18
23
|
},
|
|
19
24
|
parseDOM: [
|
|
20
25
|
{
|
|
21
|
-
tag: 'span.
|
|
26
|
+
tag: 'span.prosemirror-math-inline',
|
|
22
27
|
contentElement: 'code',
|
|
23
28
|
},
|
|
24
29
|
],
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
katexRenderer,
|
|
6
|
+
mathjaxRenderer,
|
|
7
|
+
renderers,
|
|
8
|
+
setupTest,
|
|
9
|
+
temmlRenderer,
|
|
10
|
+
} from './testing.ts'
|
|
5
11
|
|
|
6
12
|
describe.each(Object.keys(renderers))('createMathInlineView (%s)', (name) => {
|
|
7
13
|
const renderer = renderers[name as keyof typeof renderers]
|
|
@@ -11,17 +17,17 @@ describe.each(Object.keys(renderers))('createMathInlineView (%s)', (name) => {
|
|
|
11
17
|
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
12
18
|
|
|
13
19
|
const dom = editor.view.dom
|
|
14
|
-
const mathInline = dom.querySelector('.
|
|
20
|
+
const mathInline = dom.querySelector('.prosemirror-math-inline')
|
|
15
21
|
expect(mathInline).toBeTruthy()
|
|
16
|
-
expect(mathInline?.querySelector('.
|
|
17
|
-
expect(mathInline?.querySelector('.
|
|
22
|
+
expect(mathInline?.querySelector('.prosemirror-math-source')).toBeTruthy()
|
|
23
|
+
expect(mathInline?.querySelector('.prosemirror-math-display')).toBeTruthy()
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
it('uses span elements for inline math', () => {
|
|
21
27
|
const { editor, n } = setupTest(renderer)
|
|
22
28
|
editor.set(n.doc(n.paragraph(n.mathInline('x'))))
|
|
23
29
|
|
|
24
|
-
const mathInline = editor.view.dom.querySelector('.
|
|
30
|
+
const mathInline = editor.view.dom.querySelector('.prosemirror-math-inline')
|
|
25
31
|
expect(mathInline?.tagName).toBe('SPAN')
|
|
26
32
|
})
|
|
27
33
|
})
|
|
@@ -31,7 +37,7 @@ describe('createMathInlineView (temml snapshot)', () => {
|
|
|
31
37
|
const { editor, n } = setupTest(temmlRenderer)
|
|
32
38
|
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
33
39
|
|
|
34
|
-
const display = editor.view.dom.querySelector('.
|
|
40
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
35
41
|
const html = formatHTML(display?.innerHTML || '')
|
|
36
42
|
expect(html).toMatchInlineSnapshot(`
|
|
37
43
|
"
|
|
@@ -60,7 +66,7 @@ describe('createMathInlineView (katex snapshot)', () => {
|
|
|
60
66
|
const { editor, n } = setupTest(katexRenderer)
|
|
61
67
|
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
62
68
|
|
|
63
|
-
const display = editor.view.dom.querySelector('.
|
|
69
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
64
70
|
const html = formatHTML(display?.innerHTML || '')
|
|
65
71
|
expect(html).toMatchInlineSnapshot(`
|
|
66
72
|
"
|
|
@@ -138,7 +144,7 @@ describe('createMathInlineView (mathjax snapshot)', () => {
|
|
|
138
144
|
const { editor, n } = setupTest(mathjaxRenderer)
|
|
139
145
|
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
140
146
|
|
|
141
|
-
const display = editor.view.dom.querySelector('.
|
|
147
|
+
const display = editor.view.dom.querySelector('.prosemirror-math-display')
|
|
142
148
|
const html = formatHTML(display?.innerHTML || '')
|
|
143
149
|
expect(html).toMatchInlineSnapshot(`
|
|
144
150
|
"
|
package/src/math-inline-view.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Node as ProseMirrorNode } from 'prosemirror-model'
|
|
2
2
|
import type { Decoration, NodeView } from 'prosemirror-view'
|
|
3
3
|
|
|
4
|
-
import { createElement } from './create-element'
|
|
5
|
-
import { createMathViewRender } from './math-view-render'
|
|
4
|
+
import { createElement } from './create-element.ts'
|
|
5
|
+
import { createMathViewRender } from './math-view-render.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The function to render a math inline.
|
|
@@ -12,22 +12,35 @@ import { createMathViewRender } from './math-view-render'
|
|
|
12
12
|
*/
|
|
13
13
|
export type RenderMathInline = (text: string, element: HTMLElement) => void
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Creates a {@link NodeView} for an inline math node. The view will show a
|
|
17
|
+
* source editor or a rendered display area based on the text cursor position.
|
|
18
|
+
*
|
|
19
|
+
* @param renderMathInline - A function that renders math text (e.g. TeX) into
|
|
20
|
+
* the display element. You can use libraries like [Temml](https://temml.org/)
|
|
21
|
+
* or [KaTeX](https://katex.org/).
|
|
22
|
+
* @param node - The ProseMirror node to render.
|
|
23
|
+
* @param decorations - The decorations applied to the node.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
15
27
|
export function createMathInlineView(
|
|
16
28
|
renderMathInline: RenderMathInline,
|
|
17
29
|
node: ProseMirrorNode,
|
|
18
30
|
decorations: readonly Decoration[],
|
|
19
31
|
): NodeView {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
32
|
+
const code = createElement('code')
|
|
33
|
+
const source = createElement('span', 'prosemirror-math-source', code)
|
|
34
|
+
const display = createElement('span', 'prosemirror-math-display')
|
|
35
|
+
const dom = createElement('span', 'prosemirror-math-inline', source, display)
|
|
23
36
|
|
|
24
|
-
const render = createMathViewRender(renderMathInline, source, display)
|
|
37
|
+
const render = createMathViewRender(renderMathInline, source, display, true)
|
|
25
38
|
|
|
26
39
|
render(node, decorations)
|
|
27
40
|
|
|
28
41
|
return {
|
|
29
42
|
dom,
|
|
30
|
-
contentDOM:
|
|
43
|
+
contentDOM: code,
|
|
31
44
|
update: (node, decorations) => {
|
|
32
45
|
render(node, decorations)
|
|
33
46
|
return true
|
package/src/math-view-render.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Node } from 'prosemirror-model'
|
|
2
2
|
import type { Decoration } from 'prosemirror-view'
|
|
3
3
|
|
|
4
|
-
import { hasCursorInsideDecoration } from './cursor-inside-plugin'
|
|
4
|
+
import { hasCursorInsideDecoration } from './cursor-inside-plugin.ts'
|
|
5
5
|
|
|
6
6
|
type RenderMath = (text: string, element: HTMLElement) => void
|
|
7
7
|
|
|
@@ -9,6 +9,7 @@ export function createMathViewRender(
|
|
|
9
9
|
renderMath: RenderMath,
|
|
10
10
|
source: HTMLElement,
|
|
11
11
|
display: HTMLElement,
|
|
12
|
+
inline: boolean,
|
|
12
13
|
) {
|
|
13
14
|
let prevNode: Node | undefined
|
|
14
15
|
let prevText: string | undefined
|
|
@@ -32,8 +33,18 @@ export function createMathViewRender(
|
|
|
32
33
|
|
|
33
34
|
// When the math node is selected, show the source code.
|
|
34
35
|
// Otherwise, show the rendered result.
|
|
35
|
-
source.style.display = selected ? '' : 'none'
|
|
36
36
|
display.style.display = selected ? 'none' : ''
|
|
37
|
+
if (!inline) {
|
|
38
|
+
source.style.display = selected ? '' : 'none'
|
|
39
|
+
} else {
|
|
40
|
+
// For inline source code, we don't use `display: none` because we need
|
|
41
|
+
// the source text rendered in the DOM to ensure the text cursor can be
|
|
42
|
+
// placed correctly.
|
|
43
|
+
Object.assign(
|
|
44
|
+
source.style,
|
|
45
|
+
selected ? visibleInlineSourceStyle : hiddenInlineSourceStyle,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
return function updateMathView(
|
|
@@ -44,3 +55,21 @@ export function createMathViewRender(
|
|
|
44
55
|
updateStyle(decorations)
|
|
45
56
|
}
|
|
46
57
|
}
|
|
58
|
+
|
|
59
|
+
const hiddenInlineSourceStyle: Partial<CSSStyleDeclaration> = {
|
|
60
|
+
display: 'inline-flex',
|
|
61
|
+
opacity: '0',
|
|
62
|
+
pointerEvents: 'none',
|
|
63
|
+
maxWidth: '0',
|
|
64
|
+
maxHeight: '0',
|
|
65
|
+
overflow: 'hidden',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const visibleInlineSourceStyle: Partial<CSSStyleDeclaration> = {
|
|
69
|
+
display: 'inline-flex',
|
|
70
|
+
opacity: '1',
|
|
71
|
+
pointerEvents: '',
|
|
72
|
+
maxWidth: '',
|
|
73
|
+
maxHeight: '',
|
|
74
|
+
overflow: '',
|
|
75
|
+
}
|
package/src/temml.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import Temml from 'temml'
|
|
2
2
|
|
|
3
3
|
export function renderTemmlMathBlock(text: string, element: HTMLElement) {
|
|
4
|
-
Temml.render(text, element, {
|
|
4
|
+
Temml.render(text, element, {
|
|
5
|
+
displayMode: true,
|
|
6
|
+
annotate: true,
|
|
7
|
+
throwOnError: false,
|
|
8
|
+
})
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
export function renderTemmlMathInline(text: string, element: HTMLElement) {
|
|
8
|
-
Temml.render(text, element, {
|
|
12
|
+
Temml.render(text, element, {
|
|
13
|
+
displayMode: false,
|
|
14
|
+
annotate: true,
|
|
15
|
+
throwOnError: false,
|
|
16
|
+
})
|
|
9
17
|
}
|
package/src/testing.ts
CHANGED
|
@@ -19,16 +19,16 @@ import { createEnterRuleCommand, type EnterRule } from 'prosemirror-enter-rules'
|
|
|
19
19
|
import { inputRules } from 'prosemirror-inputrules'
|
|
20
20
|
import type { Attrs } from 'prosemirror-model'
|
|
21
21
|
|
|
22
|
-
import { createCursorInsidePlugin } from './cursor-inside-plugin'
|
|
23
|
-
import { renderKaTeXMathBlock, renderKaTeXMathInline } from './katex'
|
|
24
|
-
import { mathBlockEnterRule } from './math-block-enter-rule'
|
|
25
|
-
import { mathBlockSpec } from './math-block-spec'
|
|
26
|
-
import { createMathBlockView } from './math-block-view'
|
|
27
|
-
import { createMathInlineInputRule } from './math-inline-input-rule'
|
|
28
|
-
import { mathInlineSpec } from './math-inline-spec'
|
|
29
|
-
import { createMathInlineView } from './math-inline-view'
|
|
30
|
-
import { renderMathJaxMathBlock, renderMathJaxMathInline } from './mathjax'
|
|
31
|
-
import { renderTemmlMathBlock, renderTemmlMathInline } from './temml'
|
|
22
|
+
import { createCursorInsidePlugin } from './cursor-inside-plugin.ts'
|
|
23
|
+
import { renderKaTeXMathBlock, renderKaTeXMathInline } from './katex.ts'
|
|
24
|
+
import { mathBlockEnterRule } from './math-block-enter-rule.ts'
|
|
25
|
+
import { mathBlockSpec } from './math-block-spec.ts'
|
|
26
|
+
import { createMathBlockView } from './math-block-view.ts'
|
|
27
|
+
import { createMathInlineInputRule } from './math-inline-input-rule.ts'
|
|
28
|
+
import { mathInlineSpec } from './math-inline-spec.ts'
|
|
29
|
+
import { createMathInlineView } from './math-inline-view.ts'
|
|
30
|
+
import { renderMathJaxMathBlock, renderMathJaxMathInline } from './mathjax.ts'
|
|
31
|
+
import { renderTemmlMathBlock, renderTemmlMathInline } from './temml.ts'
|
|
32
32
|
|
|
33
33
|
type RenderMath = (text: string, element: HTMLElement) => void
|
|
34
34
|
|
|
@@ -135,11 +135,7 @@ function defineMathBlockView(renderer: MathRenderer): Extension {
|
|
|
135
135
|
return defineNodeView({
|
|
136
136
|
name: 'mathBlock',
|
|
137
137
|
constructor: (node, _view, _getPos, decorations) => {
|
|
138
|
-
return createMathBlockView(
|
|
139
|
-
renderer.renderBlock,
|
|
140
|
-
node,
|
|
141
|
-
decorations,
|
|
142
|
-
)
|
|
138
|
+
return createMathBlockView(renderer.renderBlock, node, decorations)
|
|
143
139
|
},
|
|
144
140
|
})
|
|
145
141
|
}
|
|
@@ -148,11 +144,7 @@ function defineMathInlineView(renderer: MathRenderer): Extension {
|
|
|
148
144
|
return defineNodeView({
|
|
149
145
|
name: 'mathInline',
|
|
150
146
|
constructor: (node, _view, _getPos, decorations) => {
|
|
151
|
-
return createMathInlineView(
|
|
152
|
-
renderer.renderInline,
|
|
153
|
-
node,
|
|
154
|
-
decorations,
|
|
155
|
-
)
|
|
147
|
+
return createMathInlineView(renderer.renderInline, node, decorations)
|
|
156
148
|
},
|
|
157
149
|
})
|
|
158
150
|
}
|
|
@@ -173,7 +165,9 @@ function defineTestExtension(renderer: MathRenderer) {
|
|
|
173
165
|
defineMathBlockView(renderer),
|
|
174
166
|
defineMathInlineView(renderer),
|
|
175
167
|
defineEnterRule([mathBlockEnterRule]),
|
|
176
|
-
definePlugin(
|
|
168
|
+
definePlugin(
|
|
169
|
+
inputRules({ rules: [createMathInlineInputRule('mathInline')] }),
|
|
170
|
+
),
|
|
177
171
|
definePlugin(createCursorInsidePlugin()),
|
|
178
172
|
defineBaseCommands(),
|
|
179
173
|
defineBaseKeymap(),
|