@wiris/mathtype-ckeditor5 8.15.1 → 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.
@@ -11297,49 +11297,68 @@
11297
11297
  * @param {String} mathml MathML to update old one or insert
11298
11298
  * @returns {module:engine/model/element~Element} The model element corresponding to the inserted image
11299
11299
  */ insertMathml(mathml) {
11300
- // This returns the value returned by the callback function (writer => {...})
11301
11300
  return this.editorObject.model.change((writer)=>{
11302
- const core = this.getCore();
11301
+ const { isNewElement, temporalImage } = this.getCore().editionProperties;
11303
11302
  const selection = this.editorObject.model.document.selection;
11303
+ const attributes = Object.fromEntries(selection.getAttributes());
11304
11304
  const modelElementNew = writer.createElement("mathml", {
11305
11305
  formula: mathml,
11306
- ...Object.fromEntries(selection.getAttributes())
11306
+ ...attributes
11307
11307
  });
11308
- // Obtain the DOM <span><img ... /></span> object corresponding to the formula
11309
- if (core.editionProperties.isNewElement) {
11310
- // Don't bother inserting anything at all if the MathML is empty.
11311
- if (!mathml) return;
11312
- const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
11313
- const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
11314
- this.editorObject.model.insertObject(modelElementNew, modelPosition);
11315
- // Remove selection
11316
- if (!viewSelection.isCollapsed) {
11317
- for (const range of viewSelection.getRanges()){
11318
- const modelRange = this.editorObject.editing.mapper.toModelRange(range);
11319
- const modelSelection = this.editorObject.model.createSelection(modelRange);
11320
- this.editorObject.model.deleteContent(modelSelection);
11321
- }
11322
- }
11323
- // Set carret after the formula
11324
- const position = this.editorObject.model.createPositionAfter(modelElementNew);
11325
- writer.setSelection(position);
11326
- } else {
11327
- const img = core.editionProperties.temporalImage;
11328
- const viewElement = this.editorObject.editing.view.domConverter.domToView(img).parent;
11329
- const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewElement);
11330
- // Insert the new <mathml> and remove the old one
11331
- const position = this.editorObject.model.createPositionBefore(modelElementOld);
11332
- // If the given MathML is empty, don't insert a new formula.
11333
- if (mathml) {
11334
- this.editorObject.model.insertObject(modelElementNew, position);
11335
- }
11336
- this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, 'on'));
11308
+ if (isNewElement) {
11309
+ return this.insertNewFormula(writer, mathml, modelElementNew);
11337
11310
  }
11338
- // eslint-disable-next-line consistent-return
11339
- return modelElementNew;
11311
+ return this.replaceExistingFormula(mathml, modelElementNew, temporalImage);
11340
11312
  });
11341
11313
  }
