@wiris/mathtype-ckeditor5 8.15.1 → 8.15.3
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/dist/browser/index.js +2110 -452
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.umd.js +2110 -452
- package/dist/browser/index.umd.js.map +1 -1
- package/dist/index.js +413 -109
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/integration.js +468 -122
- package/src/plugin.js +64 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wiris/mathtype-ckeditor5",
|
|
3
|
-
"version": "8.15.
|
|
3
|
+
"version": "8.15.3",
|
|
4
4
|
"description": "MathType Web for CKEditor5 editor",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chem",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"prepare": "npm run build:dist"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@wiris/mathtype-html-integration-devkit": "1.17.
|
|
51
|
+
"@wiris/mathtype-html-integration-devkit": "1.17.16"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@ckeditor/ckeditor5-dev-build-tools": "^42.1.0"
|
package/src/integration.js
CHANGED
|
@@ -146,58 +146,84 @@ export default class CKEditor5Integration extends IntegrationModel {
|
|
|
146
146
|
* @returns {module:engine/model/element~Element} The model element corresponding to the inserted image
|
|
147
147
|
*/
|
|
148
148
|
insertMathml(mathml) {
|
|
149
|
-
// This returns the value returned by the callback function (writer => {...})
|
|
150
149
|
return this.editorObject.model.change((writer) => {
|
|
151
|
-
const
|
|
150
|
+
const { isNewElement, temporalImage } = this.getCore().editionProperties;
|
|
152
151
|
const selection = this.editorObject.model.document.selection;
|
|
152
|
+
const attributes = Object.fromEntries(selection.getAttributes());
|
|
153
|
+
const modelElementNew = writer.createElement("mathml", { formula: mathml, ...attributes });
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
155
|
+
if (isNewElement) {
|
|
156
|
+
return this.insertNewFormula(writer, mathml, modelElementNew);
|
|
157
|
+
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!mathml) return;
|
|
159
|
+
return this.replaceExistingFormula(mathml, modelElementNew, temporalImage);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Inserts a new formula at the current selection position.
|
|
165
|
+
*/
|
|
166
|
+
insertNewFormula(writer, mathml, modelElement) {
|
|
167
|
+
if (!mathml) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
167
170
|
|
|
168
|
-
|
|
171
|
+
const viewSelection =
|
|
172
|
+
this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
|
|
173
|
+
const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
|
|
169
174
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const range of viewSelection.getRanges()) {
|
|
173
|
-
const modelRange = this.editorObject.editing.mapper.toModelRange(range);
|
|
174
|
-
const modelSelection = this.editorObject.model.createSelection(modelRange);
|
|
175
|
+
this.editorObject.model.insertObject(modelElement, modelPosition);
|
|
176
|
+
this.deleteViewSelection(viewSelection);
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// Set carret after the formula.
|
|
179
|
+
const position = this.editorObject.model.createPositionAfter(modelElement);
|
|
180
|
+
writer.setSelection(position);
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
writer.setSelection(position);
|
|
183
|
-
} else {
|
|
184
|
-
const img = core.editionProperties.temporalImage;
|
|
185
|
-
const viewElement = this.editorObject.editing.view.domConverter.domToView(img).parent;
|
|
186
|
-
const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewElement);
|
|
182
|
+
return modelElement;
|
|
183
|
+
}
|
|
187
184
|
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
deleteViewSelection(viewSelection) {
|
|
186
|
+
if (viewSelection.isCollapsed) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
for (const range of viewSelection.getRanges()) {
|
|
191
|
+
const modelRange = this.editorObject.editing.mapper.toModelRange(range);
|
|
192
|
+
const modelSelection = this.editorObject.model.createSelection(modelRange);
|
|
193
|
+
|
|
194
|
+
this.editorObject.model.deleteContent(modelSelection);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Replaces an existing formula with updated MathML.
|
|
200
|
+
*/
|
|
201
|
+
replaceExistingFormula(mathml, modelElement, temporalImage) {
|
|
202
|
+
const viewNode = this.editorObject.editing.view.domConverter.domToView(temporalImage);
|
|
203
|
+
|
|
204
|
+
// Check if image exists in view to do standard formula editing
|
|
205
|
+
if (viewNode?.parent) {
|
|
206
|
+
const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewNode.parent);
|
|
207
|
+
|
|
208
|
+
// Insert the new <mathml> and remove the old one
|
|
209
|
+
const position = this.editorObject.model.createPositionBefore(modelElementOld);
|
|
210
|
+
|
|
211
|
+
if (mathml) {
|
|
212
|
+
this.editorObject.model.insertObject(modelElement, position);
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
|
|
199
|
-
return
|
|
200
|
-
}
|
|
215
|
+
this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, "on"));
|
|
216
|
+
return modelElement;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Otherwise it's LaTeX editing, so we insert at current selection
|
|
220
|
+
if (!mathml) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.editorObject.model.insertContent(modelElement);
|
|
225
|
+
|
|
226
|
+
return modelElement;
|
|
201
227
|
}
|
|
202
228
|
|
|
203
229
|
/**
|
|
@@ -233,122 +259,442 @@ export default class CKEditor5Integration extends IntegrationModel {
|
|
|
233
259
|
}
|
|
234
260
|
|
|
235
261
|
/** @inheritdoc */
|
|
236
|
-
insertFormula(
|
|
262
|
+
insertFormula(_focusElement, windowTarget, mathml, _wirisProperties) {
|
|
237
263
|
// eslint-disable-line no-unused-vars
|
|
238
264
|
const returnObject = {};
|
|
239
|
-
|
|
240
265
|
let mathmlOrigin;
|
|
266
|
+
|
|
241
267
|
if (!mathml) {
|
|
242
268
|
this.insertMathml("");
|
|
243
269
|
} else if (this.core.editMode === "latex") {
|
|
244
|
-
returnObject
|
|
245
|
-
|
|
270
|
+
this.handleLatexInsertion(returnObject, windowTarget, mathml);
|
|
271
|
+
} else {
|
|
272
|
+
mathmlOrigin = this.handleMathmlInsertion(returnObject, windowTarget, mathml);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const payload = {
|
|
276
|
+
mathml: mathml ? MathML.safeXmlDecode(mathml) : undefined,
|
|
277
|
+
elapsed_time: Date.now() - this.core.editionProperties.editionStartTime,
|
|
278
|
+
toolbar: this.core.modalDialog.contentManager.toolbar,
|
|
279
|
+
size: mathml?.length,
|
|
280
|
+
};
|
|
246
281
|
|
|
282
|
+
if (mathmlOrigin) {
|
|
283
|
+
payload.mathml_origin = MathML.safeXmlDecode(mathmlOrigin);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
Telemeter.telemeter.track("INSERTED_FORMULA", {
|
|
288
|
+
...payload,
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error("Error tracking INSERTED_FORMULA", error);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.core.editionProperties.temporalImage = null;
|
|
295
|
+
|
|
296
|
+
return returnObject;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
handleLatexInsertion(returnObject, windowTarget, mathml) {
|
|
300
|
+
returnObject.latex = Latex.getLatexFromMathML(mathml);
|
|
301
|
+
returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
|
|
302
|
+
|
|
303
|
+
const { latexRange } = this.core.editionProperties;
|
|
304
|
+
|
|
305
|
+
// When latexRange exists (meaning the whole LaTeX was selected or the editor was opened),
|
|
306
|
+
// find the node contaning the LaTeX and replace it fully.
|
|
307
|
+
if (latexRange) {
|
|
308
|
+
const startNode = this.findText(latexRange.startContainer);
|
|
309
|
+
const endNode = this.findText(latexRange.endContainer);
|
|
310
|
+
|
|
311
|
+
// If nodes found, use standard replacement.
|
|
312
|
+
if (startNode && endNode) {
|
|
313
|
+
this.replaceLatexWithNodes(startNode, endNode, latexRange, returnObject.latex);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.replaceLatexUsingModelSearch(returnObject.latex);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
handleMathmlInsertion(returnObject, windowTarget, mathml) {
|
|
322
|
+
const mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const modelElement = this.insertMathml(mathml);
|
|
326
|
+
const viewElement = this.editorObject.editing.mapper.toViewElement(modelElement);
|
|
327
|
+
|
|
328
|
+
returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(viewElement, windowTarget.document);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error.toString().includes("Cannot read property 'parent' of undefined")) {
|
|
331
|
+
this.core.modalDialog.cancelAction();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return mathmlOrigin;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Gets selection attributes excluding track changes tags.
|
|
340
|
+
*/
|
|
341
|
+
getCleanSelectionAttributes() {
|
|
342
|
+
const attributes = {};
|
|
343
|
+
|
|
344
|
+
for (const [key, value] of this.editorObject.model.document.selection.getAttributes()) {
|
|
345
|
+
if (!key.startsWith("suggestion:") && !key.startsWith("comment:")) {
|
|
346
|
+
attributes[key] = value;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return attributes;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Searches for the original LaTeX in the model and replaces it.
|
|
355
|
+
* Fallback when findText() cannot locate DOM nodes (like when there are track changes modifications).
|
|
356
|
+
*/
|
|
357
|
+
replaceLatexUsingModelSearch(newLatex) {
|
|
358
|
+
const foundRange = this.findLatexBlockNearSelection();
|
|
359
|
+
|
|
360
|
+
if (foundRange) {
|
|
361
|
+
this.editorObject.model.change((writer) => writer.setSelection(foundRange));
|
|
362
|
+
this.replaceRangeWithLatex(newLatex);
|
|
363
|
+
} else {
|
|
364
|
+
// Insert at current position as a last resort.
|
|
247
365
|
this.editorObject.model.change((writer) => {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
366
|
+
const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
|
|
367
|
+
this.editorObject.model.insertContent(newLatexText);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.core.editionProperties.extractedLatex = null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Checks if a text proxy has a track changes deletion marker.
|
|
376
|
+
*/
|
|
377
|
+
isDeletedText(text) {
|
|
378
|
+
for (const [key, value] of text.getAttributes()) {
|
|
379
|
+
if (key.startsWith("suggestion:") && value === "deletion") {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Finds a LaTeX block ($$...$$) near the current selection.
|
|
389
|
+
* Handles track changes by considering the "accepted" version of text.
|
|
390
|
+
*/
|
|
391
|
+
findLatexBlockNearSelection() {
|
|
392
|
+
const position = this.editorObject.model.document.selection.getFirstPosition();
|
|
393
|
+
|
|
394
|
+
if (!position?.parent) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Build LaTeX with track changes accepted suggestions, if any.
|
|
399
|
+
const { textParts, acceptedText } = this.collectTextParts(position.parent);
|
|
400
|
+
|
|
401
|
+
if (!acceptedText.includes("$$")) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// To handle multiple LaTeX on same line.
|
|
406
|
+
const targetLatex = this.core.editionProperties.extractedLatex;
|
|
407
|
+
const fullLatex = `$$${targetLatex}$$`;
|
|
408
|
+
const startIndex = acceptedText.indexOf(fullLatex);
|
|
409
|
+
|
|
410
|
+
if (startIndex === -1) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const latexBoundaries = { start: startIndex, end: startIndex + fullLatex.length };
|
|
415
|
+
|
|
416
|
+
return this.convertAcceptedOffsetsToModelRange(textParts, latexBoundaries);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Collects all text fragments from a paragraph, tracking both model and accepted text positions.
|
|
421
|
+
* This is necessary to handle track changes where some LaTeX may have suggestions.
|
|
422
|
+
*/
|
|
423
|
+
collectTextParts(paragraph) {
|
|
424
|
+
const textParts = [];
|
|
425
|
+
let acceptedTextOffset = 0;
|
|
426
|
+
let acceptedText = "";
|
|
427
|
+
|
|
428
|
+
for (const item of this.editorObject.model.createRangeIn(paragraph).getItems()) {
|
|
429
|
+
if (item.is("$textProxy")) {
|
|
430
|
+
const isDeleted = this.isDeletedText(item);
|
|
431
|
+
|
|
432
|
+
textParts.push({
|
|
433
|
+
text: item.data,
|
|
434
|
+
startOffset: item.startOffset,
|
|
435
|
+
endOffset: item.startOffset + item.data.length,
|
|
436
|
+
parent: item.textNode.parent,
|
|
437
|
+
acceptedStart: isDeleted ? null : acceptedTextOffset,
|
|
438
|
+
acceptedEnd: isDeleted ? null : acceptedTextOffset + item.data.length,
|
|
439
|
+
isDeleted
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!isDeleted) {
|
|
443
|
+
acceptedText += item.data;
|
|
444
|
+
acceptedTextOffset += item.data.length;
|
|
257
445
|
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return { textParts, acceptedText };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Converts LaTeX with track changes accepted suggestions to a CKEditor model Range.
|
|
453
|
+
*/
|
|
454
|
+
convertAcceptedOffsetsToModelRange(textParts, latexBoundaries) {
|
|
455
|
+
let startPartIndex = -1, endPartIndex = -1;
|
|
456
|
+
let startOffsetInPart = 0, endOffsetInPart = 0;
|
|
457
|
+
|
|
458
|
+
// Find which text parts contain the LaTeX block boundaries
|
|
459
|
+
for (let i = 0; i < textParts.length; i++) {
|
|
460
|
+
const part = textParts[i];
|
|
461
|
+
if (part.isDeleted) continue;
|
|
462
|
+
|
|
463
|
+
if (startPartIndex === -1 && latexBoundaries.start >= part.acceptedStart && latexBoundaries.start <= part.acceptedEnd) {
|
|
464
|
+
startPartIndex = i;
|
|
465
|
+
startOffsetInPart = latexBoundaries.start - part.acceptedStart;
|
|
466
|
+
}
|
|
258
467
|
|
|
259
|
-
|
|
260
|
-
|
|
468
|
+
if (latexBoundaries.end >= part.acceptedStart && latexBoundaries.end <= part.acceptedEnd) {
|
|
469
|
+
endPartIndex = i;
|
|
470
|
+
endOffsetInPart = latexBoundaries.end - part.acceptedStart;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
261
473
|
|
|
262
|
-
|
|
263
|
-
|
|
474
|
+
if (startPartIndex === -1 || endPartIndex === -1) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
264
477
|
|
|
265
|
-
|
|
478
|
+
// Extend range to include any consecutive deleted parts after the block.
|
|
479
|
+
let finalEndIndex = endPartIndex;
|
|
480
|
+
let finalEndOffset = endOffsetInPart;
|
|
266
481
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml),
|
|
272
|
-
)}$$`;
|
|
273
|
-
let data = latexRange.startContainer.data;
|
|
482
|
+
for (let i = endPartIndex + 1; i < textParts.length && textParts[i].isDeleted; i++) {
|
|
483
|
+
finalEndIndex = i;
|
|
484
|
+
finalEndOffset = textParts[i].text.length;
|
|
485
|
+
}
|
|
274
486
|
|
|
275
|
-
|
|
276
|
-
|
|
487
|
+
const startPart = textParts[startPartIndex];
|
|
488
|
+
const endPart = textParts[finalEndIndex];
|
|
277
489
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
data = data.replace(substring, "");
|
|
490
|
+
return this.editorObject.model.createRange(
|
|
491
|
+
this.editorObject.model.createPositionAt(startPart.parent, startPart.startOffset + startOffsetInPart),
|
|
492
|
+
this.editorObject.model.createPositionAt(endPart.parent, endPart.startOffset + finalEndOffset)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
284
495
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
496
|
+
replaceRangeWithLatex(newLatex) {
|
|
497
|
+
this.editorObject.model.change((writer) => {
|
|
498
|
+
this.editorObject.model.deleteContent(this.editorObject.model.document.selection);
|
|
499
|
+
|
|
500
|
+
const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
|
|
501
|
+
this.editorObject.model.insertContent(newLatexText);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Replaces the whole LaTeX in the CKEditor5 model.
|
|
507
|
+
*/
|
|
508
|
+
replaceLatexWithNodes(startNode, endNode, latexRange, newLatex) {
|
|
509
|
+
this.editorObject.model.change((writer) => {
|
|
510
|
+
const startOffset = startNode.startOffset + latexRange.startOffset;
|
|
511
|
+
const endOffset = endNode.startOffset + latexRange.endOffset;
|
|
512
|
+
|
|
513
|
+
let startPosition = writer.createPositionAt(startNode.parent, startOffset);
|
|
514
|
+
let endPosition = writer.createPositionAt(endNode.parent, endOffset);
|
|
515
|
+
|
|
516
|
+
// Adjust positions when LaTeX is adjacent to a formula.
|
|
517
|
+
const startContainer = latexRange.startContainer;
|
|
518
|
+
if (startContainer.nodeType === Node.TEXT_NODE && startContainer.previousSibling?.nodeType === Node.ELEMENT_NODE) {
|
|
519
|
+
const originalLatex = `$$${Latex.getLatexFromMathML(
|
|
520
|
+
MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml),
|
|
521
|
+
)}$$`;
|
|
522
|
+
const textData = startContainer.data.replaceAll(String.fromCodePoint(8288), "");
|
|
523
|
+
const latexOffset = textData.indexOf(originalLatex);
|
|
524
|
+
|
|
525
|
+
if (latexOffset !== -1) {
|
|
526
|
+
const closingDelimiterOffset = textData.substring(latexOffset + 2).indexOf("$$") + 4;
|
|
527
|
+
startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexOffset);
|
|
528
|
+
endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + closingDelimiterOffset + latexOffset);
|
|
293
529
|
}
|
|
530
|
+
}
|
|
294
531
|
|
|
295
|
-
|
|
532
|
+
writer.setSelection(writer.createRange(startPosition, endPosition));
|
|
533
|
+
});
|
|
296
534
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
535
|
+
this.replaceRangeWithLatex(newLatex);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Inherited method from IntegrationModel.
|
|
540
|
+
* Gets the MathML from a text node containing LaTeX.
|
|
541
|
+
* Handles track changes by simulating "accept all changes" before conversion.
|
|
542
|
+
*/
|
|
543
|
+
getMathmlFromTextNode(textNode, caretPosition) {
|
|
544
|
+
const standardResult = Latex.getLatexFromTextNode(textNode, caretPosition);
|
|
545
|
+
const acceptedLatex = this.extractAcceptedLatexFromDOM(textNode, caretPosition);
|
|
546
|
+
|
|
547
|
+
// Prioritize accepted LaTeX if it differs from standard extraction (for track changes compatibility).
|
|
548
|
+
// Important node: use explicit undefined check to allow empty LaTeX strings, otherwise it would not detect $$$$ as valid LaTeX.
|
|
549
|
+
const latex = (acceptedLatex !== undefined && acceptedLatex !== standardResult?.latex)
|
|
550
|
+
? acceptedLatex
|
|
551
|
+
: standardResult?.latex;
|
|
552
|
+
|
|
553
|
+
// Do not continue if no LaTeX found by either method.
|
|
554
|
+
// This is necessary since both parameters can be independently undefined in some edge cases.
|
|
555
|
+
if (latex === undefined && acceptedLatex === undefined) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Verify caret is inside LaTeX block for track changes edge cases.
|
|
560
|
+
if (!standardResult && acceptedLatex !== undefined && !this.isCaretInsideLatexBlock(textNode, caretPosition)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const finalLatex = latex === undefined ? acceptedLatex : latex;
|
|
565
|
+
|
|
566
|
+
this.storeLatexRangeWithFallback(textNode, caretPosition, finalLatex);
|
|
567
|
+
|
|
568
|
+
return Latex.getMathMLFromLatex(finalLatex);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
isCaretInsideLatexBlock(textNode, caretPosition = 0) {
|
|
572
|
+
// If LaTeX is found, the caret is inside one.
|
|
573
|
+
return this.extractAcceptedLatexFromDOM(textNode, caretPosition) !== undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Stores the LaTeX range for its replacement later.
|
|
578
|
+
*/
|
|
579
|
+
storeLatexRangeWithFallback(textNode, caretPosition, latex) {
|
|
580
|
+
const parentTag = textNode.parentElement?.tagName?.toLowerCase();
|
|
581
|
+
|
|
582
|
+
if (!textNode.parentElement || parentTag === "textarea") {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const latexResult = Latex.getLatexFromTextNode(textNode, caretPosition);
|
|
587
|
+
|
|
588
|
+
if (latexResult) {
|
|
589
|
+
const range = document.createRange();
|
|
590
|
+
|
|
591
|
+
range.setStart(latexResult.startNode, latexResult.startPosition);
|
|
592
|
+
range.setEnd(latexResult.endNode, latexResult.endPosition);
|
|
593
|
+
this.core.editionProperties.latexRange = range;
|
|
300
594
|
} else {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
595
|
+
this.core.editionProperties.latexRange = null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this.core.editionProperties.extractedLatex = latex;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Finds a container element containing a complete LaTeX block.
|
|
603
|
+
* Necessary for track changes handling, to find the full LaTeX even with the suggestions.
|
|
604
|
+
*/
|
|
605
|
+
findLatexContainerElement(textNode) {
|
|
606
|
+
const MAX_DEPTH = 10; // Prevent excessive loops.
|
|
607
|
+
let element = textNode.parentElement;
|
|
608
|
+
|
|
609
|
+
for (let i = 0; i < MAX_DEPTH && element; i++) {
|
|
610
|
+
const text = element.textContent || "";
|
|
611
|
+
const openDelim = text.indexOf("$$");
|
|
612
|
+
|
|
613
|
+
if (openDelim !== -1 && text.includes("$$", openDelim + 2)) {
|
|
614
|
+
return element;
|
|
312
615
|
}
|
|
616
|
+
|
|
617
|
+
element = element.parentElement;
|
|
313
618
|
}
|
|
314
619
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
mathml_origin: mathmlOrigin ? MathML.safeXmlDecode(mathmlOrigin) : mathmlOrigin,
|
|
318
|
-
mathml: mathml ? MathML.safeXmlDecode(mathml) : mathml,
|
|
319
|
-
elapsed_time: Date.now() - this.core.editionProperties.editionStartTime,
|
|
320
|
-
editor_origin: null, // TODO read formula to find out whether it comes from Oxygen Desktop
|
|
321
|
-
toolbar: this.core.modalDialog.contentManager.toolbar,
|
|
322
|
-
size: mathml?.length,
|
|
323
|
-
};
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
324
622
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Extracts LaTeX from DOM, skipping track changes deletion markers.
|
|
625
|
+
*/
|
|
626
|
+
extractAcceptedLatexFromDOM(textNode, caretPositionInNode = 0) {
|
|
627
|
+
const container = this.findLatexContainerElement(textNode);
|
|
329
628
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
Telemeter.telemeter.track("INSERTED_FORMULA", {
|
|
333
|
-
...payload,
|
|
334
|
-
});
|
|
335
|
-
} catch (error) {
|
|
336
|
-
console.error("Error tracking INSERTED_FORMULA", error);
|
|
629
|
+
if (!container) {
|
|
630
|
+
return;
|
|
337
631
|
}
|
|
338
632
|
|
|
339
|
-
|
|
340
|
-
We probably should add it here as well, but we should look further into how */
|
|
341
|
-
// this.editorObject.fire('change');
|
|
633
|
+
const acceptedText = this.getAcceptedTextContent(container);
|
|
342
634
|
|
|
343
|
-
//
|
|
344
|
-
|
|
635
|
+
// Calculate caret offset that will be used later to find the correct LaTeX block.
|
|
636
|
+
// This includes all accepted text before textNode, plus the caret position within textNode.
|
|
637
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
638
|
+
let node = walker.nextNode();
|
|
639
|
+
let caretOffset = 0;
|
|
345
640
|
|
|
346
|
-
|
|
641
|
+
while (node && node !== textNode) {
|
|
642
|
+
if (!node.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
|
|
643
|
+
caretOffset += node.textContent?.length || 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
node = walker.nextNode();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Add the caret position within the text node, only if textNode is not deleted by Track Changes.
|
|
650
|
+
if (node === textNode && !textNode.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
|
|
651
|
+
caretOffset += caretPositionInNode;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Find the LaTeX block that contains the caret.
|
|
655
|
+
let nextSearchIndex = 0;
|
|
656
|
+
|
|
657
|
+
while (nextSearchIndex < acceptedText.length) {
|
|
658
|
+
const openDelim = acceptedText.indexOf("$$", nextSearchIndex);
|
|
659
|
+
|
|
660
|
+
if (openDelim === -1) {
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const closeDelim = acceptedText.indexOf("$$", openDelim + 2);
|
|
665
|
+
|
|
666
|
+
if (closeDelim === -1) {
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (caretOffset >= openDelim && caretOffset <= closeDelim + 2) {
|
|
671
|
+
return acceptedText.substring(openDelim + 2, closeDelim);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
nextSearchIndex = closeDelim + 2;
|
|
675
|
+
}
|
|
347
676
|
}
|
|
348
677
|
|
|
349
678
|
/**
|
|
350
|
-
*
|
|
679
|
+
* Recursively extracts text content, skipping track changes tags.
|
|
351
680
|
*/
|
|
681
|
+
getAcceptedTextContent(node) {
|
|
682
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
683
|
+
return node.textContent || "";
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
687
|
+
if (node.classList?.contains("ck-suggestion-marker-deletion")) {
|
|
688
|
+
return "";
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return Array.from(node.childNodes).map((child) => this.getAcceptedTextContent(child)).join("");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return "";
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Called when the modal window is closed. */
|
|
352
698
|
notifyWindowClosed() {
|
|
353
699
|
this.editorObject.editing.view.focus();
|
|
354
700
|
}
|