@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.
@@ -4030,6 +4030,27 @@ var translations = {
4030
4030
  if (mathML == null) {
4031
4031
  mathML = imgObject.getAttribute("alt");
4032
4032
  }
4033
+ // WARNING: This code is needed for CKEditor 5 Track Changes compatibility.
4034
+ // Preserve Track Changes attributes when converting image back to MathML.
4035
+ // This ensures change tracking information is maintained during the roundtrip conversion
4036
+ // (MathML → Image → MathML) in collaborative editing scenarios.
4037
+ const TRACK_CHANGES_ATTRIBUTE_PREFIXES = [
4038
+ "data-suggestion-",
4039
+ "data-comment-"
4040
+ ];
4041
+ const preservedAttributes = {};
4042
+ // Extract Track Changes attributes from the image element
4043
+ for (const { name: attributeName, value: attributeValue } of imgObject.attributes){
4044
+ const isTrackChangesAttribute = TRACK_CHANGES_ATTRIBUTE_PREFIXES.some((prefix)=>attributeName.startsWith(prefix));
4045
+ if (isTrackChangesAttribute) {
4046
+ preservedAttributes[attributeName] = attributeValue;
4047
+ }
4048
+ }
4049
+ // If Track Changes attributes were found, inject them into the MathML opening tag
4050
+ if (Object.keys(preservedAttributes).length > 0) {
4051
+ const attributesString = Object.keys(preservedAttributes).map((name)=>` ${name}="${Util.htmlEntities(preservedAttributes[name])}"`).join("");
4052
+ mathML = mathML.replace(/(<math)/i, `$1${attributesString}`);
4053
+ }
4033
4054
  if (convertToSafeXml) {
4034
4055
  const safeMathML = MathML.safeXmlEncode(mathML);
4035
4056
  return safeMathML;
@@ -4776,6 +4797,29 @@ var translations = {
4776
4797
  mathmlSubstring = mathmlSubstring.substring(4, mathmlSubstring.length);
4777
4798
  imgObject.setAttribute(Configuration.get("imageCustomEditorName"), mathmlSubstring);
4778
4799
  }
4800
+ // WARNING: This code is needed for CKEditor 5 Track Changes compatibility.
4801
+ // Preserve Track Changes attributes (suggestions and comments) when converting MathML to image.
4802
+ // These attributes are needed to maintain change tracking information in collaborative editing scenarios.
4803
+ // They are extracted from the input MathML and transferred to the output image element.
4804
+ const TRACK_CHANGES_ATTRIBUTE_PREFIXES = [
4805
+ "data-suggestion-",
4806
+ "data-comment-"
4807
+ ];
4808
+ const attributePattern = /([\w-]+)="([^"]*)"/g;
4809
+ let attributeMatch;
4810
+ // Parse only the opening <math> tag to extract attributes
4811
+ const mathOpeningTagEnd = mathml.indexOf(">");
4812
+ if (mathOpeningTagEnd !== -1) {
4813
+ const mathOpeningTag = mathml.substring(0, mathOpeningTagEnd);
4814
+ while((attributeMatch = attributePattern.exec(mathOpeningTag)) !== null){
4815
+ const [, attributeName, attributeValue] = attributeMatch;
4816
+ // Transfer Track Changes attributes from MathML to image
4817
+ const isTrackChangesAttribute = TRACK_CHANGES_ATTRIBUTE_PREFIXES.some((prefix)=>attributeName.startsWith(prefix));
4818
+ if (isTrackChangesAttribute) {
4819
+ imgObject.setAttribute(attributeName, Util.htmlEntitiesDecode(attributeValue));
4820
+ }
4821
+ }
4822
+ }
4779
4823
  // Performance enabled.
