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