11342
11314
  /**
11315
+ * Inserts a new formula at the current selection position.
11316
+ */ insertNewFormula(writer, mathml, modelElement) {
11317
+ if (!mathml) {
11318
+ return;
11319
+ }
11320
+ const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
11321
+ const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
11322
+ this.editorObject.model.insertObject(modelElement, modelPosition);
11323
+ this.deleteViewSelection(viewSelection);
11324
+ // Set carret after the formula.
11325
+ const position = this.editorObject.model.createPositionAfter(modelElement);
11326
+ writer.setSelection(position);
11327
+ return modelElement;
11328
+ }
11329
+ deleteViewSelection(viewSelection) {
11330
+ if (viewSelection.isCollapsed) {
11331
+ return;
11332
+ }
11333
+ for (const range of viewSelection.getRanges()){
11334
+ const modelRange = this.editorObject.editing.mapper.toModelRange(range);
11335
+ const modelSelection = this.editorObject.model.createSelection(modelRange);
11336
+ this.editorObject.model.deleteContent(modelSelection);
11337
+ }
11338
+ }
11339
+ /**
11340
+ * Replaces an existing formula with updated MathML.
11341
+ */ replaceExistingFormula(mathml, modelElement, temporalImage) {
11342
+ const viewNode = this.editorObject.editing.view.domConverter.domToView(temporalImage);
11343
+ // Check if image exists in view to do standard formula editing
11344
+ if (viewNode?.parent) {
11345
+ const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewNode.parent);
11346
+ // Insert the new <mathml> and remove the old one
11347
+ const position = this.editorObject.model.createPositionBefore(modelElementOld);
11348
+ if (mathml) {
11349
+ this.editorObject.model.insertObject(modelElement, position);
11350
+ }
11351
+ this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, "on"));
11352
+ return modelElement;
11353
+ }
11354
+ // Otherwise it's LaTeX editing, so we insert at current selection
11355
+ if (!mathml) {
11356
+ return;
11357
+ }
11358
+ this.editorObject.model.insertContent(modelElement);
11359
+ return modelElement;
11360
+ }
11361
+ /**
11343
11362
  * Finds the text node corresponding to given DOM text element.
11344
11363
  * @param {element} viewElement Element to find corresponding text node of.
11345
11364
  * @returns {module:engine/model/text~Text|undefined} Text node corresponding to the given element or undefined if it doesn't exist.
@@ -11366,81 +11385,26 @@
11366
11385
  }
11367
11386
  }
11368
11387
  }
11369
- /** @inheritdoc */ insertFormula(focusElement, windowTarget, mathml, wirisProperties) {
11388
+ /** @inheritdoc */ insertFormula(_focusElement, windowTarget, mathml, _wirisProperties) {
11370
11389
  // eslint-disable-line no-unused-vars
11371
11390
  const returnObject = {};
11372
11391
  let mathmlOrigin;
11373
11392
  if (!mathml) {
11374
11393
  this.insertMathml("");
11375
11394
  } else if (this.core.editMode === "latex") {
11376
- returnObject.latex = Latex.getLatexFromMathML(mathml);
11377
- returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
11378
- this.editorObject.model.change((writer)=>{
11379
- const { latexRange } = this.core.editionProperties;
11380
- // Add null check for latexRange.
11381
- // When the editor is initialized in a textarea element, latexRange may not be set on initial load.
11382
- // This check ensures formulas can still be inserted by falling back to MathML insertion if latexRange is not available.
11383
- if (!latexRange) {
11384
- // Fallback to regular MathML insertion if latexRange is not available
11385
- this.insertMathml(mathml);
11386
- return;
11387
- }
11388
- const startNode = this.findText(latexRange.startContainer);
11389
- const endNode = this.findText(latexRange.endContainer);
11390
- let startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexRange.startOffset);
11391
- let endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + latexRange.endOffset);
11392
- let range = writer.createRange(startPosition, endPosition);
11393
- // When Latex is next to image/formula.
11394
- if (latexRange.startContainer.nodeType === 3 && latexRange.startContainer.previousSibling?.nodeType === 1) {
11395
- // Get the position of the latex to be replaced.
11396
- const latexEdited = `$$${Latex.getLatexFromMathML(MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml))}$$`;
11397
- let data = latexRange.startContainer.data;
11398
- // Remove invisible characters.
11399
- data = data.replaceAll(String.fromCharCode(8288), "");
11400
- // Get to the start of the latex we are editing.
11401
- const offset = data.indexOf(latexEdited);
11402
- const dataOffset = data.substring(offset);
11403
- const second$ = dataOffset.substring(2).indexOf("$$") + 4;
11404
- const substring = dataOffset.substr(0, second$);
11405
- data = data.replace(substring, "");
11406
- if (!data) {
11407
- startPosition = writer.createPositionBefore(startNode);
11408
- range = startNode;
11409
- } else {
11410
- startPosition = startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + offset);
11411
- endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + second$ + offset);
11412
- range = writer.createRange(startPosition, endPosition);
11413
- }
11414
- }
11415
- const modelSelection = this.editorObject.model.createSelection(range);
11416
- this.editorObject.model.deleteContent(modelSelection);
11417
- writer.insertText(`$$${returnObject.latex}$$`, startNode.getAttributes(), startPosition);
11418
- });
11395
+ this.handleLatexInsertion(returnObject, windowTarget, mathml);
11419
11396
  } else {
11420
- mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml;
11421
- try {
11422
- returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(this.editorObject.editing.mapper.toViewElement(this.insertMathml(mathml)), windowTarget.document);
11423
- } catch (e) {
11424
- const x = e.toString();
11425
- if (x.includes("CKEditorError: Cannot read property 'parent' of undefined")) {
11426
- this.core.modalDialog.cancelAction();
11427
- }
11428
- }
11397
+ mathmlOrigin = this.handleMathmlInsertion(returnObject, windowTarget, mathml);
11429
11398
  }