4780
4824
  if (Configuration.get("wirisPluginPerformance") && (Configuration.get("saveMode") === "xml" || Configuration.get("saveMode") === "safeXml")) {
4781
4825
  let result = JSON.parse(Parser.createShowImageSrc(data, language));
@@ -11251,49 +11295,68 @@ delete Array.prototype.__class__; // @codingStandardsIgnoreEnd
11251
11295
  * @param {String} mathml MathML to update old one or insert
11252
11296
  * @returns {module:engine/model/element~Element} The model element corresponding to the inserted image
11253
11297
  */ insertMathml(mathml) {
11254
- // This returns the value returned by the callback function (writer => {...})
11255
11298
  return this.editorObject.model.change((writer)=>{
11256
- const core = this.getCore();
11299
+ const { isNewElement, temporalImage } = this.getCore().editionProperties;
11257
11300
  const selection = this.editorObject.model.document.selection;
11301
+ const attributes = Object.fromEntries(selection.getAttributes());
11258
11302
  const modelElementNew = writer.createElement("mathml", {
11259
11303
  formula: mathml,
11260
- ...Object.fromEntries(selection.getAttributes())
11304
+ ...attributes
11261
11305
  });
11262
- // Obtain the DOM <span><img ... /></span> object corresponding to the formula
11263
- if (core.editionProperties.isNewElement) {
11264
- // Don't bother inserting anything at all if the MathML is empty.
11265
- if (!mathml) return;
11266
- const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
11267
- const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
11268
- this.editorObject.model.insertObject(modelElementNew, modelPosition);
11269
- // Remove selection
11270
- if (!viewSelection.isCollapsed) {
11271
- for (const range of viewSelection.getRanges()){
11272
- const modelRange = this.editorObject.editing.mapper.toModelRange(range);
11273
- const modelSelection = this.editorObject.model.createSelection(modelRange);
11274
- this.editorObject.model.deleteContent(modelSelection);
11275
- }
11276
- }
11277
- // Set carret after the formula
11278
- const position = this.editorObject.model.createPositionAfter(modelElementNew);
11279
- writer.setSelection(position);
11280
- } else {
11281
- const img = core.editionProperties.temporalImage;
11282
- const viewElement = this.editorObject.editing.view.domConverter.domToView(img).parent;
11283
- const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewElement);
11284
- // Insert the new <mathml> and remove the old one
11285
- const position = this.editorObject.model.createPositionBefore(modelElementOld);
11286
- // If the given MathML is empty, don't insert a new formula.
11287
- if (mathml) {
11288
- this.editorObject.model.insertObject(modelElementNew, position);
11289
- }
11290
- this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, 'on'));
11306
+ if (isNewElement) {
11307
+ return this.insertNewFormula(writer, mathml, modelElementNew);
11291
11308
  }
11292
- // eslint-disable-next-line consistent-return
11293
- return modelElementNew;
11309
+ return this.replaceExistingFormula(mathml, modelElementNew, temporalImage);
11294
11310
  });
11295
11311
  }
11296
11312
  /**
11313
+ * Inserts a new formula at the current selection position.
11314
+ */ insertNewFormula(writer, mathml, modelElement) {
11315
+ if (!mathml) {
11316
+ return;
11317
+ }
11318
+ const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection;
11319
+ const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition());
11320
+ this.editorObject.model.insertObject(modelElement, modelPosition);
11321
+ this.deleteViewSelection(viewSelection);
11322
+ // Set carret after the formula.
11323
+ const position = this.editorObject.model.createPositionAfter(modelElement);
11324
+ writer.setSelection(position);
11325
+ return modelElement;
11326
+ }
11327
+ deleteViewSelection(viewSelection) {
11328
+ if (viewSelection.isCollapsed) {
11329
+ return;
11330
+ }
11331
+ for (const range of viewSelection.getRanges()){
11332
+ const modelRange = this.editorObject.editing.mapper.toModelRange(range);
11333
+ const modelSelection = this.editorObject.model.createSelection(modelRange);
11334
+ this.editorObject.model.deleteContent(modelSelection);
11335
+ }
11336
+ }
11337
+ /**
11338
+ * Replaces an existing formula with updated MathML.
11339
+ */ replaceExistingFormula(mathml, modelElement, temporalImage) {
11340
+ const viewNode = this.editorObject.editing.view.domConverter.domToView(temporalImage);
11341
+ // Check if image exists in view to do standard formula editing
11342
+ if (viewNode?.parent) {
11343
+ const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewNode.parent);
11344
+ // Insert the new <mathml> and remove the old one
11345
+ const position = this.editorObject.model.createPositionBefore(modelElementOld);
11346
+ if (mathml) {
11347
+ this.editorObject.model.insertObject(modelElement, position);
11348
+ }
11349
+ this.editorObject.model.deleteContent(this.editorObject.model.createSelection(modelElementOld, "on"));
11350
+ return modelElement;
11351
+ }
11352
+ // Otherwise it's LaTeX editing, so we insert at current selection
11353
+ if (!mathml) {
11354
+ return;
11355
+ }
11356
+ this.editorObject.model.insertContent(modelElement);
11357
+ return modelElement;
11358
+ }
11359
+ /**
11297
11360
  * Finds the text node corresponding to given DOM text element.
11298
11361
  * @param {element} viewElement Element to find corresponding text node of.
11299
11362
  * @returns {module:engine/model/text~Text|undefined} Text node corresponding to the given element or undefined if it doesn't exist.
@@ -11320,73 +11383,26 @@ delete Array.prototype.__class__; // @codingStandardsIgnoreEnd
11320
11383
  }
11321
11384
  }
11322
11385
  }
11323
- /** @inheritdoc */ insertFormula(focusElement, windowTarget, mathml, wirisProperties) {
11386
+ /** @inheritdoc */ insertFormula(_focusElement, windowTarget, mathml, _wirisProperties) {
11324
11387
  // eslint-disable-line no-unused-vars
11325
11388
  const returnObject = {};
11326
11389
  let mathmlOrigin;
11327
11390
  if (!mathml) {
11328
11391
  this.insertMathml("");
11329
11392
  } else if (this.core.editMode === "latex") {
11330
- returnObject.latex = Latex.getLatexFromMathML(mathml);
11331
- returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
11332
- this.editorObject.model.change((writer)=>{
11333
- const { latexRange } = this.core.editionProperties;
11334
- const startNode = this.findText(latexRange.startContainer);
11335
- const endNode = this.findText(latexRange.endContainer);
11336
- let startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexRange.startOffset);
11337
- let endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + latexRange.endOffset);
11338
- let range = writer.createRange(startPosition, endPosition);
11339
- // When Latex is next to image/formula.
11340
- if (latexRange.startContainer.nodeType === 3 && latexRange.startContainer.previousSibling?.nodeType === 1) {
11341
- // Get the position of the latex to be replaced.
11342
- const latexEdited = `$$${Latex.getLatexFromMathML(MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml))}$$`;
11343
- let data = latexRange.startContainer.data;
11344
- // Remove invisible characters.
11345
- data = data.replaceAll(String.fromCharCode(8288), "");
11346
- // Get to the start of the latex we are editing.
11347
- const offset = data.indexOf(latexEdited);
11348
- const dataOffset = data.substring(offset);
11349
- const second$ = dataOffset.substring(2).indexOf("$$") + 4;
11350
- const substring = dataOffset.substr(0, second$);
11351
- data = data.replace(substring, "");
11352
- if (!data) {
11353
- startPosition = writer.createPositionBefore(startNode);
11354
- range = startNode;
11355
- } else {
11356
- startPosition = startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + offset);
11357
- endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + second$ + offset);
11358
- range = writer.createRange(startPosition, endPosition);
11359
- }
11360
- }
11361
- const modelSelection = this.editorObject.model.createSelection(range);
11362
- this.editorObject.model.deleteContent(modelSelection);
11363
- writer.insertText(`$$${returnObject.latex}$$`, startNode.getAttributes(), startPosition);
11364
- });
11393
+ this.handleLatexInsertion(returnObject, windowTarget, mathml);
11365
11394
  } else {
11366
- mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml;
11367
- try {
11368
- returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(this.editorObject.editing.mapper.toViewElement(this.insertMathml(mathml)), windowTarget.document);
11369
- } catch (e) {
11370
- const x = e.toString();
11371
- if (x.includes("CKEditorError: Cannot read property 'parent' of undefined")) {
11372
- this.core.modalDialog.cancelAction();
11373
- }
11374
- }
11395
+ mathmlOrigin = this.handleMathmlInsertion(returnObject, windowTarget, mathml);
11375
11396
  }
