prosemirror-math 0.0.1
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 +21 -0
- package/README.md +118 -0
- package/dist/prosemirror-math.d.ts +42 -0
- package/dist/prosemirror-math.js +154 -0
- package/package.json +77 -0
- package/src/create-element.spec.ts +38 -0
- package/src/create-element.ts +14 -0
- package/src/cursor-inside-plugin.spec.ts +51 -0
- package/src/cursor-inside-plugin.ts +39 -0
- package/src/index.ts +7 -0
- package/src/math-block-enter-rule.spec.ts +54 -0
- package/src/math-block-enter-rule.ts +8 -0
- package/src/math-block-spec.spec.ts +65 -0
- package/src/math-block-spec.ts +22 -0
- package/src/math-block-view.spec.ts +59 -0
- package/src/math-block-view.ts +40 -0
- package/src/math-inline-input-rule.spec.ts +124 -0
- package/src/math-inline-input-rule.ts +37 -0
- package/src/math-inline-spec.spec.ts +110 -0
- package/src/math-inline-spec.ts +24 -0
- package/src/math-inline-view.spec.ts +47 -0
- package/src/math-inline-view.ts +39 -0
- package/src/testing.ts +174 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 ocavue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# prosemirror-math
|
|
2
|
+
|
|
3
|
+
Math editing extensions for [ProseMirror](https://prosemirror.net/). Provides node specs, node views, input rules, and plugins for inline and block math.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install prosemirror-math
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Node specs
|
|
14
|
+
|
|
15
|
+
`mathBlockSpec` and `mathInlineSpec` are [NodeSpec](https://prosemirror.net/docs/ref/#model.NodeSpec) objects that you can add to your schema.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { mathBlockSpec, mathInlineSpec } from 'prosemirror-math'
|
|
19
|
+
import { Schema } from 'prosemirror-model'
|
|
20
|
+
|
|
21
|
+
const schema = new Schema({
|
|
22
|
+
nodes: {
|
|
23
|
+
doc: { content: 'block+' },
|
|
24
|
+
paragraph: { content: 'inline*', group: 'block', parseDOM: [{ tag: 'p' }], toDOM: () => ['p', 0] },
|
|
25
|
+
text: { group: 'inline' },
|
|
26
|
+
mathBlock: { ...mathBlockSpec, },
|
|
27
|
+
mathInline: { ...mathInlineSpec, },
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Node views
|
|
33
|
+
|
|
34
|
+
`createMathBlockView` and `createMathInlineView` create [NodeView](https://prosemirror.net/docs/ref/#view.NodeView) instances with a source editor and a rendered display area. You provide your own math rendering function (e.g. using [Temml](https://temml.org/) or [KaTeX](https://katex.org/)).
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createMathBlockView, createMathInlineView } from 'prosemirror-math'
|
|
38
|
+
import { EditorView } from 'prosemirror-view'
|
|
39
|
+
import Temml from 'temml'
|
|
40
|
+
|
|
41
|
+
const view = new EditorView(document.body, {
|
|
42
|
+
state,
|
|
43
|
+
nodeViews: {
|
|
44
|
+
mathBlock: (node) => createMathBlockView(node, (text, element) => {
|
|
45
|
+
Temml.render(text, element, { displayMode: true })
|
|
46
|
+
}),
|
|
47
|
+
mathInline: (node) => createMathInlineView(node, (text, element) => {
|
|
48
|
+
Temml.render(text, element, { displayMode: false })
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Input rules
|
|
55
|
+
|
|
56
|
+
`createMathInlineInputRule` creates a ProseMirror [InputRule](https://prosemirror.net/docs/ref/#inputrules.InputRule) that converts `$...$` or `$$...$$` into an inline math node.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { inputRules } from 'prosemirror-inputrules'
|
|
60
|
+
import { createMathInlineInputRule } from 'prosemirror-math'
|
|
61
|
+
|
|
62
|
+
const plugin = inputRules({
|
|
63
|
+
rules: [createMathInlineInputRule('mathInline')],
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Enter rule
|
|
68
|
+
|
|
69
|
+
`mathBlockEnterRule` is an [EnterRule](https://github.com/prosekit/prosekit/tree/master/packages/prosemirror-enter-rules) that converts a paragraph containing `$$` into a math block when Enter is pressed.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createEnterRulePlugin } from 'prosemirror-enter-rules'
|
|
73
|
+
import { mathBlockEnterRule } from 'prosemirror-math'
|
|
74
|
+
|
|
75
|
+
const plugin = createEnterRulePlugin({
|
|
76
|
+
rules: [mathBlockEnterRule],
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Cursor inside plugin
|
|
81
|
+
|
|
82
|
+
`createCursorInsidePlugin` adds a `prosekit-head-inside` CSS class to math nodes when the cursor is inside them, useful for styling the active math node.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { createCursorInsidePlugin } from 'prosemirror-math'
|
|
86
|
+
|
|
87
|
+
const plugin = createCursorInsidePlugin(['mathBlock', 'mathInline'])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API
|
|
91
|
+
|
|
92
|
+
### Node specs
|
|
93
|
+
|
|
94
|
+
- **`mathBlockSpec`** — NodeSpec for block math (`div.prosekit-math-block > pre > code`)
|
|
95
|
+
- **`mathInlineSpec`** — NodeSpec for inline math (`span.prosekit-math-inline > code`)
|
|
96
|
+
|
|
97
|
+
### Node views
|
|
98
|
+
|
|
99
|
+
- **`createMathBlockView(node, renderMath)`** — Creates a block math NodeView
|
|
100
|
+
- **`createMathInlineView(node, renderMath)`** — Creates an inline math NodeView
|
|
101
|
+
|
|
102
|
+
The `renderMath` callback receives `(text: string, element: HTMLElement)` and should render the math into the element.
|
|
103
|
+
|
|
104
|
+
### Input rules
|
|
105
|
+
|
|
106
|
+
- **`createMathInlineInputRule(nodeType)`** — Creates an InputRule for inline math
|
|
107
|
+
|
|
108
|
+
### Enter rules
|
|
109
|
+
|
|
110
|
+
- **`mathBlockEnterRule`** — EnterRule for converting `$$` into a math block
|
|
111
|
+
|
|
112
|
+
### Plugins
|
|
113
|
+
|
|
114
|
+
- **`createCursorInsidePlugin(nodeTypes)`** — Plugin that decorates math nodes containing the cursor
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Plugin } from "prosemirror-state";
|
|
2
|
+
import { NodeView } from "prosemirror-view";
|
|
3
|
+
import { EnterRule } from "prosemirror-enter-rules";
|
|
4
|
+
import { InputRule } from "prosemirror-inputrules";
|
|
5
|
+
import { Node, NodeSpec } from "prosemirror-model";
|
|
6
|
+
|
|
7
|
+
//#region src/cursor-inside-plugin.d.ts
|
|
8
|
+
declare function createCursorInsidePlugin(nodeTypes: string[]): Plugin;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/math-block-enter-rule.d.ts
|
|
11
|
+
declare const mathBlockEnterRule: EnterRule;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/math-block-spec.d.ts
|
|
14
|
+
declare const mathBlockSpec: NodeSpec;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/math-block-view.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* The function to render a math block.
|
|
19
|
+
*
|
|
20
|
+
* @param code - The code of the math block. For example, a TeX expression.
|
|
21
|
+
* @param element - A `<div>` element to render the math block.
|
|
22
|
+
*/
|
|
23
|
+
type RenderMathBlock = (code: string, element: HTMLElement) => void;
|
|
24
|
+
declare function createMathBlockView(node: Node, renderMathBlock: RenderMathBlock): NodeView;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/math-inline-input-rule.d.ts
|
|
27
|
+
declare function createMathInlineInputRule(nodeType: string): InputRule;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/math-inline-spec.d.ts
|
|
30
|
+
declare const mathInlineSpec: NodeSpec;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/math-inline-view.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* The function to render a math inline.
|
|
35
|
+
*
|
|
36
|
+
* @param code - The code of the math inline. For example, a TeX expression.
|
|
37
|
+
* @param element - A `<span>` element to render the math inline.
|
|
38
|
+
*/
|
|
39
|
+
type RenderMathInline = (code: string, element: HTMLElement) => void;
|
|
40
|
+
declare function createMathInlineView(node: Node, renderMathInline: RenderMathInline): NodeView;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { type RenderMathBlock, type RenderMathInline, createCursorInsidePlugin, createMathBlockView, createMathInlineInputRule, createMathInlineView, mathBlockEnterRule, mathBlockSpec, mathInlineSpec };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
3
|
+
import { createTextBlockEnterRule } from "prosemirror-enter-rules";
|
|
4
|
+
import { supportsRegexLookbehind } from "@ocavue/utils";
|
|
5
|
+
import { InputRule } from "prosemirror-inputrules";
|
|
6
|
+
|
|
7
|
+
//#region src/cursor-inside-plugin.ts
|
|
8
|
+
function createCursorInsideDecoration(state, nodeTypes) {
|
|
9
|
+
const { $head } = state.selection;
|
|
10
|
+
const node = $head.parent;
|
|
11
|
+
if (!nodeTypes.includes(node.type.name)) return;
|
|
12
|
+
const before = $head.before();
|
|
13
|
+
const deco = Decoration.node(before, before + node.nodeSize, { class: "prosekit-head-inside" });
|
|
14
|
+
return DecorationSet.create(state.doc, [deco]);
|
|
15
|
+
}
|
|
16
|
+
const key = new PluginKey("prosemirror-math-cursor-inside");
|
|
17
|
+
function createCursorInsidePlugin(nodeTypes) {
|
|
18
|
+
return new Plugin({
|
|
19
|
+
key,
|
|
20
|
+
state: {
|
|
21
|
+
init() {},
|
|
22
|
+
apply(tr, oldValue, oldState, newState) {
|
|
23
|
+
if (oldState.selection.eq(newState.selection) && tr.docChanged === false) return oldValue;
|
|
24
|
+
return createCursorInsideDecoration(newState, nodeTypes);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
props: { decorations(state) {
|
|
28
|
+
return key.getState(state);
|
|
29
|
+
} }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/math-block-enter-rule.ts
|
|
35
|
+
const MATH_BLOCK_ENTER_REGEXP = /^\$\$$/;
|
|
36
|
+
const mathBlockEnterRule = /* @__PURE__ */ createTextBlockEnterRule({
|
|
37
|
+
regex: MATH_BLOCK_ENTER_REGEXP,
|
|
38
|
+
type: "mathBlock"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/math-block-spec.ts
|
|
43
|
+
const mathBlockSpec = {
|
|
44
|
+
atom: false,
|
|
45
|
+
group: "block",
|
|
46
|
+
content: "text*",
|
|
47
|
+
code: true,
|
|
48
|
+
toDOM() {
|
|
49
|
+
return [
|
|
50
|
+
"div",
|
|
51
|
+
{ class: "prosekit-math-block" },
|
|
52
|
+
["pre", ["code", 0]]
|
|
53
|
+
];
|
|
54
|
+
},
|
|
55
|
+
parseDOM: [{ tag: "div.prosekit-math-block" }]
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/create-element.ts
|
|
60
|
+
function createElement(tag, className, ...children) {
|
|
61
|
+
const element = document.createElement(tag);
|
|
62
|
+
if (className) element.className = className;
|
|
63
|
+
if (children.length > 0) element.append(...children);
|
|
64
|
+
return element;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/math-block-view.ts
|
|
69
|
+
function createMathBlockView(node, renderMathBlock) {
|
|
70
|
+
const code = createElement("code");
|
|
71
|
+
const source = createElement("pre", "prosekit-math-source", code);
|
|
72
|
+
const display = createElement("div", "prosekit-math-display", source);
|
|
73
|
+
const dom = createElement("div", "prosekit-math-block", source, display);
|
|
74
|
+
let prevText = "";
|
|
75
|
+
const render = (node) => {
|
|
76
|
+
const nodeText = node.textContent;
|
|
77
|
+
if (prevText !== nodeText) {
|
|
78
|
+
prevText = nodeText;
|
|
79
|
+
renderMathBlock(nodeText, display);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
render(node);
|
|
83
|
+
return {
|
|
84
|
+
dom,
|
|
85
|
+
contentDOM: code,
|
|
86
|
+
update: (node) => {
|
|
87
|
+
render(node);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/math-inline-input-rule.ts
|
|
95
|
+
const MATH_INPUT_REGEXP = (supportsRegexLookbehind() ? "(?<!\\$)" : "") + "(\\$\\$?)([^\\s$](?:[^$]*[^\\s$])?)\\1$";
|
|
96
|
+
function createMathInlineInputRule(nodeType) {
|
|
97
|
+
return new InputRule(new RegExp(MATH_INPUT_REGEXP), (state, match, start, end) => {
|
|
98
|
+
const { tr, schema } = state;
|
|
99
|
+
const mathText = match[2];
|
|
100
|
+
if (!mathText) return null;
|
|
101
|
+
const type = schema.nodes[nodeType];
|
|
102
|
+
if (!type) return null;
|
|
103
|
+
const node = type.create(null, schema.text(mathText));
|
|
104
|
+
tr.replaceWith(start, end, node);
|
|
105
|
+
return tr;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/math-inline-spec.ts
|
|
111
|
+
const mathInlineSpec = {
|
|
112
|
+
atom: false,
|
|
113
|
+
inline: true,
|
|
114
|
+
group: "inline",
|
|
115
|
+
content: "text*",
|
|
116
|
+
selectable: false,
|
|
117
|
+
code: true,
|
|
118
|
+
toDOM() {
|
|
119
|
+
return [
|
|
120
|
+
"span",
|
|
121
|
+
{ class: "prosekit-math-inline" },
|
|
122
|
+
["code", 0]
|
|
123
|
+
];
|
|
124
|
+
},
|
|
125
|
+
parseDOM: [{ tag: "span.prosekit-math-inline" }]
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/math-inline-view.ts
|
|
130
|
+
function createMathInlineView(node, renderMathInline) {
|
|
131
|
+
const source = createElement("code", "prosekit-math-source");
|
|
132
|
+
const display = createElement("span", "prosekit-math-display", source);
|
|
133
|
+
const dom = createElement("span", "prosekit-math-inline", source, display);
|
|
134
|
+
let prevText = "";
|
|
135
|
+
const render = (node) => {
|
|
136
|
+
const nodeText = node.textContent;
|
|
137
|
+
if (prevText !== nodeText) {
|
|
138
|
+
prevText = nodeText;
|
|
139
|
+
renderMathInline(nodeText, display);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
render(node);
|
|
143
|
+
return {
|
|
144
|
+
dom,
|
|
145
|
+
contentDOM: source,
|
|
146
|
+
update: (node) => {
|
|
147
|
+
render(node);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
export { createCursorInsidePlugin, createMathBlockView, createMathInlineInputRule, createMathInlineView, mathBlockEnterRule, mathBlockSpec, mathInlineSpec };
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prosemirror-math",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "Math extensions for ProseMirror",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "ocavue",
|
|
9
|
+
"email": "ocavue@gmail.com"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"funding": "https://github.com/sponsors/ocavue",
|
|
13
|
+
"homepage": "https://github.com/prosekit/prosekit#readme",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/prosekit/prosekit.git",
|
|
17
|
+
"directory": "packages/prosemirror-math"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/prosekit/prosekit/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ProseMirror"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./dist/prosemirror-math.js",
|
|
27
|
+
"module": "./dist/prosemirror-math.js",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/prosemirror-math.d.ts",
|
|
31
|
+
"default": "./dist/prosemirror-math.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@ocavue/utils": "^1.5.0",
|
|
40
|
+
"prosemirror-inputrules": "^1.4.0",
|
|
41
|
+
"prosemirror-model": "^1.25.4",
|
|
42
|
+
"prosemirror-state": "^1.4.4",
|
|
43
|
+
"prosemirror-view": "^1.41.6",
|
|
44
|
+
"prosemirror-enter-rules": "^0.1.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"diffable-html-snapshot": "^0.2.0",
|
|
48
|
+
"temml": "^0.13.1",
|
|
49
|
+
"tsdown": "^0.20.3",
|
|
50
|
+
"typescript": "~5.9.3",
|
|
51
|
+
"vitest": "^4.0.18",
|
|
52
|
+
"vitest-browser-commands": "^0.2.0",
|
|
53
|
+
"@prosekit/config-vitest": "0.0.0",
|
|
54
|
+
"@prosekit/core": "^0.10.0",
|
|
55
|
+
"@prosekit/pm": "^0.1.15"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"dev": {}
|
|
59
|
+
},
|
|
60
|
+
"dev": {
|
|
61
|
+
"entry": {
|
|
62
|
+
"prosemirror-math": "./src/index.ts"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build:tsc": "tsc -b tsconfig.json",
|
|
67
|
+
"build:tsdown": "tsdown"
|
|
68
|
+
},
|
|
69
|
+
"types": "./dist/prosemirror-math.d.ts",
|
|
70
|
+
"typesVersions": {
|
|
71
|
+
"*": {
|
|
72
|
+
".": [
|
|
73
|
+
"./dist/prosemirror-math.d.ts"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createElement } from './create-element'
|
|
4
|
+
|
|
5
|
+
describe('createElement', () => {
|
|
6
|
+
it('creates an element with the given tag', () => {
|
|
7
|
+
const el = createElement('div')
|
|
8
|
+
expect(el.tagName).toBe('DIV')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('sets the className when provided', () => {
|
|
12
|
+
const el = createElement('span', 'my-class')
|
|
13
|
+
expect(el.className).toBe('my-class')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('does not set className when empty string', () => {
|
|
17
|
+
const el = createElement('div', '')
|
|
18
|
+
expect(el.className).toBe('')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('appends children', () => {
|
|
22
|
+
const child1 = document.createElement('span')
|
|
23
|
+
const child2 = document.createElement('em')
|
|
24
|
+
const el = createElement('div', '', child1, child2)
|
|
25
|
+
expect(el.children.length).toBe(2)
|
|
26
|
+
expect(el.children[0]).toBe(child1)
|
|
27
|
+
expect(el.children[1]).toBe(child2)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('sets className and appends children together', () => {
|
|
31
|
+
const child = document.createElement('code')
|
|
32
|
+
const el = createElement('pre', 'source', child)
|
|
33
|
+
expect(el.tagName).toBe('PRE')
|
|
34
|
+
expect(el.className).toBe('source')
|
|
35
|
+
expect(el.children.length).toBe(1)
|
|
36
|
+
expect(el.children[0]).toBe(child)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createElement<Tag extends keyof HTMLElementTagNameMap>(
|
|
2
|
+
tag: Tag,
|
|
3
|
+
className?: string,
|
|
4
|
+
...children: HTMLElement[]
|
|
5
|
+
): HTMLElementTagNameMap[Tag] {
|
|
6
|
+
const element: HTMLElementTagNameMap[Tag] = document.createElement(tag)
|
|
7
|
+
if (className) {
|
|
8
|
+
element.className = className
|
|
9
|
+
}
|
|
10
|
+
if (children.length > 0) {
|
|
11
|
+
element.append(...children)
|
|
12
|
+
}
|
|
13
|
+
return element
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { TextSelection } from '@prosekit/pm/state'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { setupTest } from './testing'
|
|
5
|
+
|
|
6
|
+
describe('cursorInsidePlugin', () => {
|
|
7
|
+
it('applies decoration when cursor is inside mathBlock', () => {
|
|
8
|
+
const { editor, n } = setupTest()
|
|
9
|
+
editor.set(n.doc(n.mathBlock('x^2')))
|
|
10
|
+
|
|
11
|
+
// Place cursor inside the math block
|
|
12
|
+
const { state } = editor.view
|
|
13
|
+
const tr = state.tr.setSelection(
|
|
14
|
+
TextSelection.near(state.doc.resolve(2)),
|
|
15
|
+
)
|
|
16
|
+
editor.view.dispatch(tr)
|
|
17
|
+
|
|
18
|
+
const mathBlock = editor.view.dom.querySelector('.prosekit-math-block')
|
|
19
|
+
expect(mathBlock?.classList.contains('prosekit-head-inside')).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('applies decoration when cursor is inside mathInline', () => {
|
|
23
|
+
const { editor, n } = setupTest()
|
|
24
|
+
editor.set(n.doc(n.paragraph(n.mathInline('y'))))
|
|
25
|
+
|
|
26
|
+
// Place cursor inside the math inline node
|
|
27
|
+
const { state } = editor.view
|
|
28
|
+
const tr = state.tr.setSelection(
|
|
29
|
+
TextSelection.near(state.doc.resolve(2)),
|
|
30
|
+
)
|
|
31
|
+
editor.view.dispatch(tr)
|
|
32
|
+
|
|
33
|
+
const mathInline = editor.view.dom.querySelector('.prosekit-math-inline')
|
|
34
|
+
expect(mathInline?.classList.contains('prosekit-head-inside')).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not apply decoration when cursor is outside math nodes', () => {
|
|
38
|
+
const { editor, n } = setupTest()
|
|
39
|
+
editor.set(n.doc(n.paragraph('hello'), n.mathBlock('x^2')))
|
|
40
|
+
|
|
41
|
+
// Place cursor inside the paragraph (position 2 should be inside "hello")
|
|
42
|
+
const { state } = editor.view
|
|
43
|
+
const tr = state.tr.setSelection(
|
|
44
|
+
TextSelection.near(state.doc.resolve(2)),
|
|
45
|
+
)
|
|
46
|
+
editor.view.dispatch(tr)
|
|
47
|
+
|
|
48
|
+
const mathBlock = editor.view.dom.querySelector('.prosekit-math-block')
|
|
49
|
+
expect(mathBlock?.classList.contains('prosekit-head-inside')).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Plugin, PluginKey, type EditorState } from 'prosemirror-state'
|
|
2
|
+
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
3
|
+
|
|
4
|
+
function createCursorInsideDecoration(
|
|
5
|
+
state: EditorState,
|
|
6
|
+
nodeTypes: string[],
|
|
7
|
+
): DecorationSet | undefined {
|
|
8
|
+
const { $head } = state.selection
|
|
9
|
+
const node = $head.parent
|
|
10
|
+
|
|
11
|
+
if (!nodeTypes.includes(node.type.name)) return
|
|
12
|
+
const before = $head.before()
|
|
13
|
+
const deco = Decoration.node(before, before + node.nodeSize, { class: 'prosekit-head-inside' })
|
|
14
|
+
return DecorationSet.create(state.doc, [deco])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PluginState = DecorationSet | undefined
|
|
18
|
+
|
|
19
|
+
const key = new PluginKey<PluginState>('prosemirror-math-cursor-inside')
|
|
20
|
+
|
|
21
|
+
export function createCursorInsidePlugin(nodeTypes: string[]): Plugin {
|
|
22
|
+
return new Plugin<PluginState>({
|
|
23
|
+
key,
|
|
24
|
+
state: {
|
|
25
|
+
init(): PluginState {
|
|
26
|
+
return undefined
|
|
27
|
+
},
|
|
28
|
+
apply(tr, oldValue, oldState, newState): PluginState {
|
|
29
|
+
if (oldState.selection.eq(newState.selection) && tr.docChanged === false) return oldValue
|
|
30
|
+
return createCursorInsideDecoration(newState, nodeTypes)
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
props: {
|
|
34
|
+
decorations(state: EditorState): PluginState | undefined {
|
|
35
|
+
return key.getState(state)
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
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 { createMathInlineView, type RenderMathInline } from './math-inline-view'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { userEvent } from 'vitest/browser'
|
|
3
|
+
|
|
4
|
+
import { MATH_BLOCK_ENTER_REGEXP } from './math-block-enter-rule'
|
|
5
|
+
import { setupTest } from './testing'
|
|
6
|
+
|
|
7
|
+
describe('MATH_BLOCK_ENTER_REGEXP', () => {
|
|
8
|
+
const cases: Array<[input: string, matched: boolean]> = [
|
|
9
|
+
['$$', true],
|
|
10
|
+
['$', false],
|
|
11
|
+
['$$$', false],
|
|
12
|
+
['hello', false],
|
|
13
|
+
['$$x', false],
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
it.each(cases)('should handle %s', (input, expected) => {
|
|
17
|
+
const match = MATH_BLOCK_ENTER_REGEXP.exec(input)
|
|
18
|
+
expect(match !== null).toBe(expected)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('defineMathBlockEnterRule', () => {
|
|
23
|
+
const { editor, n } = setupTest()
|
|
24
|
+
|
|
25
|
+
it('should create mathBlock when typing $$ and pressing Enter', async () => {
|
|
26
|
+
editor.set(n.doc(n.p('<a>')))
|
|
27
|
+
|
|
28
|
+
await userEvent.keyboard('$$')
|
|
29
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
30
|
+
n.doc(n.p('$$')).toJSON(),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
await userEvent.keyboard('{Enter}')
|
|
34
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
35
|
+
n.doc(n.mathBlock()).toJSON(),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// Selection should be inside the mathBlock
|
|
39
|
+
const { $from } = editor.view.state.selection
|
|
40
|
+
expect($from.parent.type.name).toBe('mathBlock')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should not create mathBlock when typing $$ inside text', async () => {
|
|
44
|
+
editor.set(n.doc(n.p('hello <a>')))
|
|
45
|
+
|
|
46
|
+
await userEvent.keyboard('$$')
|
|
47
|
+
await userEvent.keyboard('{Enter}')
|
|
48
|
+
|
|
49
|
+
// Should not convert to mathBlock since paragraph had other text
|
|
50
|
+
expect(editor.view.state.doc.toJSON()).not.toEqual(
|
|
51
|
+
n.doc(n.mathBlock()).toJSON(),
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createTextBlockEnterRule, type EnterRule } from 'prosemirror-enter-rules'
|
|
2
|
+
|
|
3
|
+
export const MATH_BLOCK_ENTER_REGEXP: RegExp = /^\$\$$/
|
|
4
|
+
|
|
5
|
+
export const mathBlockEnterRule: EnterRule = /* @__PURE__ */ createTextBlockEnterRule({
|
|
6
|
+
regex: MATH_BLOCK_ENTER_REGEXP,
|
|
7
|
+
type: 'mathBlock',
|
|
8
|
+
})
|