11430
- // Build the telemeter payload separated to delete null/undefined entries.
11431
11399
  const payload = {
11432
- mathml_origin: mathmlOrigin ? MathML.safeXmlDecode(mathmlOrigin) : mathmlOrigin,
11433
- mathml: mathml ? MathML.safeXmlDecode(mathml) : mathml,
11400
+ mathml: mathml ? MathML.safeXmlDecode(mathml) : undefined,
11434
11401
  elapsed_time: Date.now() - this.core.editionProperties.editionStartTime,
11435
- editor_origin: null,
11436
11402
  toolbar: this.core.modalDialog.contentManager.toolbar,
11437
11403
  size: mathml?.length
11438
11404
  };
11439
- // Remove desired null keys.
11440
- Object.keys(payload).forEach((key)=>{
11441
- if (key === "mathml_origin" || key === "editor_origin") !payload[key] ? delete payload[key] : {};
11442
- });
11443
- // Call Telemetry service to track the event.
11405
+ if (mathmlOrigin) {
11406
+ payload.mathml_origin = MathML.safeXmlDecode(mathmlOrigin);
11407
+ }
11444
11408
  try {
11445
11409
  Telemeter.telemeter.track("INSERTED_FORMULA", {
11446
11410
  ...payload
@@ -11448,15 +11412,312 @@
11448
11412
  } catch (error) {
11449
11413
  console.error("Error tracking INSERTED_FORMULA", error);
11450
11414
  }
11451
- /* Due to PLUGINS-1329, we add the onChange event to the CK4 insertFormula.
11452
- We probably should add it here as well, but we should look further into how */ // this.editorObject.fire('change');
11453
- // Remove temporal image of inserted formula
11454
11415
  this.core.editionProperties.temporalImage = null;
11455
11416
  return returnObject;
11456
11417
  }
11418
+ handleLatexInsertion(returnObject, windowTarget, mathml) {
11419
+ returnObject.latex = Latex.getLatexFromMathML(mathml);
11420
+ returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
11421
+ const { latexRange } = this.core.editionProperties;
11422
+ // When latexRange exists (meaning the whole LaTeX was selected or the editor was opened),
11423
+ // find the node contaning the LaTeX and replace it fully.
11424
+ if (latexRange) {
11425
+ const startNode = this.findText(latexRange.startContainer);
11426
+ const endNode = this.findText(latexRange.endContainer);
11427
+ // If nodes found, use standard replacement.
11428
+ if (startNode && endNode) {
11429
+ this.replaceLatexWithNodes(startNode, endNode, latexRange, returnObject.latex);
11430
+ return;
11431
+ }
11432
+ }
11433
+ this.replaceLatexUsingModelSearch(returnObject.latex);
11434
+ }
11435
+ handleMathmlInsertion(returnObject, windowTarget, mathml) {
11436
+ const mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml;
11437
+ try {
11438
+ const modelElement = this.insertMathml(mathml);
11439
+ const viewElement = this.editorObject.editing.mapper.toViewElement(modelElement);
11440
+ returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(viewElement, windowTarget.document);
11441
+ } catch (error) {
11442
+ if (error.toString().includes("Cannot read property 'parent' of undefined")) {
11443
+ this.core.modalDialog.cancelAction();
11444
+ }
11445
+ }
11446
+ return mathmlOrigin;
11447
+ }
11457
11448
  /**
11458
- * Function called when the content submits an action.
11459
- */ notifyWindowClosed() {
11449
+ * Gets selection attributes excluding track changes tags.
11450
+ */ getCleanSelectionAttributes() {
11451
+ const attributes = {};
11452
+ for (const [key, value] of this.editorObject.model.document.selection.getAttributes()){
11453
+ if (!key.startsWith("suggestion:") && !key.startsWith("comment:")) {
11454
+ attributes[key] = value;
11455
+ }
11456
+ }
11457
+ return attributes;
11458
+ }
11459
+ /**
11460
+ * Searches for the original LaTeX in the model and replaces it.
11461
+ * Fallback when findText() cannot locate DOM nodes (like when there are track changes modifications).
11462
+ */ replaceLatexUsingModelSearch(newLatex) {
11463
+ const foundRange = this.findLatexBlockNearSelection();
11464
+ if (foundRange) {
11465
+ this.editorObject.model.change((writer)=>writer.setSelection(foundRange));
11466
+ this.replaceRangeWithLatex(newLatex);
11467
+ } else {
11468
+ // Insert at current position as a last resort.
11469
+ this.editorObject.model.change((writer)=>{
11470
+ const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
11471
+ this.editorObject.model.insertContent(newLatexText);
11472
+ });
11473
+ }
11474
+ this.core.editionProperties.extractedLatex = null;
11475
+ }
11476
+ /**
11477
+ * Checks if a text proxy has a track changes deletion marker.
11478
+ */ isDeletedText(text) {
11479
+ for (const [key, value] of text.getAttributes()){
11480
+ if (key.startsWith("suggestion:") && value === "deletion") {
11481
+ return true;
11482
+ }
11483
+ }
11484
+ return false;
11485
+ }
11486
+ /**
11487
+ * Finds a LaTeX block ($$...$$) near the current selection.
11488
+ * Handles track changes by considering the "accepted" version of text.
11489
+ */ findLatexBlockNearSelection() {
11490
+ const position = this.editorObject.model.document.selection.getFirstPosition();
11491
+ if (!position?.parent) {
11492
+ return;
11493
+ }
11494
+ // Build LaTeX with track changes accepted suggestions, if any.
11495
+ const { textParts, acceptedText } = this.collectTextParts(position.parent);
11496
+ if (!acceptedText.includes("$$")) {
11497
+ return;
11498
+ }
11499
+ // To handle multiple LaTeX on same line.
11500
+ const targetLatex = this.core.editionProperties.extractedLatex;
11501
+ const fullLatex = `$$${targetLatex}$$`;
11502
+ const startIndex = acceptedText.indexOf(fullLatex);
11503
+ if (startIndex === -1) {
11504
+ return;
11505
+ }
11506
+ const latexBoundaries = {
11507
+ start: startIndex,
11508
+ end: startIndex + fullLatex.length
11509
+ };
11510
+ return this.convertAcceptedOffsetsToModelRange(textParts, latexBoundaries);
11511
+ }
11512
+ /**
11513
+ * Collects all text fragments from a paragraph, tracking both model and accepted text positions.
11514
+ * This is necessary to handle track changes where some LaTeX may have suggestions.
11515
+ */ collectTextParts(paragraph) {
11516
+ const textParts = [];
11517
+ let acceptedTextOffset = 0;
11518
+ let acceptedText = "";
11519
+ for (const item of this.editorObject.model.createRangeIn(paragraph).getItems()){
11520
+ if (item.is("$textProxy")) {
11521
+ const isDeleted = this.isDeletedText(item);
11522
+ textParts.push({
11523
+ text: item.data,
11524
+ startOffset: item.startOffset,
11525
+ endOffset: item.startOffset + item.data.length,
11526
+ parent: item.textNode.parent,
11527
+ acceptedStart: isDeleted ? null : acceptedTextOffset,
11528
+ acceptedEnd: isDeleted ? null : acceptedTextOffset + item.data.length,
11529
+ isDeleted
11530
+ });
11531
+ if (!isDeleted) {
11532
+ acceptedText += item.data;
11533
+ acceptedTextOffset += item.data.length;
11534
+ }
11535
+ }
11536
+ }
11537
+ return {
11538
+ textParts,
11539
+ acceptedText
11540
+ };
11541
+ }
11542
+ /**
11543
+ * Converts LaTeX with track changes accepted suggestions to a CKEditor model Range.
11544
+ */ convertAcceptedOffsetsToModelRange(textParts, latexBoundaries) {
11545
+ let startPartIndex = -1, endPartIndex = -1;
11546
+ let startOffsetInPart = 0, endOffsetInPart = 0;
11547
+ // Find which text parts contain the LaTeX block boundaries
11548
+ for(let i = 0; i < textParts.length; i++){
11549
+ const part = textParts[i];
11550
+ if (part.isDeleted) continue;
11551
+ if (startPartIndex === -1 && latexBoundaries.start >= part.acceptedStart && latexBoundaries.start <= part.acceptedEnd) {
11552
+ startPartIndex = i;
11553
+ startOffsetInPart = latexBoundaries.start - part.acceptedStart;
11554
+ }
11555
+ if (latexBoundaries.end >= part.acceptedStart && latexBoundaries.end <= part.acceptedEnd) {
11556
+ endPartIndex = i;
11557
+ endOffsetInPart = latexBoundaries.end - part.acceptedStart;
11558
+ }
11559
+ }
11560
+ if (startPartIndex === -1 || endPartIndex === -1) {
11561
+ return;
11562
+ }
11563
+ // Extend range to include any consecutive deleted parts after the block.
11564
+ let finalEndIndex = endPartIndex;
11565
+ let finalEndOffset = endOffsetInPart;
11566
+ for(let i = endPartIndex + 1; i < textParts.length && textParts[i].isDeleted; i++){
11567
+ finalEndIndex = i;
11568
+ finalEndOffset = textParts[i].text.length;
11569
+ }
11570
+ const startPart = textParts[startPartIndex];
11571
+ const endPart = textParts[finalEndIndex];
11572
+ return this.editorObject.model.createRange(this.editorObject.model.createPositionAt(startPart.parent, startPart.startOffset + startOffsetInPart), this.editorObject.model.createPositionAt(endPart.parent, endPart.startOffset + finalEndOffset));
11573
+ }
11574
+ replaceRangeWithLatex(newLatex) {
11575
+ this.editorObject.model.change((writer)=>{
11576
+ this.editorObject.model.deleteContent(this.editorObject.model.document.selection);
11577
+ const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
11578
+ this.editorObject.model.insertContent(newLatexText);
11579
+ });
11580
+ }
11581
+ /**
11582
+ * Replaces the whole LaTeX in the CKEditor5 model.
11583
+ */ replaceLatexWithNodes(startNode, endNode, latexRange, newLatex) {
11584
+ this.editorObject.model.change((writer)=>{
11585
+ const startOffset = startNode.startOffset + latexRange.startOffset;
11586
+ const endOffset = endNode.startOffset + latexRange.endOffset;
11587
+ let startPosition = writer.createPositionAt(startNode.parent, startOffset);
11588
+ let endPosition = writer.createPositionAt(endNode.parent, endOffset);
11589
+ // Adjust positions when LaTeX is adjacent to a formula.
11590
+ const startContainer = latexRange.startContainer;
11591
+ if (startContainer.nodeType === Node.TEXT_NODE && startContainer.previousSibling?.nodeType === Node.ELEMENT_NODE) {
11592
+ const originalLatex = `$$${Latex.getLatexFromMathML(MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml))}$$`;
11593
+ const textData = startContainer.data.replaceAll(String.fromCodePoint(8288), "");
11594
+ const latexOffset = textData.indexOf(originalLatex);
11595
+ if (latexOffset !== -1) {
11596
+ const closingDelimiterOffset = textData.substring(latexOffset + 2).indexOf("$$") + 4;
11597
+ startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexOffset);
11598
+ endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + closingDelimiterOffset + latexOffset);
11599
+ }
11600
+ }
11601
+ writer.setSelection(writer.createRange(startPosition, endPosition));
11602
+ });
11603
+ this.replaceRangeWithLatex(newLatex);
11604
+ }
11605
+ /**
11606
+ * Inherited method from IntegrationModel.
11607
+ * Gets the MathML from a text node containing LaTeX.
11608
+ * Handles track changes by simulating "accept all changes" before conversion.
11609
+ */ getMathmlFromTextNode(textNode, caretPosition) {
11610
+ const standardResult = Latex.getLatexFromTextNode(textNode, caretPosition);
11611
+ const acceptedLatex = this.extractAcceptedLatexFromDOM(textNode, caretPosition);
11612
+ // Prioritize accepted LaTeX if it differs from standard extraction (for track changes compatibility).
11613
+ // Important node: use explicit undefined check to allow empty LaTeX strings, otherwise it would not detect $$$$ as valid LaTeX.
11614
+ const latex = acceptedLatex !== undefined && acceptedLatex !== standardResult?.latex ? acceptedLatex : standardResult?.latex;
11615
+ // Do not continue if no LaTeX found by either method.
11616
+ // This is necessary since both parameters can be independently undefined in some edge cases.
11617
+ if (latex === undefined && acceptedLatex === undefined) {
11618
+ return;
11619
+ }
11620
+ // Verify caret is inside LaTeX block for track changes edge cases.
11621
+ if (!standardResult && acceptedLatex !== undefined && !this.isCaretInsideLatexBlock(textNode, caretPosition)) {
11622
+ return;
11623
+ }
11624
+ const finalLatex = latex === undefined ? acceptedLatex : latex;
11625
+ this.storeLatexRangeWithFallback(textNode, caretPosition, finalLatex);
11626
+ return Latex.getMathMLFromLatex(finalLatex);
11627
+ }
11628
+ isCaretInsideLatexBlock(textNode, caretPosition = 0) {
11629
+ // If LaTeX is found, the caret is inside one.
11630
+ return this.extractAcceptedLatexFromDOM(textNode, caretPosition) !== undefined;
11631
+ }
11632
+ /**
11633
+ * Stores the LaTeX range for its replacement later.
11634
+ */ storeLatexRangeWithFallback(textNode, caretPosition, latex) {
11635
+ const parentTag = textNode.parentElement?.tagName?.toLowerCase();
11636
+ if (!textNode.parentElement || parentTag === "textarea") {
11637
+ return;
11638
+ }
11639
+ const latexResult = Latex.getLatexFromTextNode(textNode, caretPosition);
11640
+ if (latexResult) {
11641
+ const range = document.createRange();
11642
+ range.setStart(latexResult.startNode, latexResult.startPosition);
11643
+ range.setEnd(latexResult.endNode, latexResult.endPosition);
11644
+ this.core.editionProperties.latexRange = range;
11645
+ } else {
11646
+ this.core.editionProperties.latexRange = null;
11647
+ }
11648
+ this.core.editionProperties.extractedLatex = latex;
11649
+ }
11650
+ /**
11651
+ * Finds a container element containing a complete LaTeX block.
11652
+ * Necessary for track changes handling, to find the full LaTeX even with the suggestions.
11653
+ */ findLatexContainerElement(textNode) {
11654
+ const MAX_DEPTH = 10; // Prevent excessive loops.
11655
+ let element = textNode.parentElement;
11656
+ for(let i = 0; i < MAX_DEPTH && element; i++){
11657
+ const text = element.textContent || "";
11658
+ const openDelim = text.indexOf("$$");
11659
+ if (openDelim !== -1 && text.includes("$$", openDelim + 2)) {
11660
+ return element;
11661
+ }
11662
+ element = element.parentElement;
11663
+ }
11664
+ return null;
11665
+ }
11666
+ /**
11667
+ * Extracts LaTeX from DOM, skipping track changes deletion markers.
11668
+ */ extractAcceptedLatexFromDOM(textNode, caretPositionInNode = 0) {
11669
+ const container = this.findLatexContainerElement(textNode);
11670
+ if (!container) {
11671
+ return;
11672
+ }
11673
+ const acceptedText = this.getAcceptedTextContent(container);
11674
+ // Calculate caret offset that will be used later to find the correct LaTeX block.
11675
+ // This includes all accepted text before textNode, plus the caret position within textNode.
11676
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
11677
+ let node = walker.nextNode();
11678
+ let caretOffset = 0;
11679
+ while(node && node !== textNode){
11680
+ if (!node.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
11681
+ caretOffset += node.textContent?.length || 0;
11682
+ }
11683
+ node = walker.nextNode();
11684
+ }
11685
+ // Add the caret position within the text node, only if textNode is not deleted by Track Changes.
11686
+ if (node === textNode && !textNode.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
11687
+ caretOffset += caretPositionInNode;
11688
+ }
11689
+ // Find the LaTeX block that contains the caret.
11690
+ let nextSearchIndex = 0;
11691
+ while(nextSearchIndex < acceptedText.length){
11692
+ const openDelim = acceptedText.indexOf("$$", nextSearchIndex);
11693
+ if (openDelim === -1) {
11694
+ break;
11695
+ }
11696
+ const closeDelim = acceptedText.indexOf("$$", openDelim + 2);
11697
+ if (closeDelim === -1) {
11698
+ break;
11699
+ }
11700
+ if (caretOffset >= openDelim && caretOffset <= closeDelim + 2) {
11701
+ return acceptedText.substring(openDelim + 2, closeDelim);
11702
+ }
11703
+ nextSearchIndex = closeDelim + 2;
11704
+ }
11705
+ }
11706
+ /**
11707
+ * Recursively extracts text content, skipping track changes tags.
11708
+ */ getAcceptedTextContent(node) {
11709
+ if (node.nodeType === Node.TEXT_NODE) {
11710
+ return node.textContent || "";
11711
+ }
11712
+ if (node.nodeType === Node.ELEMENT_NODE) {
11713
+ if (node.classList?.contains("ck-suggestion-marker-deletion")) {
11714
+ return "";
11715
+ }
11716
+ return Array.from(node.childNodes).map((child)=>this.getAcceptedTextContent(child)).join("");
11717
+ }
11718
+ return "";
11719
+ }
11720
+ /** Called when the modal window is closed. */ notifyWindowClosed() {
11460
11721
  this.editorObject.editing.view.focus();
11461
11722
  }
