@wiris/mathtype-ckeditor5 8.15.0 → 8.15.2
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/README.md +1 -0
- package/dist/browser/index-editor.css +16 -1
- package/dist/browser/index.css +17 -0
- package/dist/browser/index.css.map +1 -1
- package/dist/browser/index.js +511 -100
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.umd.js +511 -100
- package/dist/browser/index.umd.js.map +1 -1
- package/dist/index-editor.css +15 -0
- package/dist/index.css +18 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +467 -100
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/integration.js +469 -114
- package/src/plugin.js +133 -7
- package/theme/styles.css +28 -0
package/dist/index.js
CHANGED
|
@@ -132,49 +132,68 @@ import Telemeter from '@wiris/mathtype-html-integration-devkit/src/telemeter.js'
|
|
|
132
132
|
* @param {String} mathml MathML to update old one or insert
|
|
133
133
|
* @returns {module:engine/model/element~Element} The model element corresponding to the inserted image
|
|
134
134
|
*/ insertMathml(mathml) {
|
|
135
|
-
// This returns the value returned by the callback function (writer => {...})
|
|
136
135
|
return this.editorObject.model.change((writer)=>{
|
|
137
|
-
const
|
|
136
|
+
const { isNewElement, temporalImage } = this.getCore().editionProperties;
|
|
138
137
|
const selection = this.editorObject.model.document.selection;
|
|
138
|
+
const attributes = Object.fromEntries(selection.getAttributes());
|
|
139
139
|
const modelElementNew = writer.createElement("mathml", {
|
|
140
140
|
formula: mathml,
|
|
141
|
-
...
|
|
141
|
+
...attributes
|
|
142
142
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Don't bother inserting anything at all if the MathML is empty.
|
|
146
|
-
if (!mathml) return;
|
|
147
|
-
const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
|
|
148
|
-
const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
|
|
149
|
-
this.editorObject.model.insertObject(modelElementNew, modelPosition);
|
|
150
|
-
// Remove selection
|
|
151
|
-
if (!viewSelection.isCollapsed) {
|
|
152
|
-
for (const range of viewSelection.getRanges()){
|
|
153
|
-
const modelRange = this.editorObject.editing.mapper.toModelRange(range);
|
|
154
|
-
const modelSelection = this.editorObject.model.createSelection(modelRange);
|
|
155
|
-
this.editorObject.model.deleteContent(modelSelection);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// Set carret after the formula
|
|
159
|
-
const position = this.editorObject.model.createPositionAfter(modelElementNew);
|
|
160
|
-
writer.setSelection(position);
|
|
161
|
-
} else {
|
|
162
|
-
const img = core.editionProperties.temporalImage;
|
|
163
|
-
const viewElement = this.editorObject.editing.view.domConverter.domToView(img).parent;
|
|
164
|
-
const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewElement);
|
|
165
|
-
// Insert the new <mathml> and remove the old one
|
|
166
|
-
const position = this.editorObject.model.createPositionBefore(modelElementOld);
|
|
167
|
-
// If the given MathML is empty, don't insert a new formula.
|
|
168
|
-
if (mathml) {
|
|
169
|
-
this.editorObject.model.insertObject(modelElementNew, position);
|
|
170
|
-
}
|
|
171
|
-
this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, 'on'));
|
|
143
|
+
if (isNewElement) {
|
|
144
|
+
return this.insertNewFormula(writer, mathml, modelElementNew);
|
|
172
145
|
}
|
|
173
|
-
|
|
174
|
-
return modelElementNew;
|
|
146
|
+
return this.replaceExistingFormula(mathml, modelElementNew, temporalImage);
|
|
175
147
|
});
|
|
176
148
|
}
|
|
177
149
|
/**
|
|
150
|
+
* Inserts a new formula at the current selection position.
|
|
151
|
+
*/ insertNewFormula(writer, mathml, modelElement) {
|
|
152
|
+
if (!mathml) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
|
|
156
|
+
const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
|
|
157
|
+
this.editorObject.model.insertObject(modelElement, modelPosition);
|
|
158
|
+
this.deleteViewSelection(viewSelection);
|
|
159
|
+
// Set carret after the formula.
|
|
160
|
+
const position = this.editorObject.model.createPositionAfter(modelElement);
|
|
161
|
+
writer.setSelection(position);
|
|
162
|
+
return modelElement;
|
|
163
|
+
}
|
|
164
|
+
deleteViewSelection(viewSelection) {
|
|
165
|
+
if (viewSelection.isCollapsed) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const range of viewSelection.getRanges()){
|
|
169
|
+
const modelRange = this.editorObject.editing.mapper.toModelRange(range);
|
|
170
|
+
const modelSelection = this.editorObject.model.createSelection(modelRange);
|
|
171
|
+
this.editorObject.model.deleteContent(modelSelection);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Replaces an existing formula with updated MathML.
|
|
176
|
+
*/ replaceExistingFormula(mathml, modelElement, temporalImage) {
|
|
177
|
+
const viewNode = this.editorObject.editing.view.domConverter.domToView(temporalImage);
|
|
178
|
+
// Check if image exists in view to do standard formula editing
|
|
179
|
+
if (viewNode?.parent) {
|
|
180
|
+
const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewNode.parent);
|
|
181
|
+
// Insert the new <mathml> and remove the old one
|
|
182
|
+
const position = this.editorObject.model.createPositionBefore(modelElementOld);
|
|
183
|
+
if (mathml) {
|
|
184
|
+
this.editorObject.model.insertObject(modelElement, position);
|
|
185
|
+
}
|
|
186
|
+
this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, "on"));
|
|
187
|
+
return modelElement;
|
|
188
|
+
}
|
|
189
|
+
// Otherwise it's LaTeX editing, so we insert at current selection
|
|
190
|
+
if (!mathml) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.editorObject.model.insertContent(modelElement);
|
|
194
|
+
return modelElement;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
178
197
|
* Finds the text node corresponding to given DOM text element.
|
|
179
198
|
* @param {element} viewElement Element to find corresponding text node of.
|
|
180
199
|
* @returns {module:engine/model/text~Text|undefined} Text node corresponding to the given element or undefined if it doesn't exist.
|
|
@@ -201,73 +220,26 @@ import Telemeter from '@wiris/mathtype-html-integration-devkit/src/telemeter.js'
|
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
|
-
/** @inheritdoc */ insertFormula(
|
|
223
|
+
/** @inheritdoc */ insertFormula(_focusElement, windowTarget, mathml, _wirisProperties) {
|
|
205
224
|
// eslint-disable-line no-unused-vars
|
|
206
225
|
const returnObject = {};
|
|
207
226
|
let mathmlOrigin;
|
|
208
227
|
if (!mathml) {
|
|
209
228
|
this.insertMathml("");
|
|
210
229
|
} else if (this.core.editMode === "latex") {
|
|
211
|
-
returnObject
|
|
212
|
-
returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
|
|
213
|
-
this.editorObject.model.change((writer)=>{
|
|
214
|
-
const { latexRange } = this.core.editionProperties;
|
|
215
|
-
const startNode = this.findText(latexRange.startContainer);
|
|
216
|
-
const endNode = this.findText(latexRange.endContainer);
|
|
217
|
-
let startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexRange.startOffset);
|
|
218
|
-
let endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + latexRange.endOffset);
|
|
219
|
-
let range = writer.createRange(startPosition, endPosition);
|
|
220
|
-
// When Latex is next to image/formula.
|
|
221
|
-
if (latexRange.startContainer.nodeType === 3 && latexRange.startContainer.previousSibling?.nodeType === 1) {
|
|
222
|
-
// Get the position of the latex to be replaced.
|
|
223
|
-
const latexEdited = `$$${Latex.getLatexFromMathML(MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml))}$$`;
|
|
224
|
-
let data = latexRange.startContainer.data;
|
|
225
|
-
// Remove invisible characters.
|
|
226
|
-
data = data.replaceAll(String.fromCharCode(8288), "");
|
|
227
|
-
// Get to the start of the latex we are editing.
|
|
228
|
-
const offset = data.indexOf(latexEdited);
|
|
229
|
-
const dataOffset = data.substring(offset);
|
|
230
|
-
const second$ = dataOffset.substring(2).indexOf("$$") + 4;
|
|
231
|
-
const substring = dataOffset.substr(0, second$);
|
|
232
|
-
data = data.replace(substring, "");
|
|
233
|
-
if (!data) {
|
|
234
|
-
startPosition = writer.createPositionBefore(startNode);
|
|
235
|
-
range = startNode;
|
|
236
|
-
} else {
|
|
237
|
-
startPosition = startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + offset);
|
|
238
|
-
endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + second$ + offset);
|
|
239
|
-
range = writer.createRange(startPosition, endPosition);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const modelSelection = this.editorObject.model.createSelection(range);
|
|
243
|
-
this.editorObject.model.deleteContent(modelSelection);
|
|
244
|
-
writer.insertText(`$$${returnObject.latex}$$`, startNode.getAttributes(), startPosition);
|
|
245
|
-
});
|
|
230
|
+
this.handleLatexInsertion(returnObject, windowTarget, mathml);
|
|
246
231
|
} else {
|
|
247
|
-
mathmlOrigin = this.
|
|
248
|
-
try {
|
|
249
|
-
returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(this.editorObject.editing.mapper.toViewElement(this.insertMathml(mathml)), windowTarget.document);
|
|
250
|
-
} catch (e) {
|
|
251
|
-
const x = e.toString();
|
|
252
|
-
if (x.includes("CKEditorError: Cannot read property 'parent' of undefined")) {
|
|
253
|
-
this.core.modalDialog.cancelAction();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
232
|
+
mathmlOrigin = this.handleMathmlInsertion(returnObject, windowTarget, mathml);
|
|
256
233
|
}
|
|
257
|
-
// Build the telemeter payload separated to delete null/undefined entries.
|
|
258
234
|
const payload = {
|
|
259
|
-
|
|
260
|
-
mathml: mathml ? MathML.safeXmlDecode(mathml) : mathml,
|
|
235
|
+
mathml: mathml ? MathML.safeXmlDecode(mathml) : undefined,
|
|
261
236
|
elapsed_time: Date.now() - this.core.editionProperties.editionStartTime,
|
|
262
|
-
editor_origin: null,
|
|
263
237
|
toolbar: this.core.modalDialog.contentManager.toolbar,
|
|
264
238
|
size: mathml?.length
|
|
265
239
|
};
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
});
|
|
270
|
-
// Call Telemetry service to track the event.
|
|
240
|
+
if (mathmlOrigin) {
|
|
241
|
+
payload.mathml_origin = MathML.safeXmlDecode(mathmlOrigin);
|
|
242
|
+
}
|
|
271
243
|
try {
|
|
272
244
|
Telemeter.telemeter.track("INSERTED_FORMULA", {
|
|
273
245
|
...payload
|
|
@@ -275,15 +247,312 @@ import Telemeter from '@wiris/mathtype-html-integration-devkit/src/telemeter.js'
|
|
|
275
247
|
} catch (error) {
|
|
276
248
|
console.error("Error tracking INSERTED_FORMULA", error);
|
|
277
249
|
}
|
|
278
|
-
/* Due to PLUGINS-1329, we add the onChange event to the CK4 insertFormula.
|
|
279
|
-
We probably should add it here as well, but we should look further into how */ // this.editorObject.fire('change');
|
|
280
|
-
// Remove temporal image of inserted formula
|
|
281
250
|
this.core.editionProperties.temporalImage = null;
|
|
282
251
|
return returnObject;
|
|
283
252
|
}
|
|
253
|
+
handleLatexInsertion(returnObject, windowTarget, mathml) {
|
|
254
|
+
returnObject.latex = Latex.getLatexFromMathML(mathml);
|
|
255
|
+
returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
|
|
256
|
+
const { latexRange } = this.core.editionProperties;
|
|
257
|
+
// When latexRange exists (meaning the whole LaTeX was selected or the editor was opened),
|
|
258
|
+
// find the node contaning the LaTeX and replace it fully.
|
|
259
|
+
if (latexRange) {
|
|
260
|
+
const startNode = this.findText(latexRange.startContainer);
|
|
261
|
+
const endNode = this.findText(latexRange.endContainer);
|
|
262
|
+
// If nodes found, use standard replacement.
|
|
263
|
+
if (startNode && endNode) {
|
|
264
|
+
this.replaceLatexWithNodes(startNode, endNode, latexRange, returnObject.latex);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
this.replaceLatexUsingModelSearch(returnObject.latex);
|
|
269
|
+
}
|
|
270
|
+
handleMathmlInsertion(returnObject, windowTarget, mathml) {
|
|
271
|
+
const mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml;
|
|
272
|
+
try {
|
|
273
|
+
const modelElement = this.insertMathml(mathml);
|
|
274
|
+
const viewElement = this.editorObject.editing.mapper.toViewElement(modelElement);
|
|
275
|
+
returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(viewElement, windowTarget.document);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error.toString().includes("Cannot read property 'parent' of undefined")) {
|
|
278
|
+
this.core.modalDialog.cancelAction();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return mathmlOrigin;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Gets selection attributes excluding track changes tags.
|
|
285
|
+
*/ getCleanSelectionAttributes() {
|
|
286
|
+
const attributes = {};
|
|
287
|
+
for (const [key, value] of this.editorObject.model.document.selection.getAttributes()){
|
|
288
|
+
if (!key.startsWith("suggestion:") && !key.startsWith("comment:")) {
|
|
289
|
+
attributes[key] = value;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return attributes;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Searches for the original LaTeX in the model and replaces it.
|
|
296
|
+
* Fallback when findText() cannot locate DOM nodes (like when there are track changes modifications).
|
|
297
|
+
*/ replaceLatexUsingModelSearch(newLatex) {
|
|
298
|
+
const foundRange = this.findLatexBlockNearSelection();
|
|
299
|
+
if (foundRange) {
|
|
300
|
+
this.editorObject.model.change((writer)=>writer.setSelection(foundRange));
|
|
301
|
+
this.replaceRangeWithLatex(newLatex);
|
|
302
|
+
} else {
|
|
303
|
+
// Insert at current position as a last resort.
|
|
304
|
+
this.editorObject.model.change((writer)=>{
|
|
305
|
+
const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
|
|
306
|
+
this.editorObject.model.insertContent(newLatexText);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
this.core.editionProperties.extractedLatex = null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Checks if a text proxy has a track changes deletion marker.
|
|
313
|
+
*/ isDeletedText(text) {
|
|
314
|
+
for (const [key, value] of text.getAttributes()){
|
|
315
|
+
if (key.startsWith("suggestion:") && value === "deletion") {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Finds a LaTeX block ($$...$$) near the current selection.
|
|
323
|
+
* Handles track changes by considering the "accepted" version of text.
|
|
324
|
+
*/ findLatexBlockNearSelection() {
|
|
325
|
+
const position = this.editorObject.model.document.selection.getFirstPosition();
|
|
326
|
+
if (!position?.parent) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Build LaTeX with track changes accepted suggestions, if any.
|
|
330
|
+
const { textParts, acceptedText } = this.collectTextParts(position.parent);
|
|
331
|
+
if (!acceptedText.includes("$$")) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// To handle multiple LaTeX on same line.
|
|
335
|
+
const targetLatex = this.core.editionProperties.extractedLatex;
|
|
336
|
+
const fullLatex = `$$${targetLatex}$$`;
|
|
337
|
+
const startIndex = acceptedText.indexOf(fullLatex);
|
|
338
|
+
if (startIndex === -1) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const latexBoundaries = {
|
|
342
|
+
start: startIndex,
|
|
343
|
+
end: startIndex + fullLatex.length
|
|
344
|
+
};
|
|
345
|
+
return this.convertAcceptedOffsetsToModelRange(textParts, latexBoundaries);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Collects all text fragments from a paragraph, tracking both model and accepted text positions.
|
|
349
|
+
* This is necessary to handle track changes where some LaTeX may have suggestions.
|
|
350
|
+
*/ collectTextParts(paragraph) {
|
|
351
|
+
const textParts = [];
|
|
352
|
+
let acceptedTextOffset = 0;
|
|
353
|
+
let acceptedText = "";
|
|
354
|
+
for (const item of this.editorObject.model.createRangeIn(paragraph).getItems()){
|
|
355
|
+
if (item.is("$textProxy")) {
|
|
356
|
+
const isDeleted = this.isDeletedText(item);
|
|
357
|
+
textParts.push({
|
|
358
|
+
text: item.data,
|
|
359
|
+
startOffset: item.startOffset,
|
|
360
|
+
endOffset: item.startOffset + item.data.length,
|
|
361
|
+
parent: item.textNode.parent,
|
|
362
|
+
acceptedStart: isDeleted ? null : acceptedTextOffset,
|
|
363
|
+
acceptedEnd: isDeleted ? null : acceptedTextOffset + item.data.length,
|
|
364
|
+
isDeleted
|
|
365
|
+
});
|
|
366
|
+
if (!isDeleted) {
|
|
367
|
+
acceptedText += item.data;
|
|
368
|
+
acceptedTextOffset += item.data.length;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
textParts,
|
|
374
|
+
acceptedText
|
|
375
|
+
};
|
|
376
|
+
}
|
|
284
377
|
/**
|
|
285
|
-
*
|
|
286
|
-
*/
|
|
378
|
+
* Converts LaTeX with track changes accepted suggestions to a CKEditor model Range.
|
|
379
|
+
*/ convertAcceptedOffsetsToModelRange(textParts, latexBoundaries) {
|
|
380
|
+
let startPartIndex = -1, endPartIndex = -1;
|
|
381
|
+
let startOffsetInPart = 0, endOffsetInPart = 0;
|
|
382
|
+
// Find which text parts contain the LaTeX block boundaries
|
|
383
|
+
for(let i = 0; i < textParts.length; i++){
|
|
384
|
+
const part = textParts[i];
|
|
385
|
+
if (part.isDeleted) continue;
|
|
386
|
+
if (startPartIndex === -1 && latexBoundaries.start >= part.acceptedStart && latexBoundaries.start <= part.acceptedEnd) {
|
|
387
|
+
startPartIndex = i;
|
|
388
|
+
startOffsetInPart = latexBoundaries.start - part.acceptedStart;
|
|
389
|
+
}
|
|
390
|
+
if (latexBoundaries.end >= part.acceptedStart && latexBoundaries.end <= part.acceptedEnd) {
|
|
391
|
+
endPartIndex = i;
|
|
392
|
+
endOffsetInPart = latexBoundaries.end - part.acceptedStart;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (startPartIndex === -1 || endPartIndex === -1) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// Extend range to include any consecutive deleted parts after the block.
|
|
399
|
+
let finalEndIndex = endPartIndex;
|
|
400
|
+
let finalEndOffset = endOffsetInPart;
|
|
401
|
+
for(let i = endPartIndex + 1; i < textParts.length && textParts[i].isDeleted; i++){
|
|
402
|
+
finalEndIndex = i;
|
|
403
|
+
finalEndOffset = textParts[i].text.length;
|
|
404
|
+
}
|
|
405
|
+
const startPart = textParts[startPartIndex];
|
|
406
|
+
const endPart = textParts[finalEndIndex];
|
|
407
|
+
return this.editorObject.model.createRange(this.editorObject.model.createPositionAt(startPart.parent, startPart.startOffset + startOffsetInPart), this.editorObject.model.createPositionAt(endPart.parent, endPart.startOffset + finalEndOffset));
|
|
408
|
+
}
|
|
409
|
+
replaceRangeWithLatex(newLatex) {
|
|
410
|
+
this.editorObject.model.change((writer)=>{
|
|
411
|
+
this.editorObject.model.deleteContent(this.editorObject.model.document.selection);
|
|
412
|
+
const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
|
|
413
|
+
this.editorObject.model.insertContent(newLatexText);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Replaces the whole LaTeX in the CKEditor5 model.
|
|
418
|
+
*/ replaceLatexWithNodes(startNode, endNode, latexRange, newLatex) {
|
|
419
|
+
this.editorObject.model.change((writer)=>{
|
|
420
|
+
const startOffset = startNode.startOffset + latexRange.startOffset;
|
|
421
|
+
const endOffset = endNode.startOffset + latexRange.endOffset;
|
|
422
|
+
let startPosition = writer.createPositionAt(startNode.parent, startOffset);
|
|
423
|
+
let endPosition = writer.createPositionAt(endNode.parent, endOffset);
|
|
424
|
+
// Adjust positions when LaTeX is adjacent to a formula.
|
|
425
|
+
const startContainer = latexRange.startContainer;
|
|
426
|
+
if (startContainer.nodeType === Node.TEXT_NODE && startContainer.previousSibling?.nodeType === Node.ELEMENT_NODE) {
|
|
427
|
+
const originalLatex = `$$${Latex.getLatexFromMathML(MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml))}$$`;
|
|
428
|
+
const textData = startContainer.data.replaceAll(String.fromCodePoint(8288), "");
|
|
429
|
+
const latexOffset = textData.indexOf(originalLatex);
|
|
430
|
+
if (latexOffset !== -1) {
|
|
431
|
+
const closingDelimiterOffset = textData.substring(latexOffset + 2).indexOf("$$") + 4;
|
|
432
|
+
startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexOffset);
|
|
433
|
+
endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + closingDelimiterOffset + latexOffset);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
writer.setSelection(writer.createRange(startPosition, endPosition));
|
|
437
|
+
});
|
|
438
|
+
this.replaceRangeWithLatex(newLatex);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Inherited method from IntegrationModel.
|
|
442
|
+
* Gets the MathML from a text node containing LaTeX.
|
|
443
|
+
* Handles track changes by simulating "accept all changes" before conversion.
|
|
444
|
+
*/ getMathmlFromTextNode(textNode, caretPosition) {
|
|
445
|
+
const standardResult = Latex.getLatexFromTextNode(textNode, caretPosition);
|
|
446
|
+
const acceptedLatex = this.extractAcceptedLatexFromDOM(textNode, caretPosition);
|
|
447
|
+
// Prioritize accepted LaTeX if it differs from standard extraction (for track changes compatibility).
|
|
448
|
+
// Important node: use explicit undefined check to allow empty LaTeX strings, otherwise it would not detect $$$$ as valid LaTeX.
|
|
449
|
+
const latex = acceptedLatex !== undefined && acceptedLatex !== standardResult?.latex ? acceptedLatex : standardResult?.latex;
|
|
450
|
+
// Do not continue if no LaTeX found by either method.
|
|
451
|
+
// This is necessary since both parameters can be independently undefined in some edge cases.
|
|
452
|
+
if (latex === undefined && acceptedLatex === undefined) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Verify caret is inside LaTeX block for track changes edge cases.
|
|
456
|
+
if (!standardResult && acceptedLatex !== undefined && !this.isCaretInsideLatexBlock(textNode, caretPosition)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const finalLatex = latex === undefined ? acceptedLatex : latex;
|
|
460
|
+
this.storeLatexRangeWithFallback(textNode, caretPosition, finalLatex);
|
|
461
|
+
return Latex.getMathMLFromLatex(finalLatex);
|
|
462
|
+
}
|
|
463
|
+
isCaretInsideLatexBlock(textNode, caretPosition = 0) {
|
|
464
|
+
// If LaTeX is found, the caret is inside one.
|
|
465
|
+
return this.extractAcceptedLatexFromDOM(textNode, caretPosition) !== undefined;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Stores the LaTeX range for its replacement later.
|
|
469
|
+
*/ storeLatexRangeWithFallback(textNode, caretPosition, latex) {
|
|
470
|
+
const parentTag = textNode.parentElement?.tagName?.toLowerCase();
|
|
471
|
+
if (!textNode.parentElement || parentTag === "textarea") {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const latexResult = Latex.getLatexFromTextNode(textNode, caretPosition);
|
|
475
|
+
if (latexResult) {
|
|
476
|
+
const range = document.createRange();
|
|
477
|
+
range.setStart(latexResult.startNode, latexResult.startPosition);
|
|
478
|
+
range.setEnd(latexResult.endNode, latexResult.endPosition);
|
|
479
|
+
this.core.editionProperties.latexRange = range;
|
|
480
|
+
} else {
|
|
481
|
+
this.core.editionProperties.latexRange = null;
|
|
482
|
+
}
|
|
483
|
+
this.core.editionProperties.extractedLatex = latex;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Finds a container element containing a complete LaTeX block.
|
|
487
|
+
* Necessary for track changes handling, to find the full LaTeX even with the suggestions.
|
|
488
|
+
*/ findLatexContainerElement(textNode) {
|
|
489
|
+
const MAX_DEPTH = 10; // Prevent excessive loops.
|
|
490
|
+
let element = textNode.parentElement;
|
|
491
|
+
for(let i = 0; i < MAX_DEPTH && element; i++){
|
|
492
|
+
const text = element.textContent || "";
|
|
493
|
+
const openDelim = text.indexOf("$$");
|
|
494
|
+
if (openDelim !== -1 && text.includes("$$", openDelim + 2)) {
|
|
495
|
+
return element;
|
|
496
|
+
}
|
|
497
|
+
element = element.parentElement;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Extracts LaTeX from DOM, skipping track changes deletion markers.
|
|
503
|
+
*/ extractAcceptedLatexFromDOM(textNode, caretPositionInNode = 0) {
|
|
504
|
+
const container = this.findLatexContainerElement(textNode);
|
|
505
|
+
if (!container) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const acceptedText = this.getAcceptedTextContent(container);
|
|
509
|
+
// Calculate caret offset that will be used later to find the correct LaTeX block.
|
|
510
|
+
// This includes all accepted text before textNode, plus the caret position within textNode.
|
|
511
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
512
|
+
let node = walker.nextNode();
|
|
513
|
+
let caretOffset = 0;
|
|
514
|
+
while(node && node !== textNode){
|
|
515
|
+
if (!node.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
|
|
516
|
+
caretOffset += node.textContent?.length || 0;
|
|
517
|
+
}
|
|
518
|
+
node = walker.nextNode();
|
|
519
|
+
}
|
|
520
|
+
// Add the caret position within the text node, only if textNode is not deleted by Track Changes.
|
|
521
|
+
if (node === textNode && !textNode.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
|
|
522
|
+
caretOffset += caretPositionInNode;
|
|
523
|
+
}
|
|
524
|
+
// Find the LaTeX block that contains the caret.
|
|
525
|
+
let nextSearchIndex = 0;
|
|
526
|
+
while(nextSearchIndex < acceptedText.length){
|
|
527
|
+
const openDelim = acceptedText.indexOf("$$", nextSearchIndex);
|
|
528
|
+
if (openDelim === -1) {
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
const closeDelim = acceptedText.indexOf("$$", openDelim + 2);
|
|
532
|
+
if (closeDelim === -1) {
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
if (caretOffset >= openDelim && caretOffset <= closeDelim + 2) {
|
|
536
|
+
return acceptedText.substring(openDelim + 2, closeDelim);
|
|
537
|
+
}
|
|
538
|
+
nextSearchIndex = closeDelim + 2;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Recursively extracts text content, skipping track changes tags.
|
|
543
|
+
*/ getAcceptedTextContent(node) {
|
|
544
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
545
|
+
return node.textContent || "";
|
|
546
|
+
}
|
|
547
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
548
|
+
if (node.classList?.contains("ck-suggestion-marker-deletion")) {
|
|
549
|
+
return "";
|
|
550
|
+
}
|
|
551
|
+
return Array.from(node.childNodes).map((child)=>this.getAcceptedTextContent(child)).join("");
|
|
552
|
+
}
|
|
553
|
+
return "";
|
|
554
|
+
}
|
|
555
|
+
/** Called when the modal window is closed. */ notifyWindowClosed() {
|
|
287
556
|
this.editorObject.editing.view.focus();
|
|
288
557
|
}
|
|
289
558
|
}
|
|
@@ -361,7 +630,7 @@ var mathIcon = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Generator: Adob
|
|
|
361
630
|
|
|
362
631
|
var chemIcon = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\n<svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t viewBox=\"0 0 40.3 49.5\" style=\"enable-background:new 0 0 40.3 49.5;\" xml:space=\"preserve\">\n<style type=\"text/css\">\n\t.st0{fill:#A4CF61;}\n</style>\n<path class=\"st0\" d=\"M39.2,12.1c0-1.9-1.1-3.6-2.7-4.4L24.5,0.9l0,0c-0.7-0.4-1.5-0.6-2.4-0.6c-0.9,0-1.7,0.2-2.4,0.6l0,0L2.3,10.8\n\tl0,0C0.9,11.7,0,13.2,0,14.9h0v19.6h0c0,1.7,0.9,3.3,2.3,4.1l0,0l17.4,9.9l0,0c0.7,0.4,1.5,0.6,2.4,0.6c0.9,0,1.7-0.2,2.4-0.6l0,0\n\tl12.2-6.9h0c1.5-0.8,2.6-2.5,2.6-4.3c0-2.7-2.2-4.9-4.9-4.9c-0.9,0-1.8,0.3-2.5,0.7l0,0l-9.7,5.6l-12.3-7V17.8l12.3-7l9.9,5.7l0,0\n\tc0.7,0.4,1.5,0.6,2.4,0.6C37,17,39.2,14.8,39.2,12.1\"/>\n</svg>\n";
|
|
363
632
|
|
|
364
|
-
var version = "8.15.
|
|
633
|
+
var version = "8.15.2";
|
|
365
634
|
var packageInfo = {
|
|
366
635
|
version: version};
|
|
367
636
|
|
|
@@ -703,7 +972,8 @@ class MathType extends Plugin {
|
|
|
703
972
|
imgElement = htmlDataProcessor.toView(htmlContent).getChild(0);
|
|
704
973
|
} else if (formula) {
|
|
705
974
|
const mathString = formula.replaceAll('ref="<"', 'ref="<"');
|
|
706
|
-
const
|
|
975
|
+
const lang = integration?.getLanguage() || "en"; // Safe fallback to 'en' in case integration is undefined.
|
|
976
|
+
const imgHtml = Parser.initParse(mathString, lang);
|
|
707
977
|
imgElement = htmlDataProcessor.toView(imgHtml).getChild(0);
|
|
708
978
|
// Add HTML element (<img>) to model
|
|
709
979
|
viewWriter.setAttribute("htmlContent", imgHtml, modelItem);
|
|
@@ -761,13 +1031,65 @@ class MathType extends Plugin {
|
|
|
761
1031
|
editor.editing.mapper.on("viewToModelPosition", viewToModelPositionOutsideModelElement(editor.model, (viewElement)=>viewElement.hasClass("ck-math-widget")));
|
|
762
1032
|
// Keep a reference to the original get and set function.
|
|
763
1033
|
const { get, set } = editor.data;
|
|
1034
|
+
// Listen to the preview command execution to set a flag in localStorage.
|
|
1035
|
+
// This flag will be used in the getData() to prevent converting formulas while generating the preview.
|
|
1036
|
+
// This is necessary because the preview command uses editor.getData() multiple times internally.
|
|
1037
|
+
const previewCommand = editor.commands.get("previewFinalContent");
|
|
1038
|
+
if (previewCommand) {
|
|
1039
|
+
this.listenTo(previewCommand, "execute", ()=>{
|
|
1040
|
+
localStorage.setItem("isGeneratingPreview", true);
|
|
1041
|
+
setTimeout(()=>{
|
|
1042
|
+
localStorage.setItem("isGeneratingPreview", false);
|
|
1043
|
+
}, 1000);
|
|
1044
|
+
}, {
|
|
1045
|
+
priority: "high"
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
764
1048
|
/**
|
|
765
|
-
*
|
|
1049
|
+
* Listener for getData() that handles Track Changes and semantics cleanup.
|
|
1050
|
+
*
|
|
1051
|
+
* This listener intercepts the getData() call and processes the output differently
|
|
1052
|
+
* depending on whether we're generating a preview or performing a save operation:
|
|
1053
|
+
*
|
|
1054
|
+
* - Preview Mode: Removes deleted formulas (marked with Track Changes deletion attributes)
|
|
1055
|
+
* while preserving formula images for visual representation.
|
|
1056
|
+
* - Save Mode: Converts formulas to clean MathML by removing semantic annotations
|
|
1057
|
+
* and handwritten data, ensuring clean storage format.
|
|
766
1058
|
*/ editor.data.on("get", (e)=>{
|
|
767
1059
|
const output = e.return;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1060
|
+
// Check if we're in preview mode (flag set by previewFinalContent command)
|
|
1061
|
+
const isGeneratingPreview = localStorage.getItem("isGeneratingPreview");
|
|
1062
|
+
if (isGeneratingPreview === "true") {
|
|
1063
|
+
// Wrap a <span> around all img elements to preserve Track Changes visibility for formulas.
|
|
1064
|
+
// Span must contain all the img attributes to avoid render issues.
|
|
1065
|
+
const attributesToPreserve = [
|
|
1066
|
+
"data-suggestion-",
|
|
1067
|
+
"data-comment-"
|
|
1068
|
+
];
|
|
1069
|
+
const previewOutput = output.replace(/<img([^>]*class="Wirisformula"[^>]*)>/g, (match, attributes)=>{
|
|
1070
|
+
// Extract Track Changes attributes
|
|
1071
|
+
const trackChangesAttrs = [];
|
|
1072
|
+
attributesToPreserve.forEach((prefix)=>{
|
|
1073
|
+
const regex = new RegExp(`(${prefix}[^=]*="[^"]*")`, "g");
|
|
1074
|
+
let attrMatch;
|
|
1075
|
+
while((attrMatch = regex.exec(attributes)) !== null){
|
|
1076
|
+
trackChangesAttrs.push(attrMatch[1]);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
const spanAttrs = trackChangesAttrs.length > 0 ? ` ${trackChangesAttrs.join(" ")}` : "";
|
|
1080
|
+
return `<span${spanAttrs}>${match}</span>`;
|
|
1081
|
+
});
|
|
1082
|
+
// Cleans all the semantics tag for safexml
|
|
1083
|
+
// including the handwritten data points
|
|
1084
|
+
e.return = MathML.removeSafeXMLSemantics(previewOutput);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
// Clean track changes markers only from LaTeX content before converting to MathML.
|
|
1088
|
+
const latexParsedOutput = this._endParseEditModeWithTrackChangesSupport(output);
|
|
1089
|
+
// Convert formula images to MathML. It's important to use the save mode to prevent issues.
|
|
1090
|
+
const parsedResult = Parser.endParseSaveMode(latexParsedOutput);
|
|
1091
|
+
// Remove all semantic annotations (including handwritten data points)
|
|
1092
|
+
// to ensure clean, standard MathML format for storage
|
|
771
1093
|
e.return = MathML.removeSafeXMLSemantics(parsedResult);
|
|
772
1094
|
}, {
|
|
773
1095
|
priority: "low"
|
|
@@ -807,6 +1129,38 @@ class MathType extends Plugin {
|
|
|
807
1129
|
});
|
|
808
1130
|
}
|
|
809
1131
|
/**
|
|
1132
|
+
* When track changes markers are present inside a LaTeX block:
|
|
1133
|
+
* - The LaTeX is preserved as text (not converted to MathML) to maintain suggestions.
|
|
1134
|
+
* - It also prevents the issue where the suggestions were placed as MathML tags.
|
|
1135
|
+
*
|
|
1136
|
+
* When no track changes markers are inside a LaTeX block:
|
|
1137
|
+
* - The LaTeX is converted to MathML normally. (previous default behavior)
|
|
1138
|
+
*
|
|
1139
|
+
* This is to ensure that:
|
|
1140
|
+
* 1. LaTeX without suggestions gets converted to MathML for final output.
|
|
1141
|
+
* 2. LaTeX with pending suggestions is preserved so setData(getData()) works correctly.
|
|
1142
|
+
*/ _endParseEditModeWithTrackChangesSupport(code) {
|
|
1143
|
+
if (!Configuration.get("parseModes").includes("latex")) {
|
|
1144
|
+
return code;
|
|
1145
|
+
}
|
|
1146
|
+
const latexBlockRegex = /\$\$([\s\S]*?)\$\$/g;
|
|
1147
|
+
const trackChangesRegex = /<(suggestion|comment)-(start|end)/i;
|
|
1148
|
+
//TODO: Validate if replace all is needed instead of just replace when it is all implemented.
|
|
1149
|
+
return code.replace(latexBlockRegex, (fullMatch, latexContent)=>{
|
|
1150
|
+
// Check if this LaTeX contains track changes markers to prevent conversion.
|
|
1151
|
+
if (trackChangesRegex.test(latexContent)) {
|
|
1152
|
+
return fullMatch;
|
|
1153
|
+
}
|
|
1154
|
+
// When LaTeX has no suggestion, it can be converted to MathML.
|
|
1155
|
+
const decodedLatex = Util.htmlEntitiesDecode(latexContent);
|
|
1156
|
+
let mathml = Util.htmlSanitize(Latex.getMathMLFromLatex(decodedLatex, true));
|
|
1157
|
+
if (!Configuration.get("saveHandTraces")) {
|
|
1158
|
+
mathml = MathML.removeAnnotation(mathml, "application/json");
|
|
1159
|
+
}
|
|
1160
|
+
return mathml;
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
810
1164
|
* Expose the WirisPlugin variable to the window
|
|
811
1165
|
*/ // eslint-disable-next-line class-methods-use-this
|
|
812
1166
|
_exposeWiris() {
|
|
@@ -832,9 +1186,22 @@ class MathType extends Plugin {
|
|
|
832
1186
|
trackChangesEditing.enableCommand("ChemType");
|
|
833
1187
|
// Adds custom label replacing the default 'mathml'.
|
|
834
1188
|
// Handles both singular and plural forms.
|
|
835
|
-
trackChangesEditing.descriptionFactory.registerElementLabel("mathml", (quantity)=>(quantity > 1 ? quantity
|
|
1189
|
+
trackChangesEditing.descriptionFactory.registerElementLabel("mathml", (quantity)=>(quantity > 1 ? `${quantity} ` : "") + StringManager.get(quantity > 1 ? "formulas" : "formula", integration?.getLanguage() || "en"));
|
|
1190
|
+
this._registerLatexTrackChangesAdapter(integration);
|
|
836
1191
|
}
|
|
837
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Register a custom adapter for handling LaTeX text changes.
|
|
1195
|
+
* This ensures that LaTeX formulas ($$...$$) are treated as atomic units
|
|
1196
|
+
* when used by the track changes feature and avoid partial edits.
|
|
1197
|
+
*/ _registerLatexTrackChangesAdapter(integration) {
|
|
1198
|
+
const { editor } = this;
|
|
1199
|
+
editor.model.document.on("change:data", ()=>{
|
|
1200
|
+
if (integration) {
|
|
1201
|
+
integration._trackChangesEnabled = editor.commands.get("trackChanges")?.value ?? false;
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
838
1205
|
}
|
|
839
1206
|
|
|
840
1207
|
export { CKEditor5Integration, ChemTypeCommand, MathTypeCommand, MathType as default };
|