11376
- // Build the telemeter payload separated to delete null/undefined entries.
11377
11397
  const payload = {
11378
- mathml_origin: mathmlOrigin ? MathML.safeXmlDecode(mathmlOrigin) : mathmlOrigin,
11379
- mathml: mathml ? MathML.safeXmlDecode(mathml) : mathml,
11398
+ mathml: mathml ? MathML.safeXmlDecode(mathml) : undefined,
11380
11399
  elapsed_time: Date.now() - this.core.editionProperties.editionStartTime,
11381
- editor_origin: null,
11382
11400
  toolbar: this.core.modalDialog.contentManager.toolbar,
11383
11401
  size: mathml?.length
11384
11402
  };
11385
- // Remove desired null keys.
11386
- Object.keys(payload).forEach((key)=>{
11387
- if (key === "mathml_origin" || key === "editor_origin") !payload[key] ? delete payload[key] : {};
11388
- });
11389
- // Call Telemetry service to track the event.
11403
+ if (mathmlOrigin) {
11404
+ payload.mathml_origin = MathML.safeXmlDecode(mathmlOrigin);
11405
+ }
11390
11406
  try {
11391
11407
  Telemeter.telemeter.track("INSERTED_FORMULA", {
11392
11408
  ...payload
@@ -11394,15 +11410,312 @@ delete Array.prototype.__class__; // @codingStandardsIgnoreEnd
11394
11410
  } catch (error) {
11395
11411
  console.error("Error tracking INSERTED_FORMULA", error);
11396
11412
  }
11397
- /* Due to PLUGINS-1329, we add the onChange event to the CK4 insertFormula.
11398
- We probably should add it here as well, but we should look further into how */ // this.editorObject.fire('change');
11399
- // Remove temporal image of inserted formula
11400
11413
  this.core.editionProperties.temporalImage = null;
11401
11414
  return returnObject;
11402
11415
  }
11416
+ handleLatexInsertion(returnObject, windowTarget, mathml) {
11417
+ returnObject.latex = Latex.getLatexFromMathML(mathml);
11418
+ returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`);
11419
+ const { latexRange } = this.core.editionProperties;
11420
+ // When latexRange exists (meaning the whole LaTeX was selected or the editor was opened),
11421
+ // find the node contaning the LaTeX and replace it fully.
11422
+ if (latexRange) {
11423
+ const startNode = this.findText(latexRange.startContainer);
11424
+ const endNode = this.findText(latexRange.endContainer);
11425
+ // If nodes found, use standard replacement.
11426
+ if (startNode && endNode) {
11427
+ this.replaceLatexWithNodes(startNode, endNode, latexRange, returnObject.latex);
11428
+ return;
11429
+ }
11430
+ }
11431
+ this.replaceLatexUsingModelSearch(returnObject.latex);
11432
+ }
11433
+ handleMathmlInsertion(returnObject, windowTarget, mathml) {
11434
+ const mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml;
11435
+ try {
11436
+ const modelElement = this.insertMathml(mathml);
11437
+ const viewElement = this.editorObject.editing.mapper.toViewElement(modelElement);
11438
+ returnObject.node = this.editorObject.editing.view.domConverter.viewToDom(viewElement, windowTarget.document);
11439
+ } catch (error) {
11440
+ if (error.toString().includes("Cannot read property 'parent' of undefined")) {
11441
+ this.core.modalDialog.cancelAction();
11442
+ }
11443
+ }
11444
+ return mathmlOrigin;
11445
+ }
11403
11446
  /**
11404
- * Function called when the content submits an action.
11405
- */ notifyWindowClosed() {
11447
+ * Gets selection attributes excluding track changes tags.
11448
+ */ getCleanSelectionAttributes() {
11449
+ const attributes = {};
11450
+ for (const [key, value] of this.editorObject.model.document.selection.getAttributes()){
11451
+ if (!key.startsWith("suggestion:") && !key.startsWith("comment:")) {
11452
+ attributes[key] = value;
11453
+ }
11454
+ }
11455
+ return attributes;
11456
+ }
11457
+ /**
11458
+ * Searches for the original LaTeX in the model and replaces it.
11459
+ * Fallback when findText() cannot locate DOM nodes (like when there are track changes modifications).
11460
+ */ replaceLatexUsingModelSearch(newLatex) {
11461
+ const foundRange = this.findLatexBlockNearSelection();
11462
+ if (foundRange) {
11463
+ this.editorObject.model.change((writer)=>writer.setSelection(foundRange));
11464
+ this.replaceRangeWithLatex(newLatex);
11465
+ } else {
11466
+ // Insert at current position as a last resort.
11467
+ this.editorObject.model.change((writer)=>{
11468
+ const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
11469
+ this.editorObject.model.insertContent(newLatexText);
11470
+ });
11471
+ }
11472
+ this.core.editionProperties.extractedLatex = null;
11473
+ }
11474
+ /**
11475
+ * Checks if a text proxy has a track changes deletion marker.
11476
+ */ isDeletedText(text) {
11477
+ for (const [key, value] of text.getAttributes()){
11478
+ if (key.startsWith("suggestion:") && value === "deletion") {
11479
+ return true;
11480
+ }
11481
+ }
11482
+ return false;
11483
+ }
11484
+ /**
11485
+ * Finds a LaTeX block ($$...$$) near the current selection.
11486
+ * Handles track changes by considering the "accepted" version of text.
11487
+ */ findLatexBlockNearSelection() {
11488
+ const position = this.editorObject.model.document.selection.getFirstPosition();
11489
+ if (!position?.parent) {
11490
+ return;
11491
+ }
11492
+ // Build LaTeX with track changes accepted suggestions, if any.
11493
+ const { textParts, acceptedText } = this.collectTextParts(position.parent);
11494
+ if (!acceptedText.includes("$$")) {
11495
+ return;
11496
+ }
11497
+ // To handle multiple LaTeX on same line.
11498
+ const targetLatex = this.core.editionProperties.extractedLatex;
11499
+ const fullLatex = `$$${targetLatex}$$`;
11500
+ const startIndex = acceptedText.indexOf(fullLatex);
11501
+ if (startIndex === -1) {
11502
+ return;
11503
+ }
11504
+ const latexBoundaries = {
11505
+ start: startIndex,
11506
+ end: startIndex + fullLatex.length
11507
+ };
11508
+ return this.convertAcceptedOffsetsToModelRange(textParts, latexBoundaries);
11509
+ }
11510
+ /**
11511
+ * Collects all text fragments from a paragraph, tracking both model and accepted text positions.
11512
+ * This is necessary to handle track changes where some LaTeX may have suggestions.
11513
+ */ collectTextParts(paragraph) {
11514
+ const textParts = [];
11515
+ let acceptedTextOffset = 0;
11516
+ let acceptedText = "";
11517
+ for (const item of this.editorObject.model.createRangeIn(paragraph).getItems()){
11518
+ if (item.is("$textProxy")) {
11519
+ const isDeleted = this.isDeletedText(item);
11520
+ textParts.push({
11521
+ text: item.data,
11522
+ startOffset: item.startOffset,
11523
+ endOffset: item.startOffset + item.data.length,
11524
+ parent: item.textNode.parent,
11525
+ acceptedStart: isDeleted ? null : acceptedTextOffset,
11526
+ acceptedEnd: isDeleted ? null : acceptedTextOffset + item.data.length,
11527
+ isDeleted
11528
+ });
11529
+ if (!isDeleted) {
11530
+ acceptedText += item.data;
11531
+ acceptedTextOffset += item.data.length;
11532
+ }
11533
+ }
11534
+ }
11535
+ return {
11536
+ textParts,
11537
+ acceptedText
11538
+ };
11539
+ }
11540
+ /**
11541
+ * Converts LaTeX with track changes accepted suggestions to a CKEditor model Range.
11542
+ */ convertAcceptedOffsetsToModelRange(textParts, latexBoundaries) {
11543
+ let startPartIndex = -1, endPartIndex = -1;
11544
+ let startOffsetInPart = 0, endOffsetInPart = 0;
11545
+ // Find which text parts contain the LaTeX block boundaries
11546
+ for(let i = 0; i < textParts.length; i++){
11547
+ const part = textParts[i];
11548
+ if (part.isDeleted) continue;
11549
+ if (startPartIndex === -1 && latexBoundaries.start >= part.acceptedStart && latexBoundaries.start <= part.acceptedEnd) {
11550
+ startPartIndex = i;
11551
+ startOffsetInPart = latexBoundaries.start - part.acceptedStart;
11552
+ }
11553
+ if (latexBoundaries.end >= part.acceptedStart && latexBoundaries.end <= part.acceptedEnd) {
11554
+ endPartIndex = i;
11555
+ endOffsetInPart = latexBoundaries.end - part.acceptedStart;
11556
+ }
11557
+ }
11558
+ if (startPartIndex === -1 || endPartIndex === -1) {
11559
+ return;
11560
+ }
11561
+ // Extend range to include any consecutive deleted parts after the block.
11562
+ let finalEndIndex = endPartIndex;
11563
+ let finalEndOffset = endOffsetInPart;
11564
+ for(let i = endPartIndex + 1; i < textParts.length && textParts[i].isDeleted; i++){
11565
+ finalEndIndex = i;
11566
+ finalEndOffset = textParts[i].text.length;
11567
+ }
11568
+ const startPart = textParts[startPartIndex];
11569
+ const endPart = textParts[finalEndIndex];
11570
+ return this.editorObject.model.createRange(this.editorObject.model.createPositionAt(startPart.parent, startPart.startOffset + startOffsetInPart), this.editorObject.model.createPositionAt(endPart.parent, endPart.startOffset + finalEndOffset));
11571
+ }
11572
+ replaceRangeWithLatex(newLatex) {
11573
+ this.editorObject.model.change((writer)=>{
11574
+ this.editorObject.model.deleteContent(this.editorObject.model.document.selection);
11575
+ const newLatexText = writer.createText(`$$${newLatex}$$`, this.getCleanSelectionAttributes());
11576
+ this.editorObject.model.insertContent(newLatexText);
11577
+ });
11578
+ }
11579
+ /**
11580
+ * Replaces the whole LaTeX in the CKEditor5 model.
11581
+ */ replaceLatexWithNodes(startNode, endNode, latexRange, newLatex) {
11582
+ this.editorObject.model.change((writer)=>{
11583
+ const startOffset = startNode.startOffset + latexRange.startOffset;
11584
+ const endOffset = endNode.startOffset + latexRange.endOffset;
11585
+ let startPosition = writer.createPositionAt(startNode.parent, startOffset);
11586
+ let endPosition = writer.createPositionAt(endNode.parent, endOffset);
11587
+ // Adjust positions when LaTeX is adjacent to a formula.
11588
+ const startContainer = latexRange.startContainer;
11589
+ if (startContainer.nodeType === Node.TEXT_NODE && startContainer.previousSibling?.nodeType === Node.ELEMENT_NODE) {
11590
+ const originalLatex = `$$${Latex.getLatexFromMathML(MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml))}$$`;
11591
+ const textData = startContainer.data.replaceAll(String.fromCodePoint(8288), "");
11592
+ const latexOffset = textData.indexOf(originalLatex);
11593
+ if (latexOffset !== -1) {
11594
+ const closingDelimiterOffset = textData.substring(latexOffset + 2).indexOf("$$") + 4;
11595
+ startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexOffset);
11596
+ endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + closingDelimiterOffset + latexOffset);
11597
+ }
11598
+ }
11599
+ writer.setSelection(writer.createRange(startPosition, endPosition));
11600
+ });
11601
+ this.replaceRangeWithLatex(newLatex);
11602
+ }
11603
+ /**
11604
+ * Inherited method from IntegrationModel.
11605
+ * Gets the MathML from a text node containing LaTeX.
11606
+ * Handles track changes by simulating "accept all changes" before conversion.
11607
+ */ getMathmlFromTextNode(textNode, caretPosition) {
11608
+ const standardResult = Latex.getLatexFromTextNode(textNode, caretPosition);
11609
+ const acceptedLatex = this.extractAcceptedLatexFromDOM(textNode, caretPosition);
11610
+ // Prioritize accepted LaTeX if it differs from standard extraction (for track changes compatibility).
11611
+ // Important node: use explicit undefined check to allow empty LaTeX strings, otherwise it would not detect $$$$ as valid LaTeX.
11612
+ const latex = acceptedLatex !== undefined && acceptedLatex !== standardResult?.latex ? acceptedLatex : standardResult?.latex;
11613
+ // Do not continue if no LaTeX found by either method.
11614
+ // This is necessary since both parameters can be independently undefined in some edge cases.
11615
+ if (latex === undefined && acceptedLatex === undefined) {
11616
+ return;
11617
+ }
11618
+ // Verify caret is inside LaTeX block for track changes edge cases.
11619
+ if (!standardResult && acceptedLatex !== undefined && !this.isCaretInsideLatexBlock(textNode, caretPosition)) {
11620
+ return;
11621
+ }
11622
+ const finalLatex = latex === undefined ? acceptedLatex : latex;
11623
+ this.storeLatexRangeWithFallback(textNode, caretPosition, finalLatex);
11624
+ return Latex.getMathMLFromLatex(finalLatex);
11625
+ }
11626
+ isCaretInsideLatexBlock(textNode, caretPosition = 0) {
11627
+ // If LaTeX is found, the caret is inside one.
11628
+ return this.extractAcceptedLatexFromDOM(textNode, caretPosition) !== undefined;
11629
+ }
11630
+ /**
11631
+ * Stores the LaTeX range for its replacement later.
11632
+ */ storeLatexRangeWithFallback(textNode, caretPosition, latex) {
11633
+ const parentTag = textNode.parentElement?.tagName?.toLowerCase();
11634
+ if (!textNode.parentElement || parentTag === "textarea") {
11635
+ return;
11636
+ }
11637
+ const latexResult = Latex.getLatexFromTextNode(textNode, caretPosition);
11638
+ if (latexResult) {
11639
+ const range = document.createRange();
11640
+ range.setStart(latexResult.startNode, latexResult.startPosition);
11641
+ range.setEnd(latexResult.endNode, latexResult.endPosition);
11642
+ this.core.editionProperties.latexRange = range;
11643
+ } else {
11644
+ this.core.editionProperties.latexRange = null;
11645
+ }
11646
+ this.core.editionProperties.extractedLatex = latex;
11647
+ }
11648
+ /**
11649
+ * Finds a container element containing a complete LaTeX block.
11650
+ * Necessary for track changes handling, to find the full LaTeX even with the suggestions.
11651
+ */ findLatexContainerElement(textNode) {
11652
+ const MAX_DEPTH = 10; // Prevent excessive loops.
11653
+ let element = textNode.parentElement;
11654
+ for(let i = 0; i < MAX_DEPTH && element; i++){
11655
+ const text = element.textContent || "";
11656
+ const openDelim = text.indexOf("$$");
11657
+ if (openDelim !== -1 && text.includes("$$", openDelim + 2)) {
11658
+ return element;
11659
+ }
11660
+ element = element.parentElement;
11661
+ }
11662
+ return null;
11663
+ }
11664
+ /**
11665
+ * Extracts LaTeX from DOM, skipping track changes deletion markers.
11666
+ */ extractAcceptedLatexFromDOM(textNode, caretPositionInNode = 0) {
11667
+ const container = this.findLatexContainerElement(textNode);
11668
+ if (!container) {
11669
+ return;
11670
+ }
11671
+ const acceptedText = this.getAcceptedTextContent(container);
11672
+ // Calculate caret offset that will be used later to find the correct LaTeX block.
11673
+ // This includes all accepted text before textNode, plus the caret position within textNode.
11674
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
11675
+ let node = walker.nextNode();
11676
+ let caretOffset = 0;
11677
+ while(node && node !== textNode){
11678
+ if (!node.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
11679
+ caretOffset += node.textContent?.length || 0;
11680
+ }
11681
+ node = walker.nextNode();
11682
+ }
11683
+ // Add the caret position within the text node, only if textNode is not deleted by Track Changes.
11684
+ if (node === textNode && !textNode.parentElement?.classList?.contains("ck-suggestion-marker-deletion")) {
11685
+ caretOffset += caretPositionInNode;
11686
+ }
11687
+ // Find the LaTeX block that contains the caret.
11688
+ let nextSearchIndex = 0;
11689
+ while(nextSearchIndex < acceptedText.length){
11690
+ const openDelim = acceptedText.indexOf("$$", nextSearchIndex);
11691
+ if (openDelim === -1) {
11692
+ break;
11693
+ }
11694
+ const closeDelim = acceptedText.indexOf("$$", openDelim + 2);
11695
+ if (closeDelim === -1) {
11696
+ break;
11697
+ }
11698
+ if (caretOffset >= openDelim && caretOffset <= closeDelim + 2) {
11699
+ return acceptedText.substring(openDelim + 2, closeDelim);
11700
+ }
11701
+ nextSearchIndex = closeDelim + 2;
11702
+ }
11703
+ }
11704
+ /**
11705
+ * Recursively extracts text content, skipping track changes tags.
11706
+ */ getAcceptedTextContent(node) {
11707
+ if (node.nodeType === Node.TEXT_NODE) {
11708
+ return node.textContent || "";
11709
+ }
11710
+ if (node.nodeType === Node.ELEMENT_NODE) {
11711
+ if (node.classList?.contains("ck-suggestion-marker-deletion")) {
11712
+ return "";
11713
+ }
11714
+ return Array.from(node.childNodes).map((child)=>this.getAcceptedTextContent(child)).join("");
11715
+ }
11716
+ return "";
11717
+ }
11718
+ /** Called when the modal window is closed. */ notifyWindowClosed() {
11406
11719
  this.editorObject.editing.view.focus();
11407
11720
  }
11408
11721
  }
@@ -11480,7 +11793,7 @@ var mathIcon = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Generator: Adob
11480
11793
 
11481
11794
  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";
11482
11795
 
11483
- var version = "8.15.0";
11796
+ var version = "8.15.2";
11484
11797
  var packageInfo = {
11485
11798
  version: version};
11486
11799
 
@@ -11822,7 +12135,8 @@ class MathType extends Plugin {
11822
12135
  imgElement = htmlDataProcessor.toView(htmlContent).getChild(0);
11823
12136
  } else if (formula) {
11824
12137
  const mathString = formula.replaceAll('ref="<"', 'ref="&lt;"');
11825
- const imgHtml = Parser.initParse(mathString, integration.getLanguage());
12138
+ const lang = integration?.getLanguage() || "en"; // Safe fallback to 'en' in case integration is undefined.
12139
+ const imgHtml = Parser.initParse(mathString, lang);
11826
12140
  imgElement = htmlDataProcessor.toView(imgHtml).getChild(0);
11827
12141
  // Add HTML element (<img>) to model
11828
12142
  viewWriter.setAttribute("htmlContent", imgHtml, modelItem);
@@ -11880,13 +12194,65 @@ class MathType extends Plugin {
11880
12194
  editor.editing.mapper.on("viewToModelPosition", viewToModelPositionOutsideModelElement(editor.model, (viewElement)=>viewElement.hasClass("ck-math-widget")));
11881
12195
  // Keep a reference to the original get and set function.
11882
12196
  const { get, set } = editor.data;
12197
+ // Listen to the preview command execution to set a flag in localStorage.
12198
+ // This flag will be used in the getData() to prevent converting formulas while generating the preview.
12199
+ // This is necessary because the preview command uses editor.getData() multiple times internally.
12200
+ const previewCommand = editor.commands.get("previewFinalContent");
12201
+ if (previewCommand) {
12202
+ this.listenTo(previewCommand, "execute", ()=>{
12203
+ localStorage.setItem("isGeneratingPreview", true);
12204
+ setTimeout(()=>{
12205
+ localStorage.setItem("isGeneratingPreview", false);
12206
+ }, 1000);
12207
+ }, {
12208
+ priority: "high"
12209
+ });
12210
+ }
11883
12211
  /**
11884
- * Hack to transform $$latex$$ into <math> in editor.getData()'s output.
12212
+ * Listener for getData() that handles Track Changes and semantics cleanup.
12213
+ *
12214
+ * This listener intercepts the getData() call and processes the output differently
12215
+ * depending on whether we're generating a preview or performing a save operation:
12216
+ *
12217
+ * - Preview Mode: Removes deleted formulas (marked with Track Changes deletion attributes)
12218
+ * while preserving formula images for visual representation.
12219
+ * - Save Mode: Converts formulas to clean MathML by removing semantic annotations
12220
+ * and handwritten data, ensuring clean storage format.
11885
12221
  */ editor.data.on("get", (e)=>{
11886
12222
  const output = e.return;
11887
- const parsedResult = Parser.endParse(output);
11888
- // Cleans all the semantics tag for safexml
11889
- // including the handwritten data points
12223
+ // Check if we're in preview mode (flag set by previewFinalContent command)
12224
+ const isGeneratingPreview = localStorage.getItem("isGeneratingPreview");
12225
+ if (isGeneratingPreview === "true") {
12226
+ // Wrap a <span> around all img elements to preserve Track Changes visibility for formulas.
12227
+ // Span must contain all the img attributes to avoid render issues.
12228
+ const attributesToPreserve = [
12229
+ "data-suggestion-",
12230
+ "data-comment-"
12231
+ ];
12232
+ const previewOutput = output.replace(/<img([^>]*class="Wirisformula"[^>]*)>/g, (match, attributes)=>{
12233
+ // Extract Track Changes attributes
12234
+ const trackChangesAttrs = [];
12235
+ attributesToPreserve.forEach((prefix)=>{
12236
+ const regex = new RegExp(`(${prefix}[^=]*="[^"]*")`, "g");
12237
+ let attrMatch;
12238
+ while((attrMatch = regex.exec(attributes)) !== null){
12239
+ trackChangesAttrs.push(attrMatch[1]);
12240
+ }
12241
+ });
12242
+ const spanAttrs = trackChangesAttrs.length > 0 ? ` ${trackChangesAttrs.join(" ")}` : "";
12243
+ return `<span${spanAttrs}>${match}</span>`;
12244
+ });
12245
+ // Cleans all the semantics tag for safexml
12246
+ // including the handwritten data points
12247
+ e.return = MathML.removeSafeXMLSemantics(previewOutput);
12248
+ return;
12249
+ }
12250
+ // Clean track changes markers only from LaTeX content before converting to MathML.
12251
+ const latexParsedOutput = this._endParseEditModeWithTrackChangesSupport(output);
12252
+ // Convert formula images to MathML. It's important to use the save mode to prevent issues.
12253
+ const parsedResult = Parser.endParseSaveMode(latexParsedOutput);
12254
+ // Remove all semantic annotations (including handwritten data points)
12255
+ // to ensure clean, standard MathML format for storage
11890
12256
  e.return = MathML.removeSafeXMLSemantics(parsedResult);