11462
11723
  }
@@ -11534,7 +11795,7 @@
11534
11795
 
11535
11796
  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";
11536
11797
 
11537
- var version = "8.15.1";
11798
+ var version = "8.15.2";
11538
11799
  var packageInfo = {
11539
11800
  version: version};
11540
11801
 
@@ -11988,8 +12249,10 @@
11988
12249
  e.return = MathML.removeSafeXMLSemantics(previewOutput);
11989
12250
  return;
11990
12251
  }
11991
- // In save mode, convert formula images back to clean MathML
11992
- const parsedResult = Parser.endParse(output);
12252
+ // Clean track changes markers only from LaTeX content before converting to MathML.
12253
+ const latexParsedOutput = this._endParseEditModeWithTrackChangesSupport(output);
12254
+ // Convert formula images to MathML. It's important to use the save mode to prevent issues.
12255
+ const parsedResult = Parser.endParseSaveMode(latexParsedOutput);
11993
12256
  // Remove all semantic annotations (including handwritten data points)
11994
12257
  // to ensure clean, standard MathML format for storage
11995
12258
  e.return = MathML.removeSafeXMLSemantics(parsedResult);
@@ -12031,6 +12294,38 @@
12031
12294
  });
12032
12295
  }
12033
12296
  /**
12297
+ * When track changes markers are present inside a LaTeX block:
12298
+ * - The LaTeX is preserved as text (not converted to MathML) to maintain suggestions.
12299
+ * - It also prevents the issue where the suggestions were placed as MathML tags.
12300
+ *
12301
+ * When no track changes markers are inside a LaTeX block:
12302
+ * - The LaTeX is converted to MathML normally. (previous default behavior)
12303
+ *
12304
+ * This is to ensure that:
12305
+ * 1. LaTeX without suggestions gets converted to MathML for final output.
12306
+ * 2. LaTeX with pending suggestions is preserved so setData(getData()) works correctly.
12307
+ */ _endParseEditModeWithTrackChangesSupport(code) {
12308
+ if (!Configuration.get("parseModes").includes("latex")) {
12309
+ return code;
12310
+ }
12311
+ const latexBlockRegex = /\$\$([\s\S]*?)\$\$/g;
12312
+ const trackChangesRegex = /<(suggestion|comment)-(start|end)/i;
12313
+ //TODO: Validate if replace all is needed instead of just replace when it is all implemented.
12314
+ return code.replace(latexBlockRegex, (fullMatch, latexContent)=>{
12315
+ // Check if this LaTeX contains track changes markers to prevent conversion.
12316
+ if (trackChangesRegex.test(latexContent)) {
12317
+ return fullMatch;
12318
+ }
12319
+ // When LaTeX has no suggestion, it can be converted to MathML.
12320
+ const decodedLatex = Util.htmlEntitiesDecode(latexContent);
12321
+ let mathml = Util.htmlSanitize(Latex.getMathMLFromLatex(decodedLatex, true));
12322
+ if (!Configuration.get("saveHandTraces")) {
12323
+ mathml = MathML.removeAnnotation(mathml, "application/json");
12324
+ }
12325
+ return mathml;
12326
+ });
12327
+ }
12328
+ /**
12034
12329
  * Expose the WirisPlugin variable to the window
12035
12330
  */ // eslint-disable-next-line class-methods-use-this
