@tiptap/extension-mathematics 3.0.0-beta.16 → 3.0.0-beta.18

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.
@@ -0,0 +1,251 @@
1
+ import { InputRule, mergeAttributes, Node } from '@tiptap/core'
2
+ import type { Node as PMNode } from '@tiptap/pm/model'
3
+ import katex, { type KatexOptions } from 'katex'
4
+
5
+ /**
6
+ * Configuration options for the BlockMath extension.
7
+ */
8
+ export type BlockMathOptions = {
9
+ /**
10
+ * KaTeX specific options
11
+ * @see https://katex.org/docs/options.html
12
+ * @example
13
+ * ```ts
14
+ * katexOptions: {
15
+ * displayMode: true,
16
+ * throwOnError: false,
17
+ * },
18
+ */
19
+ katexOptions?: KatexOptions
20
+
21
+ /**
22
+ * Optional click handler for block math nodes.
23
+ * Called when a user clicks on a block math expression in the editor.
24
+ *
25
+ * @param node - The ProseMirror node representing the block math element
26
+ * @param pos - The position of the node within the document
27
+ * @example
28
+ * ```ts
29
+ * onClick: (node, pos) => {
30
+ * console.log('Block math clicked:', node.attrs.latex, 'at position:', pos)
31
+ * },
32
+ * ```
33
+ */
34
+ onClick?: (node: PMNode, pos: number) => void
35
+ }
36
+
37
+ declare module '@tiptap/core' {
38
+ interface Commands<ReturnType> {
39
+ insertBlockMath: {
40
+ /**
41
+ * Inserts a math block node with LaTeX string.
42
+ * @param options - Options for inserting block math.
43
+ * @returns ReturnType
44
+ */
45
+ insertBlockMath: (options: { latex: string; pos?: number }) => ReturnType
46
+
47
+ /**
48
+ * Deletes a block math node.
49
+ * @returns ReturnType
50
+ */
51
+ deleteBlockMath: (options?: { pos?: number }) => ReturnType
52
+
53
+ /**
54
+ * Update block math node with optional LaTeX string.
55
+ * @param options - Options for updating block math.
56
+ * @returns ReturnType
57
+ */
58
+ updateBlockMath: (options?: { latex: string; pos?: number }) => ReturnType
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * BlockMath is a Tiptap extension for rendering block mathematical expressions using KaTeX.
65
+ * It allows users to insert LaTeX formatted math expressions block within text.
66
+ * It supports rendering, input rules for LaTeX syntax, and click handling for interaction.
67
+ *
68
+ * @example
69
+ * ```javascript
70
+ * import { BlockMath } from '@tiptap/extension-mathematics'
71
+ * import { Editor } from '@tiptap/core'
72
+ *
73
+ * const editor = new Editor({
74
+ * extensions: [
75
+ * BlockMath.configure({
76
+ * onClick: (node, pos) => {
77
+ * console.log('Block math clicked:', node.attrs.latex, 'at position:', pos)
78
+ * },
79
+ * }),
80
+ * ],
81
+ * })
82
+ */
83
+ export const BlockMath = Node.create<BlockMathOptions>({
84
+ name: 'blockMath',
85
+
86
+ group: 'block',
87
+
88
+ atom: true,
89
+
90
+ addOptions() {
91
+ return {
92
+ onClick: undefined,
93
+ katexOptions: undefined,
94
+ }
95
+ },
96
+
97
+ addAttributes() {
98
+ return {
99
+ latex: {
100
+ default: '',
101
+ parseHTML: element => element.getAttribute('data-latex'),
102
+ renderHTML: attributes => {
103
+ return {
104
+ 'data-latex': attributes.latex,
105
+ }
106
+ },
107
+ },
108
+ }
109
+ },
110
+
111
+ addCommands() {
112
+ return {
113
+ insertBlockMath:
114
+ options =>
115
+ ({ commands, editor }) => {
116
+ const { latex, pos } = options
117
+
118
+ if (!latex) {
119
+ return false
120
+ }
121
+
122
+ return commands.insertContentAt(pos ?? editor.state.selection.from, {
123
+ type: this.name,
124
+ attrs: { latex },
125
+ })
126
+ },
127
+
128
+ deleteBlockMath:
129
+ options =>
130
+ ({ editor, tr }) => {
131
+ const pos = options?.pos ?? editor.state.selection.$from.pos
132
+ const node = editor.state.doc.nodeAt(pos)
133
+
134
+ if (!node || node.type.name !== this.name) {
135
+ return false
136
+ }
137
+
138
+ tr.delete(pos, pos + node.nodeSize)
139
+ return true
140
+ },
141
+
142
+ updateBlockMath:
143
+ options =>
144
+ ({ editor, tr }) => {
145
+ const latex = options?.latex
146
+ let pos = options?.pos
147
+
148
+ if (pos === undefined) {
149
+ pos = editor.state.selection.$from.pos
150
+ }
151
+
152
+ const node = editor.state.doc.nodeAt(pos)
153
+
154
+ if (!node || node.type.name !== this.name) {
155
+ return false
156
+ }
157
+
158
+ tr.setNodeMarkup(pos, this.type, {
159
+ ...node.attrs,
160
+ latex: latex || node.attrs.latex,
161
+ })
162
+
163
+ return true
164
+ },
165
+ }
166
+ },
167
+
168
+ parseHTML() {
169
+ return [
170
+ {
171
+ tag: 'div[data-type="block-math"]',
172
+ },
173
+ ]
174
+ },
175
+
176
+ renderHTML({ HTMLAttributes }) {
177
+ return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'block-math' })]
178
+ },
179
+
180
+ addInputRules() {
181
+ return [
182
+ new InputRule({
183
+ find: /^\$\$\$([^$]+)\$\$\$$/,
184
+ handler: ({ state, range, match }) => {
185
+ const [, latex] = match
186
+ const { tr } = state
187
+ const start = range.from
188
+ const end = range.to
189
+
190
+ tr.replaceWith(start, end, this.type.create({ latex }))
191
+ },
192
+ }),
193
+ ]
194
+ },
195
+
196
+ addNodeView() {
197
+ const { katexOptions } = this.options
198
+
199
+ return ({ node, getPos }) => {
200
+ const wrapper = document.createElement('div')
201
+ const innerWrapper = document.createElement('div')
202
+ wrapper.className = 'tiptap-mathematics-render'
203
+
204
+ if (this.editor.isEditable) {
205
+ wrapper.classList.add('tiptap-mathematics-render--editable')
206
+ }
207
+
208
+ innerWrapper.className = 'block-math-inner'
209
+ wrapper.dataset.type = 'block-math'
210
+ wrapper.setAttribute('data-latex', node.attrs.latex)
211
+ wrapper.appendChild(innerWrapper)
212
+
213
+ function renderMath() {
214
+ try {
215
+ katex.render(node.attrs.latex, innerWrapper, katexOptions)
216
+ wrapper.classList.remove('block-math-error')
217
+ } catch {
218
+ wrapper.textContent = node.attrs.latex
219
+ wrapper.classList.add('block-math-error')
220
+ }
221
+ }
222
+
223
+ const handleClick = (event: MouseEvent) => {
224
+ event.preventDefault()
225
+ event.stopPropagation()
226
+ const pos = getPos()
227
+
228
+ if (pos == null) {
229
+ return
230
+ }
231
+
232
+ if (this.options.onClick) {
233
+ this.options.onClick(node, pos)
234
+ }
235
+ }
236
+
237
+ if (this.options.onClick) {
238
+ wrapper.addEventListener('click', handleClick)
239
+ }
240
+
241
+ renderMath()
242
+
243
+ return {
244
+ dom: wrapper,
245
+ destroy() {
246
+ wrapper.removeEventListener('click', handleClick)
247
+ },
248
+ }
249
+ }
250
+ },
251
+ })
@@ -0,0 +1,252 @@
1
+ import { InputRule, mergeAttributes, Node } from '@tiptap/core'
2
+ import type { Node as PMNode } from '@tiptap/pm/model'
3
+ import katex, { type KatexOptions } from 'katex'
4
+
5
+ /**
6
+ * Configuration options for the InlineMath extension.
7
+ */
8
+ export type InlineMathOptions = {
9
+ /**
10
+ * KaTeX specific options
11
+ * @see https://katex.org/docs/options.html
12
+ * @example
13
+ * ```ts
14
+ * katexOptions: {
15
+ * displayMode: false,
16
+ * throwOnError: false,
17
+ * macros: {
18
+ * '\\RR': '\\mathbb{R}',
19
+ * '\\ZZ': '\\mathbb{Z}'
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ katexOptions?: KatexOptions
25
+
26
+ /**
27
+ * Optional click handler for inline math nodes.
28
+ * Called when a user clicks on an inline math expression in the editor.
29
+ *
30
+ * @param node - The ProseMirror node representing the inline math element
31
+ * @param pos - The position of the node within the document
32
+ * @example
33
+ * ```ts
34
+ * onClick: (node, pos) => {
35
+ * console.log('Inline math clicked:', node.attrs.latex, 'at position:', pos)
36
+ * }
37
+ * ```
38
+ */
39
+ onClick?: (node: PMNode, pos: number) => void
40
+ }
41
+
42
+ declare module '@tiptap/core' {
43
+ interface Commands<ReturnType> {
44
+ inlineMath: {
45
+ /**
46
+ * Insert a inline math node with LaTeX string.
47
+ * @param options - Options for inserting inline math.
48
+ * @returns ReturnType
49
+ */
50
+ insertInlineMath: (options: { latex: string; pos?: number }) => ReturnType
51
+
52
+ /**
53
+ * Delete an inline math node.
54
+ * @returns ReturnType
55
+ */
56
+ deleteInlineMath: (options?: { pos?: number }) => ReturnType
57
+
58
+ /**
59
+ * Update inline math node with optional LaTeX string.
60
+ * @param options - Options for updating inline math.
61
+ * @returns ReturnType
62
+ */
63
+ updateInlineMath: (options?: { latex?: string; pos?: number }) => ReturnType
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * InlineMath is a Tiptap extension for rendering inline mathematical expressions using KaTeX.
70
+ * It allows users to insert LaTeX formatted math expressions inline within text.
71
+ * It supports rendering, input rules for LaTeX syntax, and click handling for interaction.
72
+ *
73
+ * @example
74
+ * ```javascript
75
+ * import { InlineMath } from '@tiptap/extension-mathematics'
76
+ * import { Editor } from '@tiptap/core'
77
+ *
78
+ * const editor = new Editor({
79
+ * extensions: [
80
+ * InlineMath.configure({
81
+ * onClick: (node, pos) => {
82
+ * console.log('Inline math clicked:', node.attrs.latex, 'at position:', pos)
83
+ * },
84
+ * }),
85
+ * ],
86
+ * })
87
+ */
88
+ export const InlineMath = Node.create<InlineMathOptions>({
89
+ name: 'inlineMath',
90
+
91
+ group: 'inline',
92
+
93
+ inline: true,
94
+
95
+ atom: true,
96
+
97
+ addOptions() {
98
+ return {
99
+ onClick: undefined,
100
+ katexOptions: undefined,
101
+ }
102
+ },
103
+
104
+ addAttributes() {
105
+ return {
106
+ latex: {
107
+ default: '',
108
+ parseHTML: element => element.getAttribute('data-latex'),
109
+ renderHTML: attributes => {
110
+ return {
111
+ 'data-latex': attributes.latex,
112
+ }
113
+ },
114
+ },
115
+ }
116
+ },
117
+
118
+ addCommands() {
119
+ return {
120
+ insertInlineMath:
121
+ options =>
122
+ ({ editor, tr }) => {
123
+ const latex = options.latex
124
+
125
+ const from = options?.pos ?? editor.state.selection.from
126
+
127
+ if (!latex) {
128
+ return false
129
+ }
130
+
131
+ tr.replaceWith(from, from, this.type.create({ latex }))
132
+ return true
133
+ },
134
+
135
+ deleteInlineMath:
136
+ options =>
137
+ ({ editor, tr }) => {
138
+ const pos = options?.pos ?? editor.state.selection.$from.pos
139
+ const node = editor.state.doc.nodeAt(pos)
140
+
141
+ if (!node || node.type.name !== this.name) {
142
+ return false
143
+ }
144
+
145
+ tr.delete(pos, pos + node.nodeSize)
146
+ return true
147
+ },
148
+
149
+ updateInlineMath:
150
+ options =>
151
+ ({ editor, tr }) => {
152
+ const latex = options?.latex
153
+ let pos = options?.pos
154
+
155
+ if (pos === undefined) {
156
+ pos = editor.state.selection.$from.pos
157
+ }
158
+
159
+ const node = editor.state.doc.nodeAt(pos)
160
+
161
+ if (!node || node.type.name !== this.name) {
162
+ return false
163
+ }
164
+
165
+ tr.setNodeMarkup(pos, this.type, { ...node.attrs, latex })
166
+
167
+ return true
168
+ },
169
+ }
170
+ },
171
+
172
+ parseHTML() {
173
+ return [
174
+ {
175
+ tag: 'span[data-type="inline-math"]',
176
+ },
177
+ ]
178
+ },
179
+
180
+ renderHTML({ HTMLAttributes }) {
181
+ return ['span', mergeAttributes(HTMLAttributes, { 'data-type': 'inline-math' })]
182
+ },
183
+
184
+ addInputRules() {
185
+ return [
186
+ new InputRule({
187
+ find: /(?<!\$)\$\$([^$\n]+)\$\$(?!\$)$/,
188
+ handler: ({ state, range, match }) => {
189
+ const [, latex] = match
190
+ const { tr } = state
191
+ const start = range.from
192
+ const end = range.to
193
+
194
+ tr.replaceWith(start, end, this.type.create({ latex }))
195
+ },
196
+ }),
197
+ ]
198
+ },
199
+
200
+ addNodeView() {
201
+ const { katexOptions } = this.options
202
+
203
+ return ({ node, getPos }) => {
204
+ const wrapper = document.createElement('span')
205
+ wrapper.className = 'tiptap-mathematics-render'
206
+
207
+ if (this.editor.isEditable) {
208
+ wrapper.classList.add('tiptap-mathematics-render--editable')
209
+ }
210
+
211
+ wrapper.dataset.type = 'inline-math'
212
+ wrapper.setAttribute('data-latex', node.attrs.latex)
213
+
214
+ function renderMath() {
215
+ try {
216
+ katex.render(node.attrs.latex, wrapper, katexOptions)
217
+ wrapper.classList.remove('inline-math-error')
218
+ } catch {
219
+ wrapper.textContent = node.attrs.latex
220
+ wrapper.classList.add('inline-math-error')
221
+ }
222
+ }
223
+
224
+ const handleClick = (event: MouseEvent) => {
225
+ event.preventDefault()
226
+ event.stopPropagation()
227
+ const pos = getPos()
228
+
229
+ if (pos == null) {
230
+ return
231
+ }
232
+
233
+ if (this.options.onClick) {
234
+ this.options.onClick(node, pos)
235
+ }
236
+ }
237
+
238
+ if (this.options.onClick) {
239
+ wrapper.addEventListener('click', handleClick)
240
+ }
241
+
242
+ renderMath()
243
+
244
+ return {
245
+ dom: wrapper,
246
+ destroy() {
247
+ wrapper.removeEventListener('click', handleClick)
248
+ },
249
+ }
250
+ }
251
+ },
252
+ })
@@ -0,0 +1,2 @@
1
+ export * from './BlockMath.js'
2
+ export * from './InlineMath.js'
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Mathematics } from './mathematics.js'
2
2
 
3
+ export * from './extensions/index.js'
3
4
  export * from './mathematics.js'
4
- export * from './MathematicsPlugin.js'
5
5
  export * from './types.js'
6
+ export * from './utils.js'
6
7
 
7
8
  export default Mathematics
@@ -1,30 +1,70 @@
1
1
  import { Extension } from '@tiptap/core'
2
- import type { EditorState } from '@tiptap/pm/state'
3
2
 
4
- import { MathematicsPlugin } from './MathematicsPlugin.js'
3
+ import { BlockMath, InlineMath } from './extensions/index.js'
5
4
  import type { MathematicsOptions } from './types.js'
6
5
 
7
- export const defaultShouldRender = (state: EditorState, pos: number) => {
8
- const $pos = state.doc.resolve(pos)
9
- const isInCodeBlock = $pos.parent.type.name === 'codeBlock'
10
-
11
- return !isInCodeBlock
12
- }
13
-
6
+ /**
7
+ * Mathematics extension for Tiptap that provides both inline and block math support using KaTeX.
8
+ * This extension combines the InlineMath and BlockMath extensions to provide a complete
9
+ * mathematical expression solution for rich text editing. It supports LaTeX syntax,
10
+ * custom rendering options, and interactive math nodes.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { Editor } from '@tiptap/core'
15
+ * import { Mathematics } from '@tiptap/extension-mathematics'
16
+ * import { migrateMathStrings } from '@tiptap/extension-mathematics/utils'
17
+ *
18
+ * const editor = new Editor({
19
+ * extensions: [
20
+ * Mathematics.configure({
21
+ * inlineOptions: {
22
+ * onClick: (node, pos) => {
23
+ * console.log('Inline math clicked:', node.attrs.latex)
24
+ * }
25
+ * },
26
+ * blockOptions: {
27
+ * onClick: (node, pos) => {
28
+ * console.log('Block math clicked:', node.attrs.latex)
29
+ * }
30
+ * },
31
+ * katexOptions: {
32
+ * displayMode: false,
33
+ * throwOnError: false,
34
+ * macros: {
35
+ * '\\RR': '\\mathbb{R}',
36
+ * '\\ZZ': '\\mathbb{Z}'
37
+ * }
38
+ * }
39
+ * })
40
+ * ],
41
+ * content: `
42
+ * <p>Inline math: $E = mc^2$</p>
43
+ * <div data-type="block-math" data-latex="\\sum_{i=1}^{n} x_i = X"></div>
44
+ * `,
45
+ * onCreate({ editor }) {
46
+ * // Optional: Migrate existing math strings to math nodes
47
+ * migrateMathStrings(editor)
48
+ * }
49
+ * })
50
+ * ```
51
+ */
14
52
  export const Mathematics = Extension.create<MathematicsOptions>({
15
53
  name: 'Mathematics',
16
54
 
17
55
  addOptions() {
18
56
  return {
19
- // eslint-disable-next-line no-useless-escape
20
- regex: /\$([^\$]*)\$/gi,
57
+ inlineOptions: undefined,
58
+ blockOptions: undefined,
21
59
  katexOptions: undefined,
22
- shouldRender: defaultShouldRender,
23
60
  }
24
61
  },
25
62
 
26
- addProseMirrorPlugins() {
27
- return [MathematicsPlugin({ ...this.options, editor: this.editor })]
63
+ addExtensions() {
64
+ return [
65
+ BlockMath.configure({ ...this.options.blockOptions, katexOptions: this.options.katexOptions }),
66
+ InlineMath.configure({ ...this.options.inlineOptions, katexOptions: this.options.katexOptions }),
67
+ ]
28
68
  },
29
69
  })
30
70
 
package/src/types.ts CHANGED
@@ -1,12 +1,24 @@
1
1
  import type { Editor } from '@tiptap/core'
2
- import type { Node } from '@tiptap/pm/model'
3
- import type { EditorState } from '@tiptap/pm/state'
4
2
  import type { KatexOptions } from 'katex'
5
3
 
4
+ import type { BlockMathOptions, InlineMathOptions } from './extensions'
5
+
6
+ /**
7
+ * Configuration options for the Mathematics extension.
8
+ * This type defines the available customization options for both inline and block math rendering.
9
+ */
6
10
  export type MathematicsOptions = {
7
- regex: RegExp
11
+ /** Configuration options specific to inline math nodes */
12
+ inlineOptions?: Omit<InlineMathOptions, 'katexOptions'>
13
+ /** Configuration options specific to block math nodes */
14
+ blockOptions?: Omit<BlockMathOptions, 'katexOptions'>
15
+ /** KaTeX-specific rendering options passed to the KaTeX library */
8
16
  katexOptions?: KatexOptions
9
- shouldRender: (state: EditorState, pos: number, node: Node) => boolean
10
17
  }
11
18
 
19
+ /**
20
+ * Extended mathematics options that include an editor instance.
21
+ * This type combines the base mathematics options with an editor reference,
22
+ * typically used internally by the extension for operations that require editor access.
23
+ */
12
24
  export type MathematicsOptionsWithEditor = MathematicsOptions & { editor: Editor }