11891
12257
  }, {
11892
12258
  priority: "low"
@@ -11926,6 +12292,38 @@ class MathType extends Plugin {
11926
12292
  });
11927
12293
  }
11928
12294
  /**
12295
+ * When track changes markers are present inside a LaTeX block:
12296
+ * - The LaTeX is preserved as text (not converted to MathML) to maintain suggestions.
12297
+ * - It also prevents the issue where the suggestions were placed as MathML tags.
12298
+ *
12299
+ * When no track changes markers are inside a LaTeX block:
12300
+ * - The LaTeX is converted to MathML normally. (previous default behavior)
12301
+ *
12302
+ * This is to ensure that:
12303
+ * 1. LaTeX without suggestions gets converted to MathML for final output.
12304
+ * 2. LaTeX with pending suggestions is preserved so setData(getData()) works correctly.
12305
+ */ _endParseEditModeWithTrackChangesSupport(code) {
12306
+ if (!Configuration.get("parseModes").includes("latex")) {
12307
+ return code;
12308
+ }
12309
+ const latexBlockRegex = /\$\$([\s\S]*?)\$\$/g;
12310
+ const trackChangesRegex = /<(suggestion|comment)-(start|end)/i;
12311
+ //TODO: Validate if replace all is needed instead of just replace when it is all implemented.
12312
+ return code.replace(latexBlockRegex, (fullMatch, latexContent)=>{
12313
+ // Check if this LaTeX contains track changes markers to prevent conversion.
12314
+ if (trackChangesRegex.test(latexContent)) {
12315
+ return fullMatch;
12316
+ }
12317
+ // When LaTeX has no suggestion, it can be converted to MathML.
12318
+ const decodedLatex = Util.htmlEntitiesDecode(latexContent);
12319
+ let mathml = Util.htmlSanitize(Latex.getMathMLFromLatex(decodedLatex, true));
12320
+ if (!Configuration.get("saveHandTraces")) {
12321
+ mathml = MathML.removeAnnotation(mathml, "application/json");
12322
+ }
12323
+ return mathml;
12324
+ });
12325
+ }
12326
+ /**
11929
12327
  * Expose the WirisPlugin variable to the window
11930
12328
  */ // eslint-disable-next-line class-methods-use-this