12036
12331
  _exposeWiris() {
@@ -12057,8 +12352,21 @@
12057
12352
  // Adds custom label replacing the default 'mathml'.
12058
12353
  // Handles both singular and plural forms.
12059
12354
  trackChangesEditing.descriptionFactory.registerElementLabel("mathml", (quantity)=>(quantity > 1 ? `${quantity} ` : "") + StringManager.get(quantity > 1 ? "formulas" : "formula", integration?.getLanguage() || "en"));
12355
+ this._registerLatexTrackChangesAdapter(integration);
12060
12356
  }
12061
12357
  }
12358
+ /**
12359
+ * Register a custom adapter for handling LaTeX text changes.
12360
+ * This ensures that LaTeX formulas ($$...$$) are treated as atomic units
12361
+ * when used by the track changes feature and avoid partial edits.
12362
+ */ _registerLatexTrackChangesAdapter(integration) {
12363
+ const { editor } = this;
12364
+ editor.model.document.on("change:data", ()=>{
12365
+ if (integration) {
12366
+ integration._trackChangesEnabled = editor.commands.get("trackChanges")?.value ?? false;
12367
+ }
12368
+ });
12369
+ }
12062
12370
  }
12063
12371
 
12064
12372
  exports.CKEditor5Integration = CKEditor5Integration;