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 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
+ })