@tiptap/extension-mathematics 3.0.0-beta.15 → 3.0.0-beta.17

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.
@@ -1,205 +0,0 @@
1
- import { getChangedRanges } from '@tiptap/core'
2
- import type { EditorState, Transaction } from '@tiptap/pm/state'
3
- import { Plugin, PluginKey } from '@tiptap/pm/state'
4
- import { Decoration, DecorationSet } from '@tiptap/pm/view'
5
- import katex from 'katex'
6
-
7
- import type { MathematicsOptionsWithEditor } from './types.js'
8
-
9
- type DecoSpec = {
10
- isEditable: boolean
11
- isEditing: boolean
12
- katexOptions: MathematicsOptionsWithEditor['katexOptions']
13
- content: string
14
- }
15
-
16
- type Deco = Omit<Decoration, 'spec'> & { spec: DecoSpec }
17
-
18
- type PluginState =
19
- | { decorations: DecorationSet; isEditable: boolean }
20
- | { decorations: undefined; isEditable: undefined }
21
-
22
- /**
23
- * Get the range of positions that have been affected by a transaction
24
- */
25
- function getAffectedRange(
26
- newState: EditorState,
27
- previousPluginState: PluginState,
28
- isEditable: boolean,
29
- tr: Transaction,
30
- state: EditorState,
31
- ) {
32
- const docSize = newState.doc.nodeSize - 2
33
- let minFrom = 0
34
- let maxTo = docSize
35
-
36
- if (previousPluginState.isEditable !== isEditable) {
37
- // When the editable state changes, run on all nodes just to be safe
38
- minFrom = 0
39
- maxTo = docSize
40
- } else if (tr.docChanged) {
41
- // When the document changes, only run on the nodes that have changed
42
- minFrom = docSize
43
- maxTo = 0
44
-
45
- getChangedRanges(tr).forEach(range => {
46
- // Purposefully over scan the range to ensure we catch all decorations
47
- minFrom = Math.min(minFrom, range.newRange.from - 1, range.oldRange.from - 1)
48
- maxTo = Math.max(maxTo, range.newRange.to + 1, range.oldRange.to + 1)
49
- })
50
- } else if (tr.selectionSet) {
51
- const { $from, $to } = state.selection
52
- const { $from: $newFrom, $to: $newTo } = newState.selection
53
-
54
- // When the selection changes, run on all the nodes between the old and new selection
55
- minFrom = Math.min(
56
- // Purposefully over scan the range to ensure we catch all decorations
57
- $from.depth === 0 ? 0 : $from.before(),
58
- $newFrom.depth === 0 ? 0 : $newFrom.before(),
59
- )
60
- maxTo = Math.max($to.depth === 0 ? maxTo : $to.after(), $newTo.depth === 0 ? maxTo : $newTo.after())
61
- }
62
-
63
- return {
64
- minFrom: Math.max(minFrom, 0),
65
- maxTo: Math.min(maxTo, docSize),
66
- }
67
- }
68
-
69
- export const MathematicsPlugin = (options: MathematicsOptionsWithEditor) => {
70
- const { regex, katexOptions = {}, editor, shouldRender } = options
71
-
72
- return new Plugin<PluginState>({
73
- key: new PluginKey('mathematics'),
74
-
75
- state: {
76
- init() {
77
- return { decorations: undefined, isEditable: undefined }
78
- },
79
- apply(tr, previousPluginState, state, newState) {
80
- if (!tr.docChanged && !tr.selectionSet && previousPluginState.decorations) {
81
- // Just reuse the existing decorations, since nothing should have changed
82
- return previousPluginState
83
- }
84
-
85
- const nextDecorationSet = (previousPluginState.decorations || DecorationSet.empty).map(tr.mapping, tr.doc)
86
- const { selection } = newState
87
- const isEditable = editor.isEditable
88
- const decorationsToAdd = [] as Deco[]
89
- const { minFrom, maxTo } = getAffectedRange(newState, previousPluginState, isEditable, tr, state)
90
-
91
- newState.doc.nodesBetween(minFrom, maxTo, (node, pos) => {
92
- const enabled = shouldRender(newState, pos, node)
93
-
94
- if (node.isText && node.text && enabled) {
95
- let match: RegExpExecArray | null
96
-
97
- // eslint-disable-next-line no-cond-assign
98
- while ((match = regex.exec(node.text))) {
99
- const from = pos + match.index
100
- const to = from + match[0].length
101
- const content = match.slice(1).find(Boolean)
102
-
103
- if (content) {
104
- const selectionSize = selection.from - selection.to
105
- const anchorIsInside = selection.anchor >= from && selection.anchor <= to
106
- const rangeIsInside = selection.from >= from && selection.to <= to
107
- const isEditing = (selectionSize === 0 && anchorIsInside) || rangeIsInside
108
-
109
- if (
110
- // Are the decorations already present?
111
- nextDecorationSet.find(
112
- from,
113
- to,
114
- (deco: DecoSpec) =>
115
- isEditing === deco.isEditing &&
116
- content === deco.content &&
117
- isEditable === deco.isEditable &&
118
- katexOptions === deco.katexOptions,
119
- ).length
120
- ) {
121
- // Decoration exists in set, no need to add it again
122
- continue
123
- }
124
- // Use an inline decoration to either hide original (preview is showing) or show it (editing "mode")
125
- decorationsToAdd.push(
126
- Decoration.inline(
127
- from,
128
- to,
129
- {
130
- class:
131
- isEditing && isEditable
132
- ? 'Tiptap-mathematics-editor'
133
- : 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',
134
- style:
135
- !isEditing || !isEditable
136
- ? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'
137
- : undefined,
138
- },
139
- {
140
- content,
141
- isEditable,
142
- isEditing,
143
- katexOptions,
144
- } satisfies DecoSpec,
145
- ),
146
- )
147
-
148
- if (!isEditable || !isEditing) {
149
- // Create decoration widget and add KaTeX preview if selection is not within the math-editor
150
- decorationsToAdd.push(
151
- Decoration.widget(
152
- from,
153
- () => {
154
- const element = document.createElement('span')
155
-
156
- // TODO: changeable class names
157
- element.classList.add('Tiptap-mathematics-render')
158
-
159
- if (isEditable) {
160
- element.classList.add('Tiptap-mathematics-render--editable')
161
- }
162
-
163
- try {
164
- katex.render(content!, element, katexOptions)
165
- } catch {
166
- element.innerHTML = content!
167
- }
168
-
169
- return element
170
- },
171
- {
172
- content,
173
- isEditable,
174
- isEditing,
175
- katexOptions,
176
- } satisfies DecoSpec,
177
- ),
178
- )
179
- }
180
- }
181
- }
182
- }
183
- })
184
-
185
- // Remove any decorations that exist at the same position, they will be replaced by the new decorations
186
- const decorationsToRemove = decorationsToAdd.flatMap(deco => nextDecorationSet.find(deco.from, deco.to))
187
-
188
- return {
189
- decorations: nextDecorationSet
190
- // Remove existing decorations that are going to be replaced
191
- .remove(decorationsToRemove)
192
- // Add any new decorations
193
- .add(tr.doc, decorationsToAdd),
194
- isEditable,
195
- }
196
- },
197
- },
198
-
199
- props: {
200
- decorations(state) {
201
- return this.getState(state)?.decorations ?? DecorationSet.empty
202
- },
203
- },
204
- })
205
- }