roosterjs 9.34.0 → 9.35.0

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.
@@ -2682,7 +2682,9 @@ var roosterjs_content_model_dom_1 = __webpack_require__(/*! roosterjs-content-mo
2682
2682
  */
2683
2683
  function toggleModelBlockQuote(model, formatLtr, formatRtl) {
2684
2684
  (0, splitSelectedParagraphByBr_1.splitSelectedParagraphByBr)(model);
2685
- var paragraphOfQuote = (0, roosterjs_content_model_dom_1.getOperationalBlocks)(model, ['FormatContainer', 'ListItem'], ['TableCell'], true /*deepFirst*/);
2685
+ var paragraphOfQuote = (0, roosterjs_content_model_dom_1.getOperationalBlocks)(model, ['FormatContainer', 'ListItem'], ['TableCell'], true /*deepFirst*/, function (block) {
2686
+ return block.blockGroupType == 'FormatContainer' ? block.tagName == 'blockquote' : true;
2687
+ });
2686
2688
  if (areAllBlockQuotes(paragraphOfQuote)) {
2687
2689
  // All selections are already in quote, we need to unquote them
2688
2690
  paragraphOfQuote.forEach(function (_a) {
@@ -3588,6 +3590,9 @@ var splitTextSegment_1 = __webpack_require__(/*! ../../publicApi/segment/splitTe
3588
3590
  * @returns If a link is promoted, return this segment. Otherwise return null
3589
3591
  */
3590
3592
  function promoteLink(segment, paragraph, autoLinkOptions) {
3593
+ if (segment.link) {
3594
+ return null;
3595
+ }
3591
3596
  var link = segment.text.split(' ').pop();
3592
3597
  var url = link === null || link === void 0 ? void 0 : link.trim();
3593
3598
  var linkUrl = undefined;
@@ -9366,6 +9371,8 @@ var formatContentModel = function (core, formatter, options, domToModelOptions)
9366
9371
  data: (_b = options === null || options === void 0 ? void 0 : options.getChangeData) === null || _b === void 0 ? void 0 : _b.call(options),
9367
9372
  formatApiName: options === null || options === void 0 ? void 0 : options.apiName,
9368
9373
  changedEntities: getChangedEntities(context, rawEvent),
9374
+ skipUndo: !(shouldMarkNewContent || shouldAddSnapshot) ||
9375
+ (options === null || options === void 0 ? void 0 : options.changeSource) == roosterjs_content_model_dom_1.ChangeSource.Keyboard, // Keyboard changes will be handled separately in undo plugin, so we need to skip handling it again
9369
9376
  };
9370
9377
  core.api.triggerEvent(core, eventData, true /*broadcast*/);
9371
9378
  if (canUndoByBackspace && (selection === null || selection === void 0 ? void 0 : selection.type) == 'range') {
@@ -9948,7 +9955,7 @@ var setContentModel = function (core, model, option, onNodeCreated, isInitializi
9948
9955
  : (0, roosterjs_content_model_dom_1.createModelToDomContextWithConfig)(core.environment.modelToDomSettings.calculated, editorContext);
9949
9956
  modelToDomContext.onNodeCreated = onNodeCreated;
9950
9957
  (_a = core.onFixUpModel) === null || _a === void 0 ? void 0 : _a.call(core, model);
9951
- var selection = (0, roosterjs_content_model_dom_1.contentModelToDom)(core.logicalRoot.ownerDocument, core.logicalRoot, model, modelToDomContext);
9958
+ var selection = (0, roosterjs_content_model_dom_1.contentModelToDom)(core.logicalRoot.ownerDocument.implementation.createHTMLDocument(), core.logicalRoot, model, modelToDomContext);
9952
9959
  if (!core.lifecycle.shadowEditFragment) {
9953
9960
  // Clear pending mutations since we will use our latest model object to replace existing cache
9954
9961
  (_b = core.cache.textMutationObserver) === null || _b === void 0 ? void 0 : _b.flushMutations(true /*ignoreMutations*/);
@@ -10602,11 +10609,10 @@ var CachePlugin = /** @class */ (function () {
10602
10609
  var _this = this;
10603
10610
  this.editor = null;
10604
10611
  this.onMutation = function (mutation) {
10605
- var _a, _b;
10606
10612
  if (_this.editor) {
10607
10613
  switch (mutation.type) {
10608
10614
  case 'childList':
10609
- if (!((_a = _this.state.domIndexer) === null || _a === void 0 ? void 0 : _a.reconcileChildList(mutation.addedNodes, mutation.removedNodes))) {
10615
+ if (!_this.state.domIndexer.reconcileChildList(mutation.addedNodes, mutation.removedNodes)) {
10610
10616
  _this.invalidateCache();
10611
10617
  }
10612
10618
  break;
@@ -10615,7 +10621,7 @@ var CachePlugin = /** @class */ (function () {
10615
10621
  break;
10616
10622
  case 'elementId':
10617
10623
  var element = mutation.element;
10618
- if (!((_b = _this.state.domIndexer) === null || _b === void 0 ? void 0 : _b.reconcileElementId(element))) {
10624
+ if (!_this.state.domIndexer.reconcileElementId(element)) {
10619
10625
  _this.invalidateCache();
10620
10626
  }
10621
10627
  break;
@@ -10631,13 +10637,12 @@ var CachePlugin = /** @class */ (function () {
10631
10637
  _this.updateCachedModel(_this.editor);
10632
10638
  }
10633
10639
  };
10634
- this.state = {};
10635
- if (!option.disableCache) {
10636
- this.state.domIndexer = new domIndexerImpl_1.DomIndexerImpl(option.experimentalFeatures &&
10640
+ this.state = {
10641
+ domIndexer: new domIndexerImpl_1.DomIndexerImpl(option.experimentalFeatures &&
10637
10642
  option.experimentalFeatures.indexOf('PersistCache') >= 0, option.experimentalFeatures &&
10638
- option.experimentalFeatures.indexOf('KeepSelectionMarkerWhenEnteringTextNode') >= 0);
10639
- this.state.textMutationObserver = (0, textMutationObserver_1.createTextMutationObserver)(contentDiv, this.onMutation);
10640
- }
10643
+ option.experimentalFeatures.indexOf('KeepSelectionMarkerWhenEnteringTextNode') >= 0),
10644
+ textMutationObserver: (0, textMutationObserver_1.createTextMutationObserver)(contentDiv, this.onMutation),
10645
+ };
10641
10646
  if (option.enableParagraphMap) {
10642
10647
  this.state.paragraphMap = (0, ParagraphMapImpl_1.createParagraphMap)();
10643
10648
  }
@@ -10655,10 +10660,9 @@ var CachePlugin = /** @class */ (function () {
10655
10660
  * @param editor The editor object
10656
10661
  */
10657
10662
  CachePlugin.prototype.initialize = function (editor) {
10658
- var _a;
10659
10663
  this.editor = editor;
10660
10664
  this.editor.getDocument().addEventListener('selectionchange', this.onNativeSelectionChange);
10661
- (_a = this.state.textMutationObserver) === null || _a === void 0 ? void 0 : _a.startObserving();
10665
+ this.state.textMutationObserver.startObserving();
10662
10666
  };
10663
10667
  /**
10664
10668
  * The last method that editor will call to a plugin before it is disposed.
@@ -10666,8 +10670,7 @@ var CachePlugin = /** @class */ (function () {
10666
10670
  * called, plugin should not call to any editor method since it will result in error.
10667
10671
  */
10668
10672
  CachePlugin.prototype.dispose = function () {
10669
- var _a;
10670
- (_a = this.state.textMutationObserver) === null || _a === void 0 ? void 0 : _a.stopObserving();
10673
+ this.state.textMutationObserver.stopObserving();
10671
10674
  if (this.editor) {
10672
10675
  this.editor
10673
10676
  .getDocument()
@@ -10694,25 +10697,16 @@ var CachePlugin = /** @class */ (function () {
10694
10697
  switch (event.eventType) {
10695
10698
  case 'logicalRootChanged':
10696
10699
  this.invalidateCache();
10697
- if (this.state.textMutationObserver) {
10698
- this.state.textMutationObserver.stopObserving();
10699
- this.state.textMutationObserver = (0, textMutationObserver_1.createTextMutationObserver)(event.logicalRoot, this.onMutation);
10700
- this.state.textMutationObserver.startObserving();
10701
- }
10702
- break;
10703
- case 'keyDown':
10704
- case 'input':
10705
- if (!this.state.textMutationObserver) {
10706
- // When updating cache is not enabled, need to clear the cache to make sure other plugins can get an up-to-date content model
10707
- this.invalidateCache();
10708
- }
10700
+ this.state.textMutationObserver.stopObserving();
10701
+ this.state.textMutationObserver = (0, textMutationObserver_1.createTextMutationObserver)(event.logicalRoot, this.onMutation);
10702
+ this.state.textMutationObserver.startObserving();
10709
10703
  break;
10710
10704
  case 'selectionChanged':
10711
10705
  this.updateCachedModel(this.editor);
10712
10706
  break;
10713
10707
  case 'contentChanged':
10714
10708
  var contentModel = event.contentModel, selection = event.selection;
10715
- if (contentModel && this.state.domIndexer) {
10709
+ if (contentModel) {
10716
10710
  (0, updateCache_1.updateCache)(this.state, contentModel, selection);
10717
10711
  }
10718
10712
  else {
@@ -10732,7 +10726,6 @@ var CachePlugin = /** @class */ (function () {
10732
10726
  }
10733
10727
  };
10734
10728
  CachePlugin.prototype.updateCachedModel = function (editor, forceUpdate) {
10735
- var _a;
10736
10729
  if (editor.isInShadowEdit()) {
10737
10730
  return;
10738
10731
  }
@@ -10747,7 +10740,7 @@ var CachePlugin = /** @class */ (function () {
10747
10740
  if (isSelectionChanged) {
10748
10741
  if (!model ||
10749
10742
  !newRangeEx ||
10750
- !((_a = this.state.domIndexer) === null || _a === void 0 ? void 0 : _a.reconcileSelection(model, newRangeEx, cachedSelection))) {
10743
+ !this.state.domIndexer.reconcileSelection(model, newRangeEx, cachedSelection)) {
10751
10744
  this.invalidateCache();
10752
10745
  }
10753
10746
  else {
@@ -14652,8 +14645,8 @@ function shouldAddSnapshot(currentSnapshot, snapshot) {
14652
14645
 
14653
14646
  Object.defineProperty(exports, "__esModule", ({ value: true }));
14654
14647
  exports.createUndoPlugin = void 0;
14655
- var roosterjs_content_model_dom_1 = __webpack_require__(/*! roosterjs-content-model-dom */ "./packages/roosterjs-content-model-dom/lib/index.ts");
14656
14648
  var SnapshotsManagerImpl_1 = __webpack_require__(/*! ./SnapshotsManagerImpl */ "./packages/roosterjs-content-model-core/lib/corePlugin/undo/SnapshotsManagerImpl.ts");
14649
+ var roosterjs_content_model_dom_1 = __webpack_require__(/*! roosterjs-content-model-dom */ "./packages/roosterjs-content-model-dom/lib/index.ts");
14657
14650
  var undo_1 = __webpack_require__(/*! ../../command/undo/undo */ "./packages/roosterjs-content-model-core/lib/command/undo/undo.ts");
14658
14651
  var Backspace = 'Backspace';
14659
14652
  var Delete = 'Delete';
@@ -14830,10 +14823,7 @@ var UndoPlugin = /** @class */ (function () {
14830
14823
  this.state.snapshotsManager.hasNewContent = true;
14831
14824
  };
14832
14825
  UndoPlugin.prototype.onContentChanged = function (event) {
14833
- if (!(this.state.isRestoring ||
14834
- event.source == roosterjs_content_model_dom_1.ChangeSource.SwitchToDarkMode ||
14835
- event.source == roosterjs_content_model_dom_1.ChangeSource.SwitchToLightMode ||
14836
- event.source == roosterjs_content_model_dom_1.ChangeSource.Keyboard)) {
14826
+ if (!this.state.isRestoring && !event.skipUndo) {
14837
14827
  this.clearRedoForInput();
14838
14828
  }
14839
14829
  };
@@ -15116,6 +15106,7 @@ var Editor = /** @class */ (function () {
15116
15106
  source: isDarkMode
15117
15107
  ? roosterjs_content_model_dom_1.ChangeSource.SwitchToDarkMode
15118
15108
  : roosterjs_content_model_dom_1.ChangeSource.SwitchToLightMode,
15109
+ skipUndo: true,
15119
15110
  }, true);
15120
15111
  }
15121
15112
  };
@@ -24753,12 +24744,14 @@ exports.getClosestAncestorBlockGroupIndex = void 0;
24753
24744
  * @param path The block group path, from the closest one to root
24754
24745
  * @param blockGroupTypes The expected block group types
24755
24746
  * @param stopTypes @optional Block group types that will cause stop searching
24747
+ * @param isValidTarget @optional An additional callback to validate whether a matching block group is a valid target
24756
24748
  */
24757
- function getClosestAncestorBlockGroupIndex(path, blockGroupTypes, stopTypes) {
24749
+ function getClosestAncestorBlockGroupIndex(path, blockGroupTypes, stopTypes, isValidTarget) {
24758
24750
  if (stopTypes === void 0) { stopTypes = []; }
24759
24751
  for (var i = 0; i < path.length; i++) {
24760
24752
  var group = path[i];
24761
- if (blockGroupTypes.indexOf(group.blockGroupType) >= 0) {
24753
+ if (blockGroupTypes.indexOf(group.blockGroupType) >= 0 &&
24754
+ (!isValidTarget || isValidTarget(group))) {
24762
24755
  return i;
24763
24756
  }
24764
24757
  else if (stopTypes.indexOf(group.blockGroupType) >= 0) {
@@ -25042,7 +25035,7 @@ function mergeTables(markerPosition, newTable, source) {
25042
25035
  function mergeList(markerPosition, newList) {
25043
25036
  splitParagraph(markerPosition, newList.format);
25044
25037
  var path = markerPosition.path, paragraph = markerPosition.paragraph;
25045
- var listItemIndex = (0, getClosestAncestorBlockGroupIndex_1.getClosestAncestorBlockGroupIndex)(path, ['ListItem']);
25038
+ var listItemIndex = (0, getClosestAncestorBlockGroupIndex_1.getClosestAncestorBlockGroupIndex)(path, ['ListItem'], ['TableCell']);
25046
25039
  var listItem = path[listItemIndex];
25047
25040
  var listParent = path[listItemIndex + 1]; // It is ok here when index is -1, that means there is no list and we just insert a new paragraph and use path[0] as its parent
25048
25041
  var blockIndex = listParent.blocks.indexOf(listItem || paragraph);
@@ -26415,7 +26408,7 @@ function getSelectedParagraphs(model, mutate) {
26415
26408
  return result;
26416
26409
  }
26417
26410
  exports.getSelectedParagraphs = getSelectedParagraphs;
26418
- function getOperationalBlocks(group, blockGroupTypes, stopTypes, deepFirst) {
26411
+ function getOperationalBlocks(group, blockGroupTypes, stopTypes, deepFirst, isValidTarget) {
26419
26412
  var result = [];
26420
26413
  var findSequence = deepFirst ? blockGroupTypes.map(function (type) { return [type]; }) : [blockGroupTypes];
26421
26414
  var selections = collectSelections(group, {
@@ -26426,7 +26419,7 @@ function getOperationalBlocks(group, blockGroupTypes, stopTypes, deepFirst) {
26426
26419
  selections.forEach(function (_a) {
26427
26420
  var path = _a.path, block = _a.block;
26428
26421
  var _loop_1 = function (i) {
26429
- var groupIndex = (0, getClosestAncestorBlockGroupIndex_1.getClosestAncestorBlockGroupIndex)(path, findSequence[i], stopTypes);
26422
+ var groupIndex = (0, getClosestAncestorBlockGroupIndex_1.getClosestAncestorBlockGroupIndex)(path, findSequence[i], stopTypes, isValidTarget);
26430
26423
  if (groupIndex >= 0) {
26431
26424
  if (result.filter(function (x) { return x.block == path[groupIndex]; }).length <= 0) {
26432
26425
  result.push({
@@ -27894,8 +27887,8 @@ var handleParagraph = function (doc, parent, paragraph, context, refNode) {
27894
27887
  paragraph.segments.some(function (segment) { return segment.segmentType != 'SelectionMarker'; }));
27895
27888
  var formatOnWrapper = needParagraphWrapper
27896
27889
  ? (0, tslib_1.__assign)((0, tslib_1.__assign)({}, (((_a = paragraph.decorator) === null || _a === void 0 ? void 0 : _a.format) || {})), paragraph.segmentFormat) : {};
27890
+ var prevRefNode = refNode === null || refNode === void 0 ? void 0 : refNode.previousSibling;
27897
27891
  container = doc.createElement(((_b = paragraph.decorator) === null || _b === void 0 ? void 0 : _b.tagName) || DefaultParagraphTag);
27898
- parent.insertBefore(container, refNode);
27899
27892
  context.regularSelection.current = {
27900
27893
  block: needParagraphWrapper ? container : container.parentNode,
27901
27894
  segment: null,
@@ -27937,7 +27930,13 @@ var handleParagraph = function (doc, parent, paragraph, context, refNode) {
27937
27930
  // since this paragraph it is implicit. In that case container.nextSibling will become original
27938
27931
  // inline entity's next sibling. So reset refNode to its real next sibling (after change) here
27939
27932
  // to make sure the value is correct.
27940
- refNode = container.nextSibling;
27933
+ refNode =
27934
+ prevRefNode === undefined // When refNode is not passed in
27935
+ ? null
27936
+ : prevRefNode === null // When refNode is the first child of parent
27937
+ ? parent.firstChild
27938
+ : prevRefNode.nextSibling; // Normal case
27939
+ parent.insertBefore(container, refNode);
27941
27940
  if (container) {
27942
27941
  (_d = context.onNodeCreated) === null || _d === void 0 ? void 0 : _d.call(context, paragraph, container);
27943
27942
  (_e = context.domIndexer) === null || _e === void 0 ? void 0 : _e.onParagraph(container);
@@ -28724,9 +28723,11 @@ function applySegmentFormatting(text, paragraph, decorator) {
28724
28723
  if (segment.type === 'link') {
28725
28724
  (0, applyLink_1.applyLink)(formattedSegment, segment.text, segment.url);
28726
28725
  }
28727
- (0, adjustHeading_1.adjustHeading)(formattedSegment, decorator);
28728
- var formattedSegments = (0, applyTextFormatting_1.applyTextFormatting)(formattedSegment);
28729
- (_b = paragraph.segments).push.apply(_b, (0, tslib_1.__spreadArray)([], (0, tslib_1.__read)(formattedSegments), false));
28726
+ var segmentWithAdjustedHeading = (0, adjustHeading_1.adjustHeading)(formattedSegment, decorator);
28727
+ if (segmentWithAdjustedHeading) {
28728
+ var formattedSegments = (0, applyTextFormatting_1.applyTextFormatting)(formattedSegment);
28729
+ (_b = paragraph.segments).push.apply(_b, (0, tslib_1.__spreadArray)([], (0, tslib_1.__read)(formattedSegments), false));
28730
+ }
28730
28731
  }
28731
28732
  }
28732
28733
  }
@@ -28757,52 +28758,129 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
28757
28758
  exports.applyTextFormatting = void 0;
28758
28759
  var tslib_1 = __webpack_require__(/*! tslib */ "./node_modules/tslib/tslib.es6.mjs");
28759
28760
  var roosterjs_content_model_dom_1 = __webpack_require__(/*! roosterjs-content-model-dom */ "./packages/roosterjs-content-model-dom/lib/index.ts");
28760
- var SPLIT_PATTERN = /(\*\*\*.*?\*\*\*|\*\*.*?\*\*|\*.*?\*|\~\~.*?\~\~)/;
28761
28761
  /**
28762
28762
  * @internal
28763
28763
  */
28764
28764
  function applyTextFormatting(textSegment) {
28765
- var e_1, _a;
28766
- var texts = splitSegments(textSegment.text);
28765
+ var text = textSegment.text;
28766
+ // Quick check: if the text contains only formatting markers, return original
28767
+ if (isOnlyFormattingMarkers(text)) {
28768
+ return [textSegment];
28769
+ }
28767
28770
  var textSegments = [];
28768
- try {
28769
- for (var texts_1 = (0, tslib_1.__values)(texts), texts_1_1 = texts_1.next(); !texts_1_1.done; texts_1_1 = texts_1.next()) {
28770
- var text = texts_1_1.value;
28771
- textSegments.push(createFormattedSegment(text, textSegment.format, textSegment.link));
28771
+ var currentState = { bold: false, italic: false, strikethrough: false };
28772
+ var currentText = '';
28773
+ var i = 0;
28774
+ while (i < text.length) {
28775
+ var marker = parseMarkerAt(text, i);
28776
+ if (marker) {
28777
+ // Check if this marker should be treated as formatting or as literal text
28778
+ if (shouldToggleFormatting(text, i, marker, currentState)) {
28779
+ // If we have accumulated text, create a segment for it
28780
+ if (currentText.length > 0) {
28781
+ textSegments.push(createFormattedSegment(currentText, textSegment.format, currentState, textSegment.link));
28782
+ currentText = '';
28783
+ }
28784
+ // Toggle the formatting state
28785
+ toggleFormatting(currentState, marker.type);
28786
+ // Skip the marker characters
28787
+ i += marker.length;
28788
+ }
28789
+ else {
28790
+ // Treat as regular text if marker is not valid in this context
28791
+ currentText += text[i];
28792
+ i++;
28793
+ }
28772
28794
  }
28773
- }
28774
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
28775
- finally {
28776
- try {
28777
- if (texts_1_1 && !texts_1_1.done && (_a = texts_1.return)) _a.call(texts_1);
28795
+ else {
28796
+ // Regular character, add to current text
28797
+ currentText += text[i];
28798
+ i++;
28778
28799
  }
28779
- finally { if (e_1) throw e_1.error; }
28800
+ }
28801
+ // Add any remaining text as a final segment
28802
+ if (currentText.length > 0) {
28803
+ textSegments.push(createFormattedSegment(currentText, textSegment.format, currentState, textSegment.link));
28804
+ }
28805
+ // If no meaningful formatting was applied, return the original segment
28806
+ if (textSegments.length === 0 ||
28807
+ (textSegments.length === 1 && textSegments[0].text === textSegment.text)) {
28808
+ return [textSegment];
28780
28809
  }
28781
28810
  return textSegments;
28782
28811
  }
28783
28812
  exports.applyTextFormatting = applyTextFormatting;
28784
- function splitSegments(text) {
28785
- return text.split(SPLIT_PATTERN).filter(function (s) { return s.trim().length > 0; });
28813
+ function isOnlyFormattingMarkers(text) {
28814
+ // Remove all potential formatting markers and see if anything remains
28815
+ var remaining = text;
28816
+ remaining = remaining.replace(/\*\*/g, ''); // Remove **
28817
+ remaining = remaining.replace(/~~/g, ''); // Remove ~~
28818
+ remaining = remaining.replace(/\*/g, ''); // Remove *
28819
+ // If nothing remains after removing all markers, it was only markers
28820
+ return remaining.length === 0;
28821
+ }
28822
+ function parseMarkerAt(text, index) {
28823
+ var remaining = text.substring(index);
28824
+ if (remaining.startsWith('~~')) {
28825
+ return { type: 'strikethrough', length: 2 };
28826
+ }
28827
+ if (remaining.startsWith('**')) {
28828
+ return { type: 'bold', length: 2 };
28829
+ }
28830
+ if (remaining.startsWith('*')) {
28831
+ return { type: 'italic', length: 1 };
28832
+ }
28833
+ return null;
28786
28834
  }
28787
- function createFormattedSegment(text, format, link) {
28788
- if (text.startsWith('***') && text.endsWith('***')) {
28789
- format = (0, tslib_1.__assign)((0, tslib_1.__assign)({}, format), { fontWeight: 'bold', italic: true });
28790
- text = text.replace(/\*\*\*/g, '');
28791
- text = text + ' ';
28835
+ function shouldToggleFormatting(text, index, marker, currentState) {
28836
+ var nextChar = index + marker.length < text.length ? text.charAt(index + marker.length) : '';
28837
+ var isCurrentlyActive = getCurrentFormatState(currentState, marker.type);
28838
+ if (isCurrentlyActive) {
28839
+ // We're currently in this format, so any marker can close it
28840
+ return true;
28792
28841
  }
28793
- else if (text.startsWith('**') && text.endsWith('**')) {
28794
- format = (0, tslib_1.__assign)((0, tslib_1.__assign)({}, format), { fontWeight: 'bold' });
28795
- text = text.replace(/\*\*/g, '');
28796
- text = text + ' ';
28842
+ else {
28843
+ // We're not in this format, so this marker would open it
28844
+ // Opening markers must be followed by non-whitespace
28845
+ return nextChar.length > 0 && !isWhitespace(nextChar);
28797
28846
  }
28798
- else if (text.startsWith('*') && text.endsWith('*')) {
28799
- format = (0, tslib_1.__assign)((0, tslib_1.__assign)({}, format), { italic: true });
28800
- text = text.replace(/\*/g, '');
28801
- text = text + ' ';
28847
+ }
28848
+ function isWhitespace(char) {
28849
+ return /\s/.test(char);
28850
+ }
28851
+ function toggleFormatting(state, type) {
28852
+ switch (type) {
28853
+ case 'bold':
28854
+ state.bold = !state.bold;
28855
+ break;
28856
+ case 'italic':
28857
+ state.italic = !state.italic;
28858
+ break;
28859
+ case 'strikethrough':
28860
+ state.strikethrough = !state.strikethrough;
28861
+ break;
28802
28862
  }
28803
- else if (text.startsWith('~~') && text.endsWith('~~')) {
28804
- format = (0, tslib_1.__assign)((0, tslib_1.__assign)({}, format), { strikethrough: true });
28805
- text = text.replace(/\~\~/g, '');
28863
+ }
28864
+ function getCurrentFormatState(state, type) {
28865
+ switch (type) {
28866
+ case 'bold':
28867
+ return state.bold;
28868
+ case 'italic':
28869
+ return state.italic;
28870
+ case 'strikethrough':
28871
+ return state.strikethrough;
28872
+ }
28873
+ }
28874
+ function createFormattedSegment(text, baseFormat, state, link) {
28875
+ var format = (0, tslib_1.__assign)({}, baseFormat);
28876
+ if (state.bold) {
28877
+ format.fontWeight = 'bold';
28878
+ }
28879
+ if (state.italic) {
28880
+ format.italic = true;
28881
+ }
28882
+ if (state.strikethrough) {
28883
+ format.strikethrough = true;
28806
28884
  }
28807
28885
  return (0, roosterjs_content_model_dom_1.createText)(text, format, link);
28808
28886
  }
@@ -28821,16 +28899,14 @@ function createFormattedSegment(text, format, link) {
28821
28899
  Object.defineProperty(exports, "__esModule", ({ value: true }));
28822
28900
  exports.convertMarkdownToContentModel = void 0;
28823
28901
  var markdownProcessor_1 = __webpack_require__(/*! ./processor/markdownProcessor */ "./packages/roosterjs-content-model-markdown/lib/markdownToModel/processor/markdownProcessor.ts");
28824
- /**
28825
- * Convert the whole content to ContentModel with the given plain text
28826
- * @param editor The editor instance
28827
- * @param text The markdown text
28828
- * @param splitLinesPattern The pattern to split lines. Default is /\r\n|\r|\\n|\n/
28829
- * @returns The ContentModelDocument
28830
- */
28831
- function convertMarkdownToContentModel(text, splitLinesPattern) {
28832
- var pattern = splitLinesPattern || /\r\n|\r|\\n|\n/;
28833
- return (0, markdownProcessor_1.markdownProcessor)(text, pattern);
28902
+ function convertMarkdownToContentModel(text, splitLinesPatternOrOptions) {
28903
+ var _a;
28904
+ var options = (_a = (typeof splitLinesPatternOrOptions === 'string'
28905
+ ? {
28906
+ splitLinesPattern: splitLinesPatternOrOptions,
28907
+ }
28908
+ : splitLinesPatternOrOptions)) !== null && _a !== void 0 ? _a : {};
28909
+ return (0, markdownProcessor_1.markdownProcessor)(text, options);
28834
28910
  }
28835
28911
  exports.convertMarkdownToContentModel = convertMarkdownToContentModel;
28836
28912
 
@@ -29139,14 +29215,25 @@ var MarkdownBlockType = {
29139
29215
  * @param splitLinesPattern The pattern to split lines. Default is /\r\n|\r|\\n|\n/
29140
29216
  * @returns The ContentModelDocument
29141
29217
  */
29142
- function markdownProcessor(text, splitLinesPattern) {
29218
+ function markdownProcessor(text, options) {
29219
+ var _a;
29220
+ var splitLinesPattern = options.splitLinesPattern || /\r\n|\r|\\n|\n/;
29221
+ var emptyLine = (_a = options.emptyLine) !== null && _a !== void 0 ? _a : 'merge';
29143
29222
  var markdownText = text.split(splitLinesPattern);
29144
29223
  markdownText.push(''); // Add an empty line to make sure the last block is processed
29145
29224
  var doc = (0, roosterjs_content_model_dom_1.createContentModelDocument)();
29146
- return convertMarkdownText(doc, markdownText);
29225
+ var model = convertMarkdownText(doc, markdownText, options);
29226
+ var lastBlock = model.blocks[model.blocks.length - 1];
29227
+ if (emptyLine != 'remove' &&
29228
+ lastBlock &&
29229
+ lastBlock.blockType == 'Paragraph' &&
29230
+ lastBlock.segments.every(function (x) { return x.segmentType == 'Br'; })) {
29231
+ model.blocks.pop();
29232
+ }
29233
+ return model;
29147
29234
  }
29148
29235
  exports.markdownProcessor = markdownProcessor;
29149
- function addMarkdownBlockToModel(model, blockType, markdown, patternName, markdownContext) {
29236
+ function addMarkdownBlockToModel(model, blockType, markdown, patternName, markdownContext, options) {
29150
29237
  var e_1, _a;
29151
29238
  if (blockType !== 'Table' &&
29152
29239
  markdownContext.tableLines &&
@@ -29177,10 +29264,43 @@ function addMarkdownBlockToModel(model, blockType, markdown, patternName, markdo
29177
29264
  markdownContext.tableLines.length = 0;
29178
29265
  }
29179
29266
  if (patternName == 'space') {
29180
- markdownContext.tableLines = [];
29181
- markdownContext.lastQuote = undefined;
29182
- markdownContext.lastList = undefined;
29183
- return;
29267
+ if (markdownContext.tableLines.length > 0 ||
29268
+ markdownContext.lastQuote ||
29269
+ markdownContext.lastList) {
29270
+ markdownContext.tableLines = [];
29271
+ markdownContext.lastQuote = undefined;
29272
+ markdownContext.lastList = undefined;
29273
+ return;
29274
+ }
29275
+ switch (options.emptyLine) {
29276
+ case 'remove':
29277
+ // no op, ignore this line
29278
+ return;
29279
+ case 'merge':
29280
+ switch (markdownContext.emptyLineState) {
29281
+ case 'notEmpty':
29282
+ default:
29283
+ // Last line is not empty line, so this empty line is treated as the line end of last paragraph
29284
+ markdownContext.emptyLineState = 'lineEnded';
29285
+ return;
29286
+ case 'lineEnded':
29287
+ // We already see an empty line for paragraph ends, so this line is treated as a real empty line
29288
+ markdownContext.emptyLineState = 'empty';
29289
+ // Keep going, process as a normal paragraph
29290
+ break;
29291
+ case 'empty':
29292
+ // Already processed empty line, so this one should be ignored
29293
+ return;
29294
+ }
29295
+ break;
29296
+ case 'preserve':
29297
+ default:
29298
+ // no op, treat it as paragraph
29299
+ break;
29300
+ }
29301
+ }
29302
+ else {
29303
+ markdownContext.emptyLineState = 'notEmpty';
29184
29304
  }
29185
29305
  if (blockType == 'Paragraph' && (markdownContext.lastList || markdownContext.lastQuote)) {
29186
29306
  blockType = 'BlockGroup';
@@ -29219,7 +29339,7 @@ function addMarkdownBlockToModel(model, blockType, markdown, patternName, markdo
29219
29339
  markdownContext.lastList = undefined;
29220
29340
  }
29221
29341
  }
29222
- function convertMarkdownText(model, lines) {
29342
+ function convertMarkdownText(model, lines, options) {
29223
29343
  var e_2, _a;
29224
29344
  var markdownContext = {
29225
29345
  lastQuote: undefined,
@@ -29234,14 +29354,14 @@ function convertMarkdownText(model, lines) {
29234
29354
  if (MarkdownPattern.hasOwnProperty(patternName)) {
29235
29355
  var pattern = MarkdownPattern[patternName];
29236
29356
  if (pattern.test(line)) {
29237
- addMarkdownBlockToModel(model, MarkdownBlockType[patternName], line, patternName, markdownContext);
29357
+ addMarkdownBlockToModel(model, MarkdownBlockType[patternName], line, patternName, markdownContext, options);
29238
29358
  matched = true;
29239
29359
  break;
29240
29360
  }
29241
29361
  }
29242
29362
  }
29243
29363
  if (!matched) {
29244
- addMarkdownBlockToModel(model, 'Paragraph', line, 'paragraph', markdownContext);
29364
+ addMarkdownBlockToModel(model, 'Paragraph', line, 'paragraph', markdownContext, options);
29245
29365
  }
29246
29366
  }
29247
29367
  }
@@ -29276,6 +29396,10 @@ function adjustHeading(textSegment, decorator) {
29276
29396
  var markdownToBeRemoved = headings_1.MarkdownHeadings[(decorator === null || decorator === void 0 ? void 0 : decorator.tagName) || ''];
29277
29397
  if (markdownToBeRemoved) {
29278
29398
  textSegment.text = textSegment.text.replace(markdownToBeRemoved, '');
29399
+ if (textSegment.text.length === 0) {
29400
+ // If the text becomes empty after removing the heading markdown, we can remove the segment
29401
+ return null;
29402
+ }
29279
29403
  }
29280
29404
  return textSegment;
29281
29405
  }
@@ -30278,8 +30402,7 @@ function createLink(editor, autoLinkOptions) {
30278
30402
  (0, roosterjs_content_model_api_1.formatTextSegmentBeforeSelectionMarker)(editor, function (_model, segment, paragraph) {
30279
30403
  var promotedSegment = null;
30280
30404
  if (segment.link) {
30281
- links.push(segment.link);
30282
- return true;
30405
+ return false;
30283
30406
  }
30284
30407
  else if ((promotedSegment = (0, roosterjs_content_model_api_1.promoteLink)(segment, paragraph, autoLinkOptions)) &&
30285
30408
  promotedSegment.link) {
@@ -31538,26 +31661,41 @@ exports.deleteList = deleteList;
31538
31661
  /*!***********************************************************************************************!*\
31539
31662
  !*** ./packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteParagraphStyle.ts ***!
31540
31663
  \***********************************************************************************************/
31541
- /***/ ((__unused_webpack_module, exports) => {
31664
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
31542
31665
 
31543
31666
  "use strict";
31544
31667
 
31545
31668
  Object.defineProperty(exports, "__esModule", ({ value: true }));
31546
31669
  exports.deleteParagraphStyle = void 0;
31670
+ var roosterjs_content_model_dom_1 = __webpack_require__(/*! roosterjs-content-model-dom */ "./packages/roosterjs-content-model-dom/lib/index.ts");
31547
31671
  /**
31548
31672
  * @internal
31549
31673
  */
31550
31674
  var deleteParagraphStyle = function (context) {
31551
31675
  if (context.deleteResult === 'nothingToDelete') {
31552
31676
  var insertPoint = context.insertPoint;
31553
- var paragraph = insertPoint.paragraph;
31677
+ var paragraph = insertPoint.paragraph, path = insertPoint.path;
31678
+ var group = path[0];
31679
+ var parentGroup = path[1];
31554
31680
  // If the paragraph is empty, we will delete any style in it
31555
31681
  // This is to ensure the paragraph style is reset to default when there is no content in the paragraph
31556
31682
  if (paragraph.segments.every(function (s) { return s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'; }) &&
31557
- paragraph.segments.filter(function (s) { return s.segmentType === 'Br'; }).length <= 1 &&
31558
- Object.keys(paragraph.format).length > 0) {
31559
- paragraph.format = {};
31560
- context.deleteResult = 'range';
31683
+ paragraph.segments.filter(function (s) { return s.segmentType === 'Br'; }).length <= 1) {
31684
+ if (Object.keys(paragraph.format).length > 0) {
31685
+ paragraph.format = {};
31686
+ context.deleteResult = 'range';
31687
+ }
31688
+ else if (group.blocks.length == 1 &&
31689
+ group.blocks[0] == paragraph &&
31690
+ parentGroup &&
31691
+ (group.blockGroupType == 'FormatContainer' ||
31692
+ group.blockGroupType == 'ListItem' ||
31693
+ group.blockGroupType == 'General')) {
31694
+ // Still has nothing to delete, try to unwrap parent container
31695
+ (0, roosterjs_content_model_dom_1.unwrapBlock)(parentGroup, group);
31696
+ path.shift();
31697
+ context.deleteResult = 'range';
31698
+ }
31561
31699
  }
31562
31700
  }
31563
31701
  };
@@ -31782,6 +31920,7 @@ function handleKeyboardEventResult(editor, model, rawEvent, result, context) {
31782
31920
  // We have deleted what we need from content model, no need to let browser keep handling the event
31783
31921
  rawEvent.preventDefault();
31784
31922
  (0, roosterjs_content_model_dom_1.normalizeContentModel)(model);
31923
+ deleteEmptyBlockGroups(model);
31785
31924
  if (result == 'range') {
31786
31925
  // A range is about to be deleted, so add an undo snapshot immediately
31787
31926
  context.skipUndoSnapshot = false;
@@ -31810,6 +31949,22 @@ function shouldDeleteAllSegmentsBefore(rawEvent) {
31810
31949
  return rawEvent.metaKey && !rawEvent.altKey;
31811
31950
  }
31812
31951
  exports.shouldDeleteAllSegmentsBefore = shouldDeleteAllSegmentsBefore;
31952
+ function deleteEmptyBlockGroups(group) {
31953
+ var modified = false;
31954
+ for (var i = group.blocks.length - 1; i >= 0; i--) {
31955
+ var block = group.blocks[i];
31956
+ if (block.blockType == 'BlockGroup') {
31957
+ deleteEmptyBlockGroups(block);
31958
+ if (block.blocks.length == 0) {
31959
+ (0, roosterjs_content_model_dom_1.mutateBlock)(group).blocks.splice(i, 1);
31960
+ modified = true;
31961
+ }
31962
+ }
31963
+ }
31964
+ if (modified) {
31965
+ group.blocks.forEach(roosterjs_content_model_dom_1.setParagraphNotImplicit);
31966
+ }
31967
+ }
31813
31968
 
31814
31969
 
31815
31970
  /***/ }),
@@ -33424,12 +33579,14 @@ var ImageEditPlugin = /** @class */ (function () {
33424
33579
  var _this = this;
33425
33580
  var editingImageModel;
33426
33581
  var selection = editor.getDOMSelection();
33427
- editor.formatContentModel(function (model) {
33582
+ editor.formatContentModel(function (model, context) {
33428
33583
  var editingImage = (0, getSelectedImage_1.getSelectedImage)(model);
33429
33584
  var previousSelectedImage = isApiOperation
33430
33585
  ? editingImage
33431
33586
  : (0, findEditingImage_1.findEditingImage)(model);
33432
33587
  var result = false;
33588
+ // Skip adding undo snapshot for now. If we detect any changes later, we will reset it
33589
+ context.skipUndoSnapshot = 'SkipAll';
33433
33590
  if (shouldSelectImage ||
33434
33591
  (previousSelectedImage === null || previousSelectedImage === void 0 ? void 0 : previousSelectedImage.image) != (editingImage === null || editingImage === void 0 ? void 0 : editingImage.image) ||
33435
33592
  (previousSelectedImage === null || previousSelectedImage === void 0 ? void 0 : previousSelectedImage.image.format.imageState) == findEditingImage_1.EDITING_MARKER ||
@@ -33442,7 +33599,10 @@ var ImageEditPlugin = /** @class */ (function () {
33442
33599
  imageEditInfo_1 &&
33443
33600
  clonedImage_1) {
33444
33601
  (0, roosterjs_content_model_dom_1.mutateSegment)(previousSelectedImage.paragraph, previousSelectedImage.image, function (image) {
33445
- (0, applyChange_1.applyChange)(editor, selectedImage_1, image, imageEditInfo_1, lastSrc_1, _this.wasImageResized || _this.isCropMode, clonedImage_1);
33602
+ var changeState = (0, applyChange_1.applyChange)(editor, selectedImage_1, image, imageEditInfo_1, lastSrc_1, _this.wasImageResized || _this.isCropMode, clonedImage_1);
33603
+ if (_this.wasImageResized || changeState == 'FullyChanged') {
33604
+ context.skipUndoSnapshot = false;
33605
+ }
33446
33606
  image.isSelected = shouldSelectImage;
33447
33607
  image.isSelectedAsImageSelection = shouldSelectImage;
33448
33608
  image.format.imageState = undefined;
@@ -34340,14 +34500,14 @@ function applyChange(editor, image, contentModelImage, editInfo, previousSrc, wa
34340
34500
  }
34341
34501
  // Write back the change to image, and set its new size
34342
34502
  var generatedImageSize = (0, generateImageSize_1.getGeneratedImageSize)(editInfo);
34343
- if (!generatedImageSize) {
34344
- return;
34345
- }
34346
- contentModelImage.src = newSrc;
34347
- if (wasResizedOrCropped || state == 'FullyChanged') {
34348
- contentModelImage.format.width = generatedImageSize.targetWidth + 'px';
34349
- contentModelImage.format.height = generatedImageSize.targetHeight + 'px';
34503
+ if (generatedImageSize) {
34504
+ contentModelImage.src = newSrc;
34505
+ if (wasResizedOrCropped || state == 'FullyChanged') {
34506
+ contentModelImage.format.width = generatedImageSize.targetWidth + 'px';
34507
+ contentModelImage.format.height = generatedImageSize.targetHeight + 'px';
34508
+ }
34350
34509
  }
34510
+ return state;
34351
34511
  }
34352
34512
  exports.applyChange = applyChange;
34353
34513
 
@@ -34438,8 +34598,7 @@ function checkEditInfoState(editInfo, compareTo) {
34438
34598
  return 'ResizeOnly';
34439
34599
  }
34440
34600
  else if (compareTo &&
34441
- ROTATE_KEYS.every(function (key) { return areSameNumber(editInfo[key], 0); }) &&
34442
- ROTATE_KEYS.every(function (key) { return areSameNumber(compareTo[key], 0); }) &&
34601
+ ROTATE_KEYS.every(function (key) { return areSameNumber(editInfo[key], compareTo[key]); }) &&
34443
34602
  CROP_KEYS.every(function (key) { return areSameNumber(editInfo[key], compareTo[key]); }) &&
34444
34603
  compareTo.flippedHorizontal === editInfo.flippedHorizontal &&
34445
34604
  compareTo.flippedVertical === editInfo.flippedVertical) {