11931
12329
  _exposeWiris() {
@@ -11951,9 +12349,22 @@ class MathType extends Plugin {
11951
12349
  trackChangesEditing.enableCommand("ChemType");
11952
12350
  // Adds custom label replacing the default 'mathml'.
11953
12351
  // Handles both singular and plural forms.
11954
- trackChangesEditing.descriptionFactory.registerElementLabel("mathml", (quantity)=>(quantity > 1 ? quantity + ' ' : '') + StringManager.get(quantity > 1 ? "formulas" : "formula", integration.getLanguage()));
12352
+ trackChangesEditing.descriptionFactory.registerElementLabel("mathml", (quantity)=>(quantity > 1 ? `${quantity} ` : "") + StringManager.get(quantity > 1 ? "formulas" : "formula", integration?.getLanguage() || "en"));
12353
+ this._registerLatexTrackChangesAdapter(integration);
11955
12354
  }
11956
12355
  }
12356
+ /**
12357
+ * Register a custom adapter for handling LaTeX text changes.
12358
+ * This ensures that LaTeX formulas ($$...$$) are treated as atomic units
12359
+ * when used by the track changes feature and avoid partial edits.
12360
+ */ _registerLatexTrackChangesAdapter(integration) {
12361
+ const { editor } = this;
12362
+ editor.model.document.on("change:data", ()=>{
12363
+ if (integration) {
12364
+ integration._trackChangesEnabled = editor.commands.get("trackChanges")?.value ?? false;
12365
+ }
12366
+ });
12367
+ }
11957
12368
  }
11958
12369
 
11959
12370
  export { CKEditor5Integration, ChemTypeCommand, MathTypeCommand, MathType as default };