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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { setupTest } from './testing'
|
|
5
|
+
|
|
6
|
+
describe('mathBlockSpec', () => {
|
|
7
|
+
it('can serialize to HTML', () => {
|
|
8
|
+
const { editor, n } = setupTest()
|
|
9
|
+
editor.set(n.doc(n.mathBlock('E = mc^2')))
|
|
10
|
+
const html = editor.getDocHTML()
|
|
11
|
+
expect(formatHTML(html)).toMatchInlineSnapshot(
|
|
12
|
+
`
|
|
13
|
+
"
|
|
14
|
+
<div>
|
|
15
|
+
<div class="prosekit-math-block">
|
|
16
|
+
<pre>
|
|
17
|
+
<code>
|
|
18
|
+
E = mc^2
|
|
19
|
+
</code>
|
|
20
|
+
</pre>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
"
|
|
24
|
+
`,
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('can serialize to JSON', () => {
|
|
29
|
+
const { editor, n } = setupTest()
|
|
30
|
+
editor.set(n.doc(n.mathBlock('x^2 + y^2 = z^2')))
|
|
31
|
+
const json = editor.getDocJSON()
|
|
32
|
+
expect(json).toMatchInlineSnapshot(`
|
|
33
|
+
{
|
|
34
|
+
"content": [
|
|
35
|
+
{
|
|
36
|
+
"content": [
|
|
37
|
+
{
|
|
38
|
+
"text": "x^2 + y^2 = z^2",
|
|
39
|
+
"type": "text",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
"type": "mathBlock",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
"type": "doc",
|
|
46
|
+
}
|
|
47
|
+
`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('can handle empty math block', () => {
|
|
51
|
+
const { editor, n } = setupTest()
|
|
52
|
+
editor.set(n.doc(n.mathBlock()))
|
|
53
|
+
const json = editor.getDocJSON()
|
|
54
|
+
expect(json).toMatchInlineSnapshot(`
|
|
55
|
+
{
|
|
56
|
+
"content": [
|
|
57
|
+
{
|
|
58
|
+
"type": "mathBlock",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
"type": "doc",
|
|
62
|
+
}
|
|
63
|
+
`)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NodeSpec } from 'prosemirror-model'
|
|
2
|
+
|
|
3
|
+
export const mathBlockSpec: NodeSpec = {
|
|
4
|
+
atom: false,
|
|
5
|
+
group: 'block',
|
|
6
|
+
content: 'text*',
|
|
7
|
+
code: true,
|
|
8
|
+
toDOM() {
|
|
9
|
+
return [
|
|
10
|
+
'div',
|
|
11
|
+
{
|
|
12
|
+
class: 'prosekit-math-block',
|
|
13
|
+
},
|
|
14
|
+
['pre', ['code', 0]],
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
parseDOM: [
|
|
18
|
+
{
|
|
19
|
+
tag: 'div.prosekit-math-block',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { setupTest } from './testing'
|
|
5
|
+
|
|
6
|
+
describe('createMathBlockView', () => {
|
|
7
|
+
it('renders the math block DOM structure', () => {
|
|
8
|
+
const { editor, n } = setupTest()
|
|
9
|
+
editor.set(n.doc(n.mathBlock('x^2')))
|
|
10
|
+
|
|
11
|
+
const dom = editor.view.dom
|
|
12
|
+
const mathBlock = dom.querySelector('.prosekit-math-block')
|
|
13
|
+
expect(mathBlock).toBeTruthy()
|
|
14
|
+
expect(mathBlock?.querySelector('.prosekit-math-source')).toBeTruthy()
|
|
15
|
+
expect(mathBlock?.querySelector('.prosekit-math-display')).toBeTruthy()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders Temml output in the display element', () => {
|
|
19
|
+
const { editor, n } = setupTest()
|
|
20
|
+
editor.set(n.doc(n.mathBlock('x^2')))
|
|
21
|
+
|
|
22
|
+
const display = editor.view.dom.querySelector('.prosekit-math-display')
|
|
23
|
+
const html = formatHTML(display?.innerHTML || '')
|
|
24
|
+
expect(html).toMatchInlineSnapshot(`
|
|
25
|
+
"
|
|
26
|
+
<math
|
|
27
|
+
class="tml-display"
|
|
28
|
+
display="block"
|
|
29
|
+
style="display: block math;"
|
|
30
|
+
>
|
|
31
|
+
<msup>
|
|
32
|
+
<mi>
|
|
33
|
+
x
|
|
34
|
+
</mi>
|
|
35
|
+
<mn class="tml-sml-pad">
|
|
36
|
+
2
|
|
37
|
+
</mn>
|
|
38
|
+
</msup>
|
|
39
|
+
</math>
|
|
40
|
+
"
|
|
41
|
+
`)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('updates display when content changes', () => {
|
|
45
|
+
const { editor, n } = setupTest()
|
|
46
|
+
editor.set(n.doc(n.mathBlock('x^2')))
|
|
47
|
+
|
|
48
|
+
const display = editor.view.dom.querySelector('.prosekit-math-display')
|
|
49
|
+
const initialHTML = display?.innerHTML
|
|
50
|
+
|
|
51
|
+
// Dispatch a transaction to change content
|
|
52
|
+
const { state } = editor.view
|
|
53
|
+
const tr = state.tr.insertText(' + y^2', 3)
|
|
54
|
+
editor.view.dispatch(tr)
|
|
55
|
+
|
|
56
|
+
const updatedHTML = display?.innerHTML
|
|
57
|
+
expect(updatedHTML).not.toBe(initialHTML)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Node as ProseMirrorNode } from 'prosemirror-model'
|
|
2
|
+
import type { NodeView } from 'prosemirror-view'
|
|
3
|
+
|
|
4
|
+
import { createElement } from './create-element'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The function to render a math block.
|
|
8
|
+
*
|
|
9
|
+
* @param code - The code of the math block. For example, a TeX expression.
|
|
10
|
+
* @param element - A `<div>` element to render the math block.
|
|
11
|
+
*/
|
|
12
|
+
export type RenderMathBlock = (code: string, element: HTMLElement) => void
|
|
13
|
+
|
|
14
|
+
export function createMathBlockView(node: ProseMirrorNode, renderMathBlock: RenderMathBlock): NodeView {
|
|
15
|
+
const code = createElement('code')
|
|
16
|
+
const source = createElement('pre', 'prosekit-math-source', code)
|
|
17
|
+
const display = createElement('div', 'prosekit-math-display', source)
|
|
18
|
+
const dom = createElement('div', 'prosekit-math-block', source, display)
|
|
19
|
+
|
|
20
|
+
let prevText = ''
|
|
21
|
+
|
|
22
|
+
const render = (node: ProseMirrorNode) => {
|
|
23
|
+
const nodeText = node.textContent
|
|
24
|
+
if (prevText !== nodeText) {
|
|
25
|
+
prevText = nodeText
|
|
26
|
+
renderMathBlock(nodeText, display)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render(node)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
dom,
|
|
34
|
+
contentDOM: code,
|
|
35
|
+
update: (node) => {
|
|
36
|
+
render(node)
|
|
37
|
+
return true
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { userEvent } from 'vitest/browser'
|
|
3
|
+
|
|
4
|
+
import { MATH_INPUT_REGEXP } from './math-inline-input-rule'
|
|
5
|
+
import { setupTest } from './testing'
|
|
6
|
+
|
|
7
|
+
describe('MATH_INPUT_REGEXP', () => {
|
|
8
|
+
const regexp = new RegExp(MATH_INPUT_REGEXP)
|
|
9
|
+
|
|
10
|
+
const cases: Array<[input: string, delimiter: string | null, captured: string | null]> = [
|
|
11
|
+
// Single dollar: inline math
|
|
12
|
+
['$x$', '$', 'x'],
|
|
13
|
+
['$x^2$', '$', 'x^2'],
|
|
14
|
+
['$a+b=c$', '$', 'a+b=c'],
|
|
15
|
+
[String.raw`$\alpha$`, '$', String.raw`\alpha`],
|
|
16
|
+
[String.raw`$\frac{a}{b}$`, '$', String.raw`\frac{a}{b}`],
|
|
17
|
+
['hello $x$', '$', 'x'],
|
|
18
|
+
['The formula $E=mc^2$', '$', 'E=mc^2'],
|
|
19
|
+
['$a + b$', '$', 'a + b'],
|
|
20
|
+
['$a$', '$', 'a'],
|
|
21
|
+
|
|
22
|
+
// Double dollar: block math
|
|
23
|
+
['$$x$$', '$$', 'x'],
|
|
24
|
+
['$$x^2$$', '$$', 'x^2'],
|
|
25
|
+
['$$a+b=c$$', '$$', 'a+b=c'],
|
|
26
|
+
[String.raw`$$\alpha$$`, '$$', String.raw`\alpha`],
|
|
27
|
+
['$$a + b$$', '$$', 'a + b'],
|
|
28
|
+
|
|
29
|
+
// Empty content — should not match
|
|
30
|
+
['$$', null, null],
|
|
31
|
+
['$$$$', null, null],
|
|
32
|
+
|
|
33
|
+
// Whitespace at boundaries — should not match
|
|
34
|
+
['$ x$', null, null],
|
|
35
|
+
['$x $', null, null],
|
|
36
|
+
['$ x $', null, null],
|
|
37
|
+
['$$ x$$', null, null],
|
|
38
|
+
['$$x $$', null, null],
|
|
39
|
+
|
|
40
|
+
// Mismatched delimiters — should not match
|
|
41
|
+
['$x$$', null, null],
|
|
42
|
+
['$$x$', null, null],
|
|
43
|
+
['$$$x=2$$', null, null],
|
|
44
|
+
|
|
45
|
+
// No dollar signs
|
|
46
|
+
['hello', null, null],
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
it.each(cases)('should handle %s', (input, expectedDelimiter, expectedContent) => {
|
|
50
|
+
const match = regexp.exec(input)
|
|
51
|
+
const delimiter = match?.[1] ?? null
|
|
52
|
+
const captured = match?.[2] ?? null
|
|
53
|
+
expect(delimiter).toEqual(expectedDelimiter)
|
|
54
|
+
expect(captured).toEqual(expectedContent)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('defineMathInlineInputRule', () => {
|
|
59
|
+
const { editor, n } = setupTest()
|
|
60
|
+
|
|
61
|
+
it('should create mathInline when typing $...$', async () => {
|
|
62
|
+
editor.set(n.doc(n.p('<a>')))
|
|
63
|
+
|
|
64
|
+
await userEvent.keyboard('$x^2')
|
|
65
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
66
|
+
n.doc(n.p('$x^2')).toJSON(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
await userEvent.keyboard('$')
|
|
70
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
71
|
+
n.doc(n.p(n.mathInline('x^2'))).toJSON(),
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should create mathInline when typing $$...$$', async () => {
|
|
76
|
+
editor.set(n.doc(n.p('<a>')))
|
|
77
|
+
|
|
78
|
+
await userEvent.keyboard('$$x^2$')
|
|
79
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
80
|
+
n.doc(n.p('$$x^2$')).toJSON(),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await userEvent.keyboard('$')
|
|
84
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
85
|
+
n.doc(n.p(n.mathInline('x^2'))).toJSON(),
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should create mathInline with multi-character content', async () => {
|
|
90
|
+
editor.set(n.doc(n.p('<a>')))
|
|
91
|
+
|
|
92
|
+
await userEvent.keyboard('$a+b=c$')
|
|
93
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
94
|
+
n.doc(n.p(n.mathInline('a+b=c'))).toJSON(),
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should not trigger with empty content $$', async () => {
|
|
99
|
+
editor.set(n.doc(n.p('<a>')))
|
|
100
|
+
|
|
101
|
+
await userEvent.keyboard('$$')
|
|
102
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
103
|
+
n.doc(n.p('$$')).toJSON(),
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should not trigger with spaces at boundaries', async () => {
|
|
108
|
+
editor.set(n.doc(n.p('<a>')))
|
|
109
|
+
|
|
110
|
+
await userEvent.keyboard('$ x^2 $')
|
|
111
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
112
|
+
n.doc(n.p('$ x^2 $')).toJSON(),
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should not trigger inside a code block', async () => {
|
|
117
|
+
editor.set(n.doc(n.codeBlock('<a>')))
|
|
118
|
+
|
|
119
|
+
await userEvent.keyboard('$x^2$')
|
|
120
|
+
expect(editor.view.state.doc.toJSON()).toEqual(
|
|
121
|
+
n.doc(n.codeBlock('$x^2$')).toJSON(),
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* eslint-disable unicorn/prefer-string-raw -- Don't use String.raw here for better bundler minification */
|
|
2
|
+
|
|
3
|
+
import { supportsRegexLookbehind } from '@ocavue/utils'
|
|
4
|
+
import { InputRule } from 'prosemirror-inputrules'
|
|
5
|
+
|
|
6
|
+
// Matches text wrapped in `$` or `$$`, e.g. `$x^2$` or `$$x^2$$`.
|
|
7
|
+
export const MATH_INPUT_REGEXP: string = (
|
|
8
|
+
// Don't allow $ before the opening delimiter
|
|
9
|
+
(supportsRegexLookbehind() ? '(?<!\\$)' : '')
|
|
10
|
+
// capture group 1: opening delimiter (`$` or `$$`)
|
|
11
|
+
+ '(\\$\\$?)'
|
|
12
|
+
// capture group 2: the math content
|
|
13
|
+
// - a single non-whitespace, non-`$` character
|
|
14
|
+
// - optionally followed by any non-`$` characters and a final non-whitespace, non-`$` character
|
|
15
|
+
+ '([^\\s$](?:[^$]*[^\\s$])?)'
|
|
16
|
+
// backreference: closing delimiter must match the opening
|
|
17
|
+
+ '\\1'
|
|
18
|
+
// end of input (required by ProseMirror InputRule)
|
|
19
|
+
+ '$'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export function createMathInlineInputRule(
|
|
23
|
+
nodeType: string,
|
|
24
|
+
): InputRule {
|
|
25
|
+
return new InputRule(new RegExp(MATH_INPUT_REGEXP), (state, match, start, end) => {
|
|
26
|
+
const { tr, schema } = state
|
|
27
|
+
const mathText = match[2]
|
|
28
|
+
if (!mathText) return null
|
|
29
|
+
|
|
30
|
+
const type = schema.nodes[nodeType]
|
|
31
|
+
if (!type) return null
|
|
32
|
+
|
|
33
|
+
const node = type.create(null, schema.text(mathText))
|
|
34
|
+
tr.replaceWith(start, end, node)
|
|
35
|
+
return tr
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { setupTest } from './testing'
|
|
5
|
+
|
|
6
|
+
describe('mathInlineSpec', () => {
|
|
7
|
+
it('can serialize to HTML', () => {
|
|
8
|
+
const { editor, n } = setupTest()
|
|
9
|
+
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
10
|
+
const html = editor.getDocHTML()
|
|
11
|
+
expect(formatHTML(html)).toMatchInlineSnapshot(
|
|
12
|
+
`
|
|
13
|
+
"
|
|
14
|
+
<div>
|
|
15
|
+
<p>
|
|
16
|
+
<span class="prosekit-math-inline">
|
|
17
|
+
<code>
|
|
18
|
+
x^2
|
|
19
|
+
</code>
|
|
20
|
+
</span>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
"
|
|
24
|
+
`,
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('can serialize to JSON', () => {
|
|
29
|
+
const { editor, n } = setupTest()
|
|
30
|
+
editor.set(n.doc(n.paragraph(n.mathInline(String.raw`\alpha + \beta`))))
|
|
31
|
+
const json = editor.getDocJSON()
|
|
32
|
+
expect(json).toMatchInlineSnapshot(String.raw`
|
|
33
|
+
{
|
|
34
|
+
"content": [
|
|
35
|
+
{
|
|
36
|
+
"content": [
|
|
37
|
+
{
|
|
38
|
+
"content": [
|
|
39
|
+
{
|
|
40
|
+
"text": "\alpha + \beta",
|
|
41
|
+
"type": "text",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
"type": "mathInline",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
"type": "paragraph",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
"type": "doc",
|
|
51
|
+
}
|
|
52
|
+
`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('can handle empty math inline', () => {
|
|
56
|
+
const { editor, n } = setupTest()
|
|
57
|
+
editor.set(n.doc(n.paragraph(n.mathInline())))
|
|
58
|
+
const json = editor.getDocJSON()
|
|
59
|
+
expect(json).toMatchInlineSnapshot(`
|
|
60
|
+
{
|
|
61
|
+
"content": [
|
|
62
|
+
{
|
|
63
|
+
"content": [
|
|
64
|
+
{
|
|
65
|
+
"type": "mathInline",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
"type": "paragraph",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
"type": "doc",
|
|
72
|
+
}
|
|
73
|
+
`)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('can be mixed with regular text', () => {
|
|
77
|
+
const { editor, n } = setupTest()
|
|
78
|
+
editor.set(n.doc(n.paragraph('The formula ', n.mathInline('E=mc^2'), ' is famous.')))
|
|
79
|
+
const json = editor.getDocJSON()
|
|
80
|
+
expect(json).toMatchInlineSnapshot(`
|
|
81
|
+
{
|
|
82
|
+
"content": [
|
|
83
|
+
{
|
|
84
|
+
"content": [
|
|
85
|
+
{
|
|
86
|
+
"text": "The formula ",
|
|
87
|
+
"type": "text",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"content": [
|
|
91
|
+
{
|
|
92
|
+
"text": "E=mc^2",
|
|
93
|
+
"type": "text",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
"type": "mathInline",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"text": " is famous.",
|
|
100
|
+
"type": "text",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
"type": "paragraph",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
"type": "doc",
|
|
107
|
+
}
|
|
108
|
+
`)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NodeSpec } from 'prosemirror-model'
|
|
2
|
+
|
|
3
|
+
export const mathInlineSpec: NodeSpec = {
|
|
4
|
+
atom: false,
|
|
5
|
+
inline: true,
|
|
6
|
+
group: 'inline',
|
|
7
|
+
content: 'text*',
|
|
8
|
+
selectable: false,
|
|
9
|
+
code: true,
|
|
10
|
+
toDOM() {
|
|
11
|
+
return [
|
|
12
|
+
'span',
|
|
13
|
+
{
|
|
14
|
+
class: 'prosekit-math-inline',
|
|
15
|
+
},
|
|
16
|
+
['code', 0],
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
parseDOM: [
|
|
20
|
+
{
|
|
21
|
+
tag: 'span.prosekit-math-inline',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { formatHTML } from 'diffable-html-snapshot'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { setupTest } from './testing'
|
|
5
|
+
|
|
6
|
+
describe('createMathInlineView', () => {
|
|
7
|
+
it('renders the math inline DOM structure', () => {
|
|
8
|
+
const { editor, n } = setupTest()
|
|
9
|
+
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
10
|
+
|
|
11
|
+
const dom = editor.view.dom
|
|
12
|
+
const mathInline = dom.querySelector('.prosekit-math-inline')
|
|
13
|
+
expect(mathInline).toBeTruthy()
|
|
14
|
+
expect(mathInline?.querySelector('.prosekit-math-source')).toBeTruthy()
|
|
15
|
+
expect(mathInline?.querySelector('.prosekit-math-display')).toBeTruthy()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders Temml output in the display element', () => {
|
|
19
|
+
const { editor, n } = setupTest()
|
|
20
|
+
editor.set(n.doc(n.paragraph(n.mathInline('x^2'))))
|
|
21
|
+
|
|
22
|
+
const display = editor.view.dom.querySelector('.prosekit-math-display')
|
|
23
|
+
const html = formatHTML(display?.innerHTML || '')
|
|
24
|
+
expect(html).toMatchInlineSnapshot(`
|
|
25
|
+
"
|
|
26
|
+
<math>
|
|
27
|
+
<msup>
|
|
28
|
+
<mi>
|
|
29
|
+
x
|
|
30
|
+
</mi>
|
|
31
|
+
<mn class="tml-sml-pad">
|
|
32
|
+
2
|
|
33
|
+
</mn>
|
|
34
|
+
</msup>
|
|
35
|
+
</math>
|
|
36
|
+
"
|
|
37
|
+
`)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('uses span elements for inline math', () => {
|
|
41
|
+
const { editor, n } = setupTest()
|
|
42
|
+
editor.set(n.doc(n.paragraph(n.mathInline('x'))))
|
|
43
|
+
|
|
44
|
+
const mathInline = editor.view.dom.querySelector('.prosekit-math-inline')
|
|
45
|
+
expect(mathInline?.tagName).toBe('SPAN')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Node as ProseMirrorNode } from 'prosemirror-model'
|
|
2
|
+
import type { NodeView } from 'prosemirror-view'
|
|
3
|
+
|
|
4
|
+
import { createElement } from './create-element'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The function to render a math inline.
|
|
8
|
+
*
|
|
9
|
+
* @param code - The code of the math inline. For example, a TeX expression.
|
|
10
|
+
* @param element - A `<span>` element to render the math inline.
|
|
11
|
+
*/
|
|
12
|
+
export type RenderMathInline = (code: string, element: HTMLElement) => void
|
|
13
|
+
|
|
14
|
+
export function createMathInlineView(node: ProseMirrorNode, renderMathInline: RenderMathInline): NodeView {
|
|
15
|
+
const source = createElement('code', 'prosekit-math-source')
|
|
16
|
+
const display = createElement('span', 'prosekit-math-display', source)
|
|
17
|
+
const dom = createElement('span', 'prosekit-math-inline', source, display)
|
|
18
|
+
|
|
19
|
+
let prevText = ''
|
|
20
|
+
|
|
21
|
+
const render = (node: ProseMirrorNode) => {
|
|
22
|
+
const nodeText = node.textContent
|
|
23
|
+
if (prevText !== nodeText) {
|
|
24
|
+
prevText = nodeText
|
|
25
|
+
renderMathInline(nodeText, display)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(node)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
dom,
|
|
33
|
+
contentDOM: source,
|
|
34
|
+
update: (node) => {
|
|
35
|
+
render(node)
|
|
36
|
+
return true
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|