@lobehub/editor 4.24.1 → 4.25.1

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.
Files changed (148) hide show
  1. package/es/codemirror/style.js +42 -0
  2. package/es/editor-kernel/kernel.d.ts +9 -0
  3. package/es/editor-kernel/kernel.js +74 -11
  4. package/es/editor-kernel/react/useDecorators.js +2 -4
  5. package/es/headless/collapsible-plugin.js +74 -0
  6. package/es/headless/index.js +2 -0
  7. package/es/headless.d.ts +30 -0
  8. package/es/headless.js +2618 -139
  9. package/es/index.d.ts +26 -3
  10. package/es/index.js +17 -2
  11. package/es/locale/index.d.ts +14 -0
  12. package/es/locale/index.js +14 -0
  13. package/es/plugins/block/command/index.js +24 -0
  14. package/es/plugins/block/plugin/index.js +7 -5
  15. package/es/plugins/block/react/ReactBlockPlugin.js +43 -1
  16. package/es/plugins/block/react/core/menu-position.js +20 -0
  17. package/es/plugins/block/react/drag/drag-session.js +3 -2
  18. package/es/plugins/block/react/drag/drag-utils.js +13 -1
  19. package/es/plugins/codemirror-block/index.d.ts +2 -1
  20. package/es/plugins/codemirror-block/plugin/index.js +2 -0
  21. package/es/plugins/codemirror-block/react/CodemirrorNode.js +95 -5
  22. package/es/plugins/codemirror-block/react/useCodemirrorEditLock.js +33 -0
  23. package/es/plugins/codemirror-block/service/index.d.ts +40 -0
  24. package/es/plugins/codemirror-block/service/index.js +59 -0
  25. package/es/plugins/collapsible/command/index.d.ts +17 -0
  26. package/es/plugins/collapsible/command/index.js +72 -0
  27. package/es/plugins/collapsible/index.d.ts +7 -0
  28. package/es/plugins/collapsible/node/CollapsibleNode.d.ts +30 -0
  29. package/es/plugins/collapsible/node/CollapsibleNode.js +138 -0
  30. package/es/plugins/collapsible/plugin/index.d.ts +11 -0
  31. package/es/plugins/collapsible/plugin/index.js +535 -0
  32. package/es/plugins/collapsible/react/ReactCollapsiblePlugin.d.ts +6 -0
  33. package/es/plugins/collapsible/react/ReactCollapsiblePlugin.js +19 -0
  34. package/es/plugins/collapsible/react/index.d.ts +3 -0
  35. package/es/plugins/collapsible/react/style.js +67 -0
  36. package/es/plugins/collapsible/react/type.d.ts +6 -0
  37. package/es/plugins/common/plugin/index.js +14 -4
  38. package/es/plugins/common/plugin/progressive-select-all.js +81 -0
  39. package/es/plugins/common/plugin/register.js +73 -4
  40. package/es/plugins/common/react/ReactPlainText.js +27 -9
  41. package/es/plugins/link/command/index.js +26 -3
  42. package/es/plugins/link/conversion/index.js +237 -0
  43. package/es/plugins/link/index.d.ts +5 -3
  44. package/es/plugins/link/node/LinkBlockCardNode.d.ts +16 -0
  45. package/es/plugins/link/node/LinkBlockCardNode.js +66 -0
  46. package/es/plugins/link/node/LinkCardNode.d.ts +42 -0
  47. package/es/plugins/link/node/LinkCardNode.js +161 -0
  48. package/es/plugins/link/node/LinkIframeNode.d.ts +33 -0
  49. package/es/plugins/link/node/LinkIframeNode.js +131 -0
  50. package/es/plugins/link/node/LinkNode.d.ts +48 -2
  51. package/es/plugins/link/node/LinkNode.js +14 -6
  52. package/es/plugins/link/node/SchemaNode.d.ts +36 -0
  53. package/es/plugins/link/node/SchemaNode.js +149 -0
  54. package/es/plugins/link/normalization/index.d.ts +7 -0
  55. package/es/plugins/link/normalization/index.js +38 -0
  56. package/es/plugins/link/plugin/index.d.ts +26 -1
  57. package/es/plugins/link/plugin/index.js +124 -4
  58. package/es/plugins/link/react/ReactLinkPlugin.js +165 -7
  59. package/es/plugins/link/react/components/LinkCard.js +235 -0
  60. package/es/plugins/link/react/components/LinkEdit.js +93 -14
  61. package/es/plugins/link/react/components/LinkIframe.js +128 -0
  62. package/es/plugins/link/react/components/LinkToolbar.js +440 -111
  63. package/es/plugins/link/react/components/SchemaLink.js +49 -0
  64. package/es/plugins/link/react/index.d.ts +2 -2
  65. package/es/plugins/link/react/renderer-registry.d.ts +81 -0
  66. package/es/plugins/link/react/renderer-registry.js +80 -0
  67. package/es/plugins/link/react/style.js +63 -1
  68. package/es/plugins/link/react/type.d.ts +24 -1
  69. package/es/plugins/link/service/i-link-service.d.ts +156 -1
  70. package/es/plugins/link/service/i-link-service.js +258 -5
  71. package/es/plugins/list/plugin/index.js +9 -2
  72. package/es/plugins/litexml/command/diffCommand.js +94 -14
  73. package/es/plugins/litexml/command/index.js +187 -6
  74. package/es/plugins/litexml/diff-validation.js +237 -0
  75. package/es/plugins/litexml/node/DiffContentNode.js +67 -0
  76. package/es/plugins/litexml/node/DiffNode.d.ts +1 -1
  77. package/es/plugins/litexml/node/DiffNode.js +5 -2
  78. package/es/plugins/litexml/node/TableCellDiffNode.js +77 -0
  79. package/es/plugins/litexml/node/TableRowDiffNode.js +73 -0
  80. package/es/plugins/litexml/plugin/index.d.ts +2 -0
  81. package/es/plugins/litexml/plugin/index.js +62 -9
  82. package/es/plugins/litexml/react/DiffNodeToolbar/index.js +10 -12
  83. package/es/plugins/litexml/react/TableRowDiffToolbar.js +123 -0
  84. package/es/plugins/litexml/react/hooks/useHasDiffNode.js +5 -2
  85. package/es/plugins/litexml/react/index.js +6 -2
  86. package/es/plugins/litexml/react/style.js +18 -5
  87. package/es/plugins/litexml/table-cell-diff.js +153 -0
  88. package/es/plugins/litexml/table-row-diff.js +82 -0
  89. package/es/plugins/markdown/data-source/markdown/parse.js +25 -1
  90. package/es/plugins/markdown/plugin/index.js +5 -3
  91. package/es/plugins/slash/react/ReactSlashPlugin.js +22 -43
  92. package/es/plugins/slash/react/components/DefaultSlashMenu.js +76 -16
  93. package/es/plugins/slash/react/menu-utils.js +84 -0
  94. package/es/plugins/slash/service/i-slash-service.d.ts +3 -0
  95. package/es/plugins/table/plugin/index.js +4 -1
  96. package/es/plugins/table/plugin/single-cell-paste.js +79 -0
  97. package/es/plugins/table/react/TableController/hooks.js +7 -4
  98. package/es/plugins/table/react/hooks.js +2 -2
  99. package/es/plugins/table/react/index.js +2 -0
  100. package/es/plugins/table/react/style.js +15 -5
  101. package/es/plugins/table/react/useScrollFocusedTableCellIntoView.js +46 -0
  102. package/es/plugins/toc/index.d.ts +10 -0
  103. package/es/plugins/toc/plugin/index.d.ts +8 -0
  104. package/es/plugins/toc/plugin/index.js +56 -0
  105. package/es/plugins/toc/react/ReactTocPlugin.d.ts +6 -0
  106. package/es/plugins/toc/react/ReactTocPlugin.js +36 -0
  107. package/es/plugins/toc/react/getNearestScrollContainer.d.ts +6 -0
  108. package/es/plugins/toc/react/getNearestScrollContainer.js +18 -0
  109. package/es/plugins/toc/react/hooks/useActiveHeading.js +59 -0
  110. package/es/plugins/toc/react/hooks/useTocItems.js +50 -0
  111. package/es/plugins/toc/react/index.d.ts +5 -0
  112. package/es/plugins/toc/react/type.d.ts +30 -0
  113. package/es/plugins/toc/react/useToc.d.ts +5 -0
  114. package/es/plugins/toc/react/useToc.js +41 -0
  115. package/es/plugins/toc/service/i-toc-service.d.ts +39 -0
  116. package/es/plugins/toc/service/i-toc-service.js +125 -0
  117. package/es/plugins/toc/service/index.d.ts +3 -0
  118. package/es/plugins/toc/types.d.ts +23 -0
  119. package/es/plugins/toc/utils/index.d.ts +6 -0
  120. package/es/plugins/toc/utils/index.js +30 -0
  121. package/es/plugins/toolbar/react/index.js +26 -12
  122. package/es/plugins/toolbar/react/style.js +3 -2
  123. package/es/plugins/virtual-block/plugin/register.js +34 -12
  124. package/es/plugins/yjs/index.d.ts +6 -0
  125. package/es/plugins/yjs/plugin/index.d.ts +7 -0
  126. package/es/plugins/yjs/plugin/index.js +191 -0
  127. package/es/plugins/yjs/plugin/types.d.ts +16 -0
  128. package/es/plugins/yjs/plugin/utils/awareness.js +9 -0
  129. package/es/plugins/yjs/plugin/utils/editor-state.js +47 -0
  130. package/es/plugins/yjs/plugin/utils/history.js +31 -0
  131. package/es/plugins/yjs/plugin/utils/node-properties.js +39 -0
  132. package/es/plugins/yjs/plugin/utils/sync.js +24 -0
  133. package/es/plugins/yjs/react/codemirrorEditLockProvider.js +59 -0
  134. package/es/plugins/yjs/react/index.d.ts +23 -0
  135. package/es/plugins/yjs/react/index.js +200 -0
  136. package/es/plugins/yjs/service/index.d.ts +47 -0
  137. package/es/plugins/yjs/service/index.js +84 -0
  138. package/es/plugins/yjs/websocket-provider.d.ts +78 -0
  139. package/es/plugins/yjs/websocket-provider.js +263 -0
  140. package/es/react/hooks/useEditor.d.ts +24 -2
  141. package/es/react/hooks/useEditor.js +13 -3
  142. package/es/react.d.ts +2 -2
  143. package/es/renderer/LexicalRenderer.js +9 -2
  144. package/es/types/kernel.d.ts +16 -0
  145. package/es/utils/updatePosition.js +5 -2
  146. package/package.json +3 -1
  147. package/patches/@lexical__yjs@0.42.0.patch +42 -0
  148. package/scripts/postinstall-lexical-patch.cjs +62 -36
package/es/headless.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { LITEXML_APPLY_COMMAND, LITEXML_DIFFNODE_ALL_COMMAND, LITEXML_DIFFNODE_COMMAND, LITEXML_INSERT_COMMAND, LITEXML_MODIFY_COMMAND, LITEXML_REMOVE_COMMAND } from "./plugins/litexml/command/symbols.js";
2
2
  import { createHeadlessEditor as createHeadlessEditor$1 } from "@lexical/headless";
3
3
  import { createEmptyHistoryState, registerHistory } from "@lexical/history";
4
- import { $computeTableMapSkipCellCheck, $createTableNodeWithDimensions, $createTableSelection, $deleteTableColumnAtSelection, $deleteTableRowAtSelection, $findTableNode, $insertTableColumnAtSelection, $insertTableRowAtSelection, $isSimpleTable, $isTableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, TableCellNode, TableNode, TableNode as TableNode$1, TableRowNode, registerTableCellUnmergeTransform, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive } from "@lexical/table";
4
+ import { $computeTableMapSkipCellCheck, $createTableCellNode, $createTableNodeWithDimensions, $createTableRowNode, $createTableSelection, $deleteTableColumnAtSelection, $deleteTableRowAtSelection, $findTableNode, $insertTableColumnAtSelection, $insertTableRowAtSelection, $isSimpleTable, $isTableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, TableCellNode, TableNode, TableNode as TableNode$1, TableRowNode, registerTableCellUnmergeTransform, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive } from "@lexical/table";
5
5
  import { get, merge, template, templateSettings } from "es-toolkit/compat";
6
6
  import EventEmitter from "eventemitter3";
7
- import { $applyNodeReplacement, $caretFromPoint, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createRangeSelection, $createTextNode, $getCaretRange, $getCharacterOffsets, $getChildCaret, $getNearestNodeFromDOMNode, $getNodeByKey, $getPreviousSelection, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, $isDecoratorNode, $isElementNode, $isLineBreakNode, $isNodeSelection, $isRangeSelection, $isRootNode, $isRootOrShadowRoot, $isTextNode, $isTextPointCaret, $nodesOfType, $normalizeSelection__EXPERIMENTAL, $parseSerializedNode, $setSelection, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, CONTROLLED_TEXT_INSERTION_COMMAND, COPY_COMMAND, DecoratorNode, ElementNode, FORMAT_TEXT_COMMAND, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, INDENT_CONTENT_COMMAND, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, IS_BOLD, IS_CODE, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, ParagraphNode, REDO_COMMAND, SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, TEXT_TYPE_TO_FORMAT, TextNode, UNDO_COMMAND, createCommand, createEditor, getNearestEditorFromDOMNode, isHTMLElement, isModifierMatch, resetRandomKey, setDOMUnmanaged } from "lexical";
7
+ import { $applyNodeReplacement, $caretFromPoint, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createRangeSelection, $createTextNode, $getCaretRange, $getCharacterOffsets, $getChildCaret, $getNearestNodeFromDOMNode, $getNodeByKey, $getPreviousSelection, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, $isDecoratorNode, $isElementNode, $isLineBreakNode, $isNodeSelection, $isRangeSelection, $isRootNode, $isRootOrShadowRoot, $isTextNode, $isTextPointCaret, $nodesOfType, $normalizeSelection__EXPERIMENTAL, $parseSerializedNode, $setSelection, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, CONTROLLED_TEXT_INSERTION_COMMAND, COPY_COMMAND, DecoratorNode, ElementNode, FORMAT_TEXT_COMMAND, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, INDENT_CONTENT_COMMAND, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, IS_BOLD, IS_CODE, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, ParagraphNode, REDO_COMMAND, SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, SELECT_ALL_COMMAND, TEXT_TYPE_TO_FORMAT, TextNode, UNDO_COMMAND, createCommand, createEditor, getNearestEditorFromDOMNode, isHTMLElement, isModifierMatch, resetRandomKey, setDOMUnmanaged } from "lexical";
8
8
  import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text";
9
9
  import createDebug from "debug";
10
10
  import { $filter, $findMatchingParent, $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $insertNodeToNearestRoot, $wrapNodeInElement, CAN_USE_DOM, IS_APPLE as isApple, addClassNamesToElement, calculateZoomLevel, isHTMLAnchorElement, isHTMLElement as isHTMLElement$1, mergeRegister } from "@lexical/utils";
@@ -46,6 +46,12 @@ var locale_default = {
46
46
  replace: "Replace"
47
47
  },
48
48
  link: {
49
+ copy: "Copy Link",
50
+ copySuccess: "Link copied",
51
+ convertToCard: "Convert to Card",
52
+ convertToIframe: "Convert to Iframe",
53
+ convertToLink: "Convert to Link",
54
+ convertToSchema: "Convert to Schema",
49
55
  edit: "Edit Link",
50
56
  editLinkTitle: "Link",
51
57
  editTextTitle: "Text",
@@ -76,6 +82,14 @@ var locale_default = {
76
82
  insertColumnRight: "Insert {{count}} column(s) to the right",
77
83
  insertRowAbove: "Insert {{count}} row(s) above",
78
84
  insertRowBelow: "Insert {{count}} row(s) below"
85
+ },
86
+ toc: {
87
+ ariaLabel: "Table of contents",
88
+ empty: "No headings",
89
+ expand: "Expand table of contents",
90
+ pin: "Pin table of contents",
91
+ title: "TOC",
92
+ unpin: "Unpin table of contents"
79
93
  }
80
94
  };
81
95
  //#endregion
@@ -804,26 +818,46 @@ var Kernel = class Kernel extends EventEmitter {
804
818
  return this.editor || null;
805
819
  }
806
820
  destroy() {
807
- unregisterEditorKernel(this.themes[EDITOR_THEME_KEY]);
821
+ const editor = this.editor;
822
+ const editorId = this.getEditorId();
808
823
  this.logger.info(`🗑️ Destroying editor with ${this.pluginsInstances.length} plugins`);
824
+ if (editor) try {
825
+ editor.setRootElement(null);
826
+ } catch (error) {
827
+ this.logger.warn("Failed to detach editor root during destroy:", error);
828
+ }
829
+ unregisterEditorKernel(editorId);
809
830
  try {
810
- this.editor?.setEditorState(createEmptyEditorState());
831
+ editor?.setEditorState(createEmptyEditorState());
811
832
  } catch (error) {
812
833
  this.logger.warn("Failed to reset editor state during destroy:", error);
813
834
  }
814
- this.dataTypeMap.clear();
835
+ this.unregisterRootLifecycle?.();
836
+ this.unregisterRootLifecycle = void 0;
837
+ let destroyError;
815
838
  this.pluginsInstances.forEach((plugin) => {
816
- if (plugin.destroy) plugin.destroy();
839
+ try {
840
+ plugin.destroy?.();
841
+ } catch (error) {
842
+ destroyError ??= error;
843
+ }
817
844
  });
818
845
  this.pluginsInstances = [];
819
- this.beforeEditorInitHooks = [];
846
+ this.dataTypeMap.clear();
847
+ this.decorators = {};
820
848
  this.nodeTransforms = [];
849
+ this.nodes = [];
850
+ this.beforeEditorInitHooks = [];
821
851
  this.rootClassNames.clear();
822
852
  this.serviceMap.clear();
823
- this.decorators = {};
824
- this.themes = {};
853
+ this.themes = { [EDITOR_THEME_KEY]: generateEditorId() };
854
+ this.historyState = createEmptyHistoryState();
855
+ this._commands.clear();
856
+ this._commandsClean.clear();
857
+ this.editor = void 0;
825
858
  this.headlessEditor = false;
826
859
  this.logger.info("✅ Editor destroyed");
860
+ if (destroyError) throw destroyError;
827
861
  }
828
862
  getRootElement() {
829
863
  if (this.headlessEditor) return null;
@@ -831,12 +865,23 @@ var Kernel = class Kernel extends EventEmitter {
831
865
  }
832
866
  setRootElement(dom, editable = true) {
833
867
  if (this.editor) {
834
- if (this.headlessEditor) throw new Error("Headless editor cannot be attached to a root element.");
868
+ if (this.headlessEditor) {
869
+ if (dom === null) return this.editor;
870
+ throw new Error("Headless editor cannot be attached to a root element.");
871
+ }
835
872
  this.logger.warn("[Editor] Editor is already initialized, updating root element only");
873
+ if (dom) registerEditorKernel(this.getEditorId(), this);
836
874
  this.editor.setRootElement(dom);
837
- this.applyRootClassNames(dom);
875
+ if (dom) {
876
+ this.editor.setEditable(editable);
877
+ this.applyRootClassNames(dom);
878
+ }
838
879
  return this.editor;
839
880
  }
881
+ if (dom === null) {
882
+ unregisterEditorKernel(this.getEditorId());
883
+ return null;
884
+ }
840
885
  if (this.pluginsInstances.length === 0) {
841
886
  this.logger.info(`🔌 Initializing ${this.plugins.length} plugins`);
842
887
  for (const plugin of this.plugins) {
@@ -847,7 +892,7 @@ var Kernel = class Kernel extends EventEmitter {
847
892
  this.runBeforeEditorInitLifecycle();
848
893
  const resolvedNodes = this.resolveNodesForInitialization();
849
894
  this.logger.info(`📝 Creating editor with ${resolvedNodes.length} nodes`);
850
- registerEditorKernel(this.themes[EDITOR_THEME_KEY], this);
895
+ registerEditorKernel(this.getEditorId(), this);
851
896
  const editor = this.editor = createEditor({
852
897
  __kernel: this,
853
898
  editable,
@@ -860,9 +905,9 @@ var Kernel = class Kernel extends EventEmitter {
860
905
  theme: this.themes
861
906
  });
862
907
  this.headlessEditor = false;
908
+ this.registerRootLifecycle(editor);
863
909
  this.editor.setRootElement(dom);
864
910
  this.applyRootClassNames(dom);
865
- registerEvent(editor, dom);
866
911
  this.pluginsInstances.forEach((plugin) => {
867
912
  plugin.onInit?.(editor);
868
913
  });
@@ -902,6 +947,7 @@ var Kernel = class Kernel extends EventEmitter {
902
947
  theme: this.themes
903
948
  });
904
949
  this.headlessEditor = false;
950
+ this.registerRootLifecycle(editor);
905
951
  this.pluginsInstances.forEach((plugin) => {
906
952
  plugin.onInit?.(editor);
907
953
  });
@@ -955,6 +1001,9 @@ var Kernel = class Kernel extends EventEmitter {
955
1001
  this.historyState.current = null;
956
1002
  }
957
1003
  datasource.read(this.editor, content, options);
1004
+ this.pluginsInstances.forEach((plugin) => {
1005
+ plugin.onDocumentChange?.(type, content, options);
1006
+ });
958
1007
  this.emit("documentChange", type, content);
959
1008
  this.logger.debug(`📥 Set ${type} document`);
960
1009
  }
@@ -1219,6 +1268,34 @@ var Kernel = class Kernel extends EventEmitter {
1219
1268
  if (currentRootElement) currentRootElement.classList.remove(...classNames);
1220
1269
  };
1221
1270
  }
1271
+ registerRootListener(listener) {
1272
+ if (!this.editor || this.headlessEditor) return noop;
1273
+ return this.editor.registerRootListener(listener);
1274
+ }
1275
+ getEditorId() {
1276
+ return this.themes[EDITOR_THEME_KEY];
1277
+ }
1278
+ registerRootLifecycle(editor) {
1279
+ this.unregisterRootLifecycle?.();
1280
+ let rootEventCleanup;
1281
+ const clearRootEvent = () => {
1282
+ rootEventCleanup?.();
1283
+ rootEventCleanup = void 0;
1284
+ };
1285
+ const unregisterRootListener = editor.registerRootListener((rootElement) => {
1286
+ clearRootEvent();
1287
+ if (!rootElement) {
1288
+ unregisterEditorKernel(this.getEditorId());
1289
+ return;
1290
+ }
1291
+ registerEditorKernel(this.getEditorId(), this);
1292
+ rootEventCleanup = registerEvent(editor, rootElement);
1293
+ });
1294
+ this.unregisterRootLifecycle = () => {
1295
+ clearRootEvent();
1296
+ unregisterRootListener();
1297
+ };
1298
+ }
1222
1299
  runBeforeEditorInitLifecycle() {
1223
1300
  for (const hook of this.beforeEditorInitHooks) hook();
1224
1301
  }
@@ -1597,8 +1674,309 @@ function registerCursorNode(editor) {
1597
1674
  }, COMMAND_PRIORITY_HIGH));
1598
1675
  }
1599
1676
  //#endregion
1677
+ //#region src/plugins/litexml/diff-validation.ts
1678
+ const ACTIONABLE_DIFF_TYPES = /* @__PURE__ */ new Set([
1679
+ "add",
1680
+ "remove",
1681
+ "modify",
1682
+ "listItemAdd",
1683
+ "listItemModify",
1684
+ "listItemRemove"
1685
+ ]);
1686
+ const isActionableDiff = (node) => node?.type === "diff" && ACTIONABLE_DIFF_TYPES.has(node.diffType || "");
1687
+ const isElementNode = (node) => Array.isArray(node.children);
1688
+ const cloneNode = (node) => structuredClone(node);
1689
+ function hasActionableDiffDescendant(node) {
1690
+ return (node.children || []).some((child) => isActionableDiff(child) || hasActionableDiffDescendant(child));
1691
+ }
1692
+ /**
1693
+ * Validate the serialized public tree. Paths use serialized node ids, not
1694
+ * Lexical's private node map, so this remains stable across JSON reloads.
1695
+ */
1696
+ function collectIllegalNestedDiffPaths(root) {
1697
+ const violations = [];
1698
+ const visit = (node, ancestors, path) => {
1699
+ const actionableAncestor = [...ancestors].reverse().find(isActionableDiff);
1700
+ const rowDiffAncestor = [...ancestors].reverse().find((ancestor) => ancestor.type === "table-row-diff");
1701
+ const allowedCellDiffChild = ancestors.at(-1)?.type === "table-cell-diff" && isActionableDiff(node);
1702
+ if (isActionableDiff(node) && actionableAncestor && !allowedCellDiffChild) violations.push(`${path}:diff-inside-diff`);
1703
+ if (rowDiffAncestor) {
1704
+ if (node.type === "table-cell-diff") violations.push(`${path}:cell-inside-row-diff`);
1705
+ else if (isActionableDiff(node)) violations.push(`${path}:diff-inside-row-diff`);
1706
+ }
1707
+ if (actionableAncestor) {
1708
+ if (node.type === "table-cell-diff") violations.push(`${path}:cell-diff-inside-diff`);
1709
+ else if (node.type === "table-row-diff") violations.push(`${path}:row-diff-inside-diff`);
1710
+ }
1711
+ if (!isElementNode(node)) return;
1712
+ node.children?.forEach((child) => {
1713
+ visit(child, [...ancestors, node], `${path}/${child.id || child.type || "node"}`);
1714
+ });
1715
+ };
1716
+ visit(root, [], root.id || root.type || "root");
1717
+ return violations;
1718
+ }
1719
+ function findNewIllegalDiffPaths(previous, projected) {
1720
+ const previousCounts = /* @__PURE__ */ new Map();
1721
+ collectIllegalNestedDiffPaths(previous.root).forEach((path) => {
1722
+ previousCounts.set(path, (previousCounts.get(path) || 0) + 1);
1723
+ });
1724
+ const newPaths = [];
1725
+ const projectedCounts = /* @__PURE__ */ new Map();
1726
+ collectIllegalNestedDiffPaths(projected.root).forEach((path) => {
1727
+ const count = (projectedCounts.get(path) || 0) + 1;
1728
+ projectedCounts.set(path, count);
1729
+ if (count > (previousCounts.get(path) || 0)) newPaths.push(path);
1730
+ });
1731
+ return newPaths;
1732
+ }
1733
+ function findNodeLocation(root, id, ancestors = []) {
1734
+ if (root.id === id) return {
1735
+ ancestors,
1736
+ index: -1,
1737
+ node: root,
1738
+ parent: null
1739
+ };
1740
+ for (const [index, child] of (root.children || []).entries()) {
1741
+ if (child.id === id) return {
1742
+ ancestors: [...ancestors, root],
1743
+ index,
1744
+ node: child,
1745
+ parent: root
1746
+ };
1747
+ const nested = findNodeLocation(child, id, [...ancestors, root]);
1748
+ if (nested) return nested;
1749
+ }
1750
+ return null;
1751
+ }
1752
+ function replaceNode(location, replacements) {
1753
+ if (!location.parent || location.index < 0) return;
1754
+ location.parent.children?.splice(location.index, 1, ...replacements);
1755
+ }
1756
+ function createDiff(diffType, children) {
1757
+ return {
1758
+ children,
1759
+ diffType,
1760
+ type: "diff"
1761
+ };
1762
+ }
1763
+ function createDiffContent(side, children) {
1764
+ return {
1765
+ children,
1766
+ side,
1767
+ type: "diff-content"
1768
+ };
1769
+ }
1770
+ function projectModifyTarget(root, incoming) {
1771
+ if (!incoming.id) return;
1772
+ const location = findNodeLocation(root, incoming.id);
1773
+ if (!location) return;
1774
+ const target = location.node;
1775
+ const next = cloneNode(incoming);
1776
+ if (target.type === "tablerow" && location.parent?.type === "table") {
1777
+ replaceNode(location, [{
1778
+ children: cloneNode(target.children || []),
1779
+ diffType: "remove",
1780
+ type: "table-row-diff"
1781
+ }, {
1782
+ children: cloneNode(next.children || []),
1783
+ diffType: "add",
1784
+ type: "table-row-diff"
1785
+ }]);
1786
+ return;
1787
+ }
1788
+ if (target.type === "tablecell" || target.type === "table-cell-diff") {
1789
+ const existingDiff = (target.children || []).find((child) => child.type === "diff");
1790
+ if (existingDiff && target.type === "table-cell-diff") {
1791
+ existingDiff.children = cloneNode(next.children || []);
1792
+ return;
1793
+ }
1794
+ const after = existingDiff?.children?.find((child) => child.type === "diff-content" && child.side === "after");
1795
+ if (existingDiff?.diffType === "modify" && after) {
1796
+ after.children = cloneNode(next.children || []);
1797
+ return;
1798
+ }
1799
+ target.children = [createDiff("modify", [createDiffContent("before", cloneNode(target.children || [])), createDiffContent("after", cloneNode(next.children || []))])];
1800
+ return;
1801
+ }
1802
+ if (location.ancestors.some(isActionableDiff)) {
1803
+ replaceNode(location, [next]);
1804
+ return;
1805
+ }
1806
+ replaceNode(location, [createDiff("modify", [cloneNode(target), next])]);
1807
+ }
1808
+ function projectRemoveTarget(root, id) {
1809
+ const location = findNodeLocation(root, id);
1810
+ if (!location) return;
1811
+ if (location.ancestors.some(isActionableDiff)) {
1812
+ replaceNode(location, []);
1813
+ return;
1814
+ }
1815
+ const target = location.node;
1816
+ if (target.type === "tablecell" || target.type === "table-cell-diff") {
1817
+ replaceNode(location, [{
1818
+ children: [createDiff("remove", cloneNode(target.children || []))],
1819
+ diffType: "remove",
1820
+ type: "table-cell-diff"
1821
+ }]);
1822
+ return;
1823
+ }
1824
+ if (target.type === "tablerow" && location.parent?.type === "table") {
1825
+ replaceNode(location, [{
1826
+ children: cloneNode(target.children || []),
1827
+ diffType: "remove",
1828
+ type: "table-row-diff"
1829
+ }]);
1830
+ return;
1831
+ }
1832
+ replaceNode(location, [createDiff("remove", [cloneNode(target)])]);
1833
+ }
1834
+ function projectInsert(root, operation, readXml) {
1835
+ const referenceId = "beforeId" in operation ? operation.beforeId : operation.afterId;
1836
+ const reference = referenceId === "root" ? null : findNodeLocation(root, referenceId)?.node || null;
1837
+ const location = referenceId === "root" ? {
1838
+ ancestors: [root],
1839
+ index: "beforeId" in operation ? 0 : (root.children || []).length - 1,
1840
+ node: root.children?.[0] || root,
1841
+ parent: root
1842
+ } : findNodeLocation(root, referenceId);
1843
+ if (!location) return;
1844
+ const incoming = (readXml(operation.litexml).root?.children || []).map(cloneNode);
1845
+ if (incoming.length === 0 || !location.parent) return;
1846
+ const insertAt = "beforeId" in operation ? location.index : location.index + 1;
1847
+ if (incoming.every((node) => node.type === "tablecell") && reference?.type === "tablecell") {
1848
+ location.parent.children?.splice(insertAt, 0, ...incoming.map((node) => ({
1849
+ children: [createDiff("add", cloneNode(node.children || []))],
1850
+ diffType: "add",
1851
+ type: "table-cell-diff"
1852
+ })));
1853
+ return;
1854
+ }
1855
+ if (incoming.every((node) => node.type === "tablerow") && reference?.type === "tablerow") {
1856
+ location.parent.children?.splice(insertAt, 0, ...incoming.map((node) => ({
1857
+ children: cloneNode(node.children || []),
1858
+ diffType: "add",
1859
+ type: "table-row-diff"
1860
+ })));
1861
+ return;
1862
+ }
1863
+ const actionableAncestor = location.ancestors.findLast(isActionableDiff);
1864
+ const isInlineReference = [
1865
+ "text",
1866
+ "span",
1867
+ "link",
1868
+ "autolink",
1869
+ "linebreak"
1870
+ ].includes(reference?.type || "");
1871
+ const blockAncestor = [...location.ancestors].reverse().find((node) => [
1872
+ "paragraph",
1873
+ "heading",
1874
+ "listitem",
1875
+ "table",
1876
+ "tablerow",
1877
+ "tablecell"
1878
+ ].includes(node.type || ""));
1879
+ if (!actionableAncestor && blockAncestor && hasActionableDiffDescendant(blockAncestor)) {
1880
+ const wrapped = createDiff("modify", [cloneNode(blockAncestor), cloneNode(blockAncestor)]);
1881
+ const blockLocation = findNodeLocation(root, blockAncestor.id || "");
1882
+ if (blockLocation) replaceNode(blockLocation, [wrapped]);
1883
+ return;
1884
+ }
1885
+ if (actionableAncestor && !isInlineReference) {
1886
+ const originLocation = actionableAncestor.id ? findNodeLocation(root, actionableAncestor.id) : null;
1887
+ if (originLocation?.parent && originLocation.index >= 0) {
1888
+ const originInsertAt = "beforeId" in operation ? originLocation.index : originLocation.index + 1;
1889
+ originLocation.parent.children?.splice(originInsertAt, 0, ...incoming.map((node) => createDiff("add", [node])));
1890
+ return;
1891
+ }
1892
+ }
1893
+ const inserted = actionableAncestor && isInlineReference ? incoming : incoming.map((node) => createDiff("add", [node]));
1894
+ location.parent.children?.splice(insertAt, 0, ...inserted);
1895
+ }
1896
+ /**
1897
+ * Project one operation on a cloned serialized tree. The real Lexical tree is
1898
+ * untouched; callers compare illegal paths before/after and only dispatch safe
1899
+ * operations to the live editor.
1900
+ */
1901
+ function projectLiteXmlOperation(document, operation, readXml) {
1902
+ const projected = cloneNode(document);
1903
+ if (operation.action === "modify") (Array.isArray(operation.litexml) ? operation.litexml : [operation.litexml]).forEach((xml) => {
1904
+ (readXml(xml).root?.children || []).forEach((child) => {
1905
+ projectModifyTarget(projected.root, child);
1906
+ });
1907
+ });
1908
+ else if (operation.action === "remove") projectRemoveTarget(projected.root, operation.id);
1909
+ else projectInsert(projected.root, operation, readXml);
1910
+ return projected;
1911
+ }
1912
+ //#endregion
1913
+ //#region src/plugins/litexml/node/DiffContentNode.ts
1914
+ /** Groups one side of a modify diff so it can contain any number of block nodes. */
1915
+ var DiffContentNode = class DiffContentNode extends ElementNode {
1916
+ static getType() {
1917
+ return "diff-content";
1918
+ }
1919
+ static clone(node) {
1920
+ return new DiffContentNode(node.__side, node.__key);
1921
+ }
1922
+ static importJSON(serializedNode) {
1923
+ return $createDiffContentNode(serializedNode.side).updateFromJSON(serializedNode);
1924
+ }
1925
+ static importDOM() {
1926
+ return null;
1927
+ }
1928
+ constructor(side, key) {
1929
+ super(key);
1930
+ this.__side = side;
1931
+ }
1932
+ get side() {
1933
+ return this.__side;
1934
+ }
1935
+ setSide(side) {
1936
+ this.getWritable().__side = side;
1937
+ return this;
1938
+ }
1939
+ updateFromJSON(serializedNode) {
1940
+ return super.updateFromJSON(serializedNode).setSide(serializedNode.side);
1941
+ }
1942
+ exportJSON() {
1943
+ return {
1944
+ ...super.exportJSON(),
1945
+ side: this.__side,
1946
+ type: DiffContentNode.getType(),
1947
+ version: 1
1948
+ };
1949
+ }
1950
+ createDOM(_config, _editor) {
1951
+ const element = document.createElement("div");
1952
+ element.dataset.diffContent = this.__side;
1953
+ return element;
1954
+ }
1955
+ updateDOM(previousNode, dom) {
1956
+ if (previousNode.__side !== this.__side) dom.dataset.diffContent = this.__side;
1957
+ return false;
1958
+ }
1959
+ exportDOM(_editor) {
1960
+ const element = document.createElement("div");
1961
+ element.dataset.diffContent = this.__side;
1962
+ return { element };
1963
+ }
1964
+ canBeEmpty() {
1965
+ return true;
1966
+ }
1967
+ isInline() {
1968
+ return false;
1969
+ }
1970
+ };
1971
+ function $createDiffContentNode(side) {
1972
+ return $applyNodeReplacement(new DiffContentNode(side));
1973
+ }
1974
+ function $isDiffContentNode(node) {
1975
+ return node instanceof DiffContentNode;
1976
+ }
1977
+ //#endregion
1600
1978
  //#region src/plugins/litexml/node/DiffNode.ts
1601
- /** DiffNode - contains two block children: original and modified */
1979
+ /** DiffNode - contains the original and modified sides, optionally grouped into multiple blocks. */
1602
1980
  var DiffNode = class DiffNode extends CardLikeElementNode {
1603
1981
  static getType() {
1604
1982
  return "diff";
@@ -1690,6 +2068,83 @@ var DiffNode = class DiffNode extends CardLikeElementNode {
1690
2068
  function $createDiffNode(diffType = "unchanged") {
1691
2069
  return $applyNodeReplacement(new DiffNode(diffType));
1692
2070
  }
2071
+ function $isDiffNode(node) {
2072
+ return node instanceof DiffNode;
2073
+ }
2074
+ //#endregion
2075
+ //#region src/plugins/litexml/node/TableCellDiffNode.ts
2076
+ /**
2077
+ * Review-only table cell. The diff state lives on a TableCellNode subclass so
2078
+ * TableRowNode children remain cells throughout staging and approval.
2079
+ */
2080
+ var TableCellDiffNode = class TableCellDiffNode extends TableCellNode {
2081
+ static getType() {
2082
+ return "table-cell-diff";
2083
+ }
2084
+ static clone(node) {
2085
+ const clone = new TableCellDiffNode(node.__diffType, node.__changeId, node.getHeaderStyles(), node.getColSpan(), node.getWidth(), node.__key);
2086
+ clone.__rowSpan = node.__rowSpan;
2087
+ clone.__backgroundColor = node.__backgroundColor;
2088
+ clone.__verticalAlign = node.__verticalAlign;
2089
+ return clone;
2090
+ }
2091
+ static importJSON(serializedNode) {
2092
+ return $createTableCellDiffNode(serializedNode.diffType, serializedNode.changeId, serializedNode.headerState, serializedNode.colSpan, serializedNode.width).updateFromJSON(serializedNode);
2093
+ }
2094
+ constructor(diffType, changeId, headerState, colSpan, width, key) {
2095
+ super(headerState, colSpan, width, key);
2096
+ this.__diffType = diffType;
2097
+ this.__changeId = changeId;
2098
+ }
2099
+ createDOM(config) {
2100
+ const element = super.createDOM(config);
2101
+ const theme = config.theme.tableCellDiff;
2102
+ if (typeof theme === "string" && theme) element.classList.add(theme);
2103
+ this.syncDOMState(element);
2104
+ return element;
2105
+ }
2106
+ updateDOM(prevNode) {
2107
+ return super.updateDOM(prevNode) || prevNode.__diffType !== this.__diffType || prevNode.__changeId !== this.__changeId;
2108
+ }
2109
+ exportJSON() {
2110
+ return {
2111
+ ...super.exportJSON(),
2112
+ changeId: this.__changeId,
2113
+ diffType: this.__diffType,
2114
+ type: TableCellDiffNode.getType(),
2115
+ version: 1
2116
+ };
2117
+ }
2118
+ getChangeId() {
2119
+ return this.getLatest().__changeId;
2120
+ }
2121
+ getDiffType() {
2122
+ return this.getLatest().__diffType;
2123
+ }
2124
+ setChangeId(changeId) {
2125
+ this.getWritable().__changeId = changeId;
2126
+ return this;
2127
+ }
2128
+ setDiffType(diffType) {
2129
+ this.getWritable().__diffType = diffType;
2130
+ return this;
2131
+ }
2132
+ updateFromJSON(serializedNode) {
2133
+ return super.updateFromJSON(serializedNode).setDiffType(serializedNode.diffType).setChangeId(serializedNode.changeId);
2134
+ }
2135
+ syncDOMState(element) {
2136
+ element.contentEditable = "false";
2137
+ element.dataset.diffType = this.__diffType;
2138
+ if (this.__changeId) element.dataset.diffChangeId = this.__changeId;
2139
+ else delete element.dataset.diffChangeId;
2140
+ }
2141
+ };
2142
+ function $createTableCellDiffNode(diffType, changeId, headerState, colSpan, width) {
2143
+ return $applyNodeReplacement(new TableCellDiffNode(diffType, changeId, headerState, colSpan, width));
2144
+ }
2145
+ function $isTableCellDiffNode(node) {
2146
+ return node instanceof TableCellDiffNode;
2147
+ }
1693
2148
  //#endregion
1694
2149
  //#region src/plugins/litexml/utils/index.ts
1695
2150
  const getNumericId$1 = (id) => {
@@ -1748,11 +2203,333 @@ function charToId(char) {
1748
2203
  return String((parseInt(char, 36) - startId + maxId) * modInverse % maxId);
1749
2204
  }
1750
2205
  //#endregion
2206
+ //#region src/plugins/litexml/table-cell-diff.ts
2207
+ function copyCellStructure(source, target) {
2208
+ target.setRowSpan(source.getRowSpan());
2209
+ target.setBackgroundColor(source.getBackgroundColor());
2210
+ target.setVerticalAlign(source.getVerticalAlign());
2211
+ }
2212
+ function $createTableCellDiffFromCell(editor, cell, diffType, changeId) {
2213
+ const diffCell = $createTableCellDiffNode(diffType, changeId, cell.getHeaderStyles(), cell.getColSpan(), cell.getWidth());
2214
+ copyCellStructure(cell, diffCell);
2215
+ const diff = $createDiffNode(diffType);
2216
+ diff.append(...cell.getChildren().map((child) => $cloneNode(child, editor)));
2217
+ diffCell.append(diff);
2218
+ return diffCell;
2219
+ }
2220
+ function $createPlainTableCellFromDiff(editor, cell) {
2221
+ const plainCell = $createTableCellNode(cell.getHeaderStyles(), cell.getColSpan(), cell.getWidth());
2222
+ copyCellStructure(cell, plainCell);
2223
+ const diff = cell.getFirstChild();
2224
+ if (diff instanceof DiffNode) plainCell.append(...diff.getChildren().map((child) => $cloneNode(child, editor)));
2225
+ else plainCell.append(...cell.getChildren().map((child) => $cloneNode(child, editor)));
2226
+ return plainCell;
2227
+ }
2228
+ function $getTableForCell(cell) {
2229
+ const row = cell.getParent();
2230
+ if (!$isTableRowNode(row)) return null;
2231
+ const table = row.getParent();
2232
+ return $isTableNode(table) ? table : null;
2233
+ }
2234
+ function $getTableCellColumnIndex(cell) {
2235
+ const row = cell.getParent();
2236
+ if (!$isTableRowNode(row)) return -1;
2237
+ let index = 0;
2238
+ for (const sibling of row.getChildren()) {
2239
+ if (sibling === cell) return index;
2240
+ if ($isTableCellNode(sibling)) index += sibling.getColSpan();
2241
+ }
2242
+ return -1;
2243
+ }
2244
+ function $getLogicalRowWidth(cell) {
2245
+ const row = cell.getParent();
2246
+ if (!$isTableRowNode(row)) return 0;
2247
+ return row.getChildren().reduce((total, child) => total + ($isTableCellNode(child) ? child.getColSpan() : 0), 0);
2248
+ }
2249
+ function $getTableCellDiffGroup(cell) {
2250
+ const changeId = cell.getChangeId();
2251
+ const table = $getTableForCell(cell);
2252
+ if (!changeId || !table) return [cell];
2253
+ return $nodesOfType(TableCellDiffNode).filter((candidate) => candidate.getChangeId() === changeId && candidate.getDiffType() === cell.getDiffType() && $getTableForCell(candidate) === table);
2254
+ }
2255
+ function $updateTableWidthsForCellInsertion(table, rowWidthBefore, columnIndex, insertedSpan) {
2256
+ const currentWidths = table.getColWidths() || Array.from({ length: rowWidthBefore }, () => 250);
2257
+ if (rowWidthBefore < currentWidths.length) return;
2258
+ const fallback = currentWidths[Math.max(0, columnIndex - 1)] || 250;
2259
+ const next = [...currentWidths];
2260
+ next.splice(columnIndex, 0, ...Array.from({ length: insertedSpan }, () => fallback));
2261
+ table.setColWidths(next);
2262
+ }
2263
+ function $removeTableWidthsForCompleteCellGroup(table, group, columnIndex, span) {
2264
+ const rows = table.getChildren().filter($isTableRowNode);
2265
+ const rowKeys = new Set(group.map((cell) => cell.getParent()?.getKey()));
2266
+ if (rows.length !== rowKeys.size) return;
2267
+ const widths = table.getColWidths();
2268
+ if (!widths) return;
2269
+ const next = [...widths];
2270
+ next.splice(columnIndex, span);
2271
+ table.setColWidths(next);
2272
+ }
2273
+ function $shrinkTableWidthsAfterCellRemoval(table, columnIndex, span) {
2274
+ const widths = table.getColWidths();
2275
+ if (!widths) return;
2276
+ if (table.getChildren().reduce((max, row) => {
2277
+ if (!$isTableRowNode(row)) return max;
2278
+ return Math.max(max, row.getChildren().reduce((total, cell) => total + ($isTableCellNode(cell) ? cell.getColSpan() : 0), 0));
2279
+ }, 0) > widths.length - span) return;
2280
+ const next = [...widths];
2281
+ next.splice(columnIndex, span);
2282
+ table.setColWidths(next);
2283
+ }
2284
+ function $getMaximumLogicalColumnCount(table) {
2285
+ return table.getChildren().reduce((maximum, row) => {
2286
+ if (!$isTableRowNode(row)) return maximum;
2287
+ const columnCount = row.getChildren().reduce((total, cell) => {
2288
+ return total + ($isTableCellNode(cell) ? cell.getColSpan() : 0);
2289
+ }, 0);
2290
+ return Math.max(maximum, columnCount);
2291
+ }, 0);
2292
+ }
2293
+ function $hasLegacyTableCellDiffs() {
2294
+ return $nodesOfType(DiffNode).some((diff) => {
2295
+ return (diff.diffType === "add" || diff.diffType === "remove") && $isTableRowNode(diff.getParent()) && diff.getChildren().some($isTableCellNode);
2296
+ });
2297
+ }
2298
+ function $hasIncompleteTableWidths() {
2299
+ return $nodesOfType(TableNode).some((table) => {
2300
+ return (table.getColWidths()?.length || 0) < $getMaximumLogicalColumnCount(table);
2301
+ });
2302
+ }
2303
+ /**
2304
+ * Migrates the old `TableRow > DiffNode > TableCellNode` representation into
2305
+ * cell-shaped diff nodes and makes sure the table has a width for every
2306
+ * logical column. Safe to call repeatedly.
2307
+ */
2308
+ function $normalizeLegacyTableCellDiffs(editor) {
2309
+ let changed = false;
2310
+ $nodesOfType(DiffNode).forEach((legacyDiff) => {
2311
+ const diffType = legacyDiff.diffType;
2312
+ if (diffType !== "add" && diffType !== "remove" || !$isTableRowNode(legacyDiff.getParent())) return;
2313
+ const cells = legacyDiff.getChildren().filter($isTableCellNode);
2314
+ if (cells.length === 0) return;
2315
+ const changeId = `legacy-table-cell-${legacyDiff.getKey()}`;
2316
+ cells.forEach((cell) => {
2317
+ legacyDiff.insertBefore($createTableCellDiffFromCell(editor, cell, diffType, changeId));
2318
+ });
2319
+ legacyDiff.remove();
2320
+ changed = true;
2321
+ });
2322
+ $nodesOfType(TableNode).forEach((table) => {
2323
+ const requiredWidthCount = $getMaximumLogicalColumnCount(table);
2324
+ const currentWidths = table.getColWidths() || [];
2325
+ if (currentWidths.length >= requiredWidthCount) return;
2326
+ const fallback = currentWidths.at(-1) || 250;
2327
+ table.setColWidths([...currentWidths, ...Array.from({ length: requiredWidthCount - currentWidths.length }, () => fallback)]);
2328
+ changed = true;
2329
+ });
2330
+ return changed;
2331
+ }
2332
+ function registerLegacyTableCellDiffNormalization(editor) {
2333
+ let destroyed = false;
2334
+ let scheduled = false;
2335
+ const unregister = editor.registerUpdateListener(() => {
2336
+ if (scheduled || destroyed) return;
2337
+ scheduled = true;
2338
+ queueMicrotask(() => {
2339
+ scheduled = false;
2340
+ if (destroyed) return;
2341
+ if (!editor.getEditorState().read(() => $hasLegacyTableCellDiffs() || $hasIncompleteTableWidths())) return;
2342
+ editor.update(() => {
2343
+ $normalizeLegacyTableCellDiffs(editor);
2344
+ }, { discrete: true });
2345
+ });
2346
+ });
2347
+ return () => {
2348
+ destroyed = true;
2349
+ unregister();
2350
+ };
2351
+ }
2352
+ //#endregion
2353
+ //#region src/plugins/litexml/node/TableRowDiffNode.ts
2354
+ /**
2355
+ * A review-only table row. Keeping the diff state on a TableRowNode subclass
2356
+ * preserves Lexical's table invariant: TableNode children are always rows.
2357
+ */
2358
+ var TableRowDiffNode = class TableRowDiffNode extends TableRowNode {
2359
+ static getType() {
2360
+ return "table-row-diff";
2361
+ }
2362
+ static clone(node) {
2363
+ return new TableRowDiffNode(node.__diffType, node.__changeId, node.getHeight(), node.__key);
2364
+ }
2365
+ static importJSON(serializedNode) {
2366
+ return $createTableRowDiffNode(serializedNode.diffType, serializedNode.changeId, serializedNode.height).updateFromJSON(serializedNode);
2367
+ }
2368
+ constructor(diffType, changeId, height, key) {
2369
+ super(height, key);
2370
+ this.__diffType = diffType;
2371
+ this.__changeId = changeId;
2372
+ }
2373
+ createDOM(config) {
2374
+ const element = super.createDOM(config);
2375
+ const theme = config.theme.tableRowDiff;
2376
+ if (typeof theme === "string" && theme) element.classList.add(theme);
2377
+ this.syncDOMState(element);
2378
+ return element;
2379
+ }
2380
+ exportJSON() {
2381
+ return {
2382
+ ...super.exportJSON(),
2383
+ changeId: this.__changeId,
2384
+ diffType: this.__diffType,
2385
+ type: TableRowDiffNode.getType(),
2386
+ version: 1
2387
+ };
2388
+ }
2389
+ getChangeId() {
2390
+ return this.getLatest().__changeId;
2391
+ }
2392
+ getDiffType() {
2393
+ return this.getLatest().__diffType;
2394
+ }
2395
+ setChangeId(changeId) {
2396
+ this.getWritable().__changeId = changeId;
2397
+ return this;
2398
+ }
2399
+ setDiffType(diffType) {
2400
+ this.getWritable().__diffType = diffType;
2401
+ return this;
2402
+ }
2403
+ updateDOM(prevNode) {
2404
+ return super.updateDOM(prevNode) || prevNode.__diffType !== this.__diffType || prevNode.__changeId !== this.__changeId;
2405
+ }
2406
+ updateFromJSON(serializedNode) {
2407
+ return super.updateFromJSON(serializedNode).setDiffType(serializedNode.diffType).setChangeId(serializedNode.changeId);
2408
+ }
2409
+ syncDOMState(element) {
2410
+ element.dataset.diffType = this.__diffType;
2411
+ if (this.__changeId) element.dataset.diffChangeId = this.__changeId;
2412
+ else delete element.dataset.diffChangeId;
2413
+ element.contentEditable = "false";
2414
+ }
2415
+ };
2416
+ function $createTableRowDiffNode(diffType, changeId, height) {
2417
+ return $applyNodeReplacement(new TableRowDiffNode(diffType, changeId, height));
2418
+ }
2419
+ function $isTableRowDiffNode(node) {
2420
+ return node instanceof TableRowDiffNode;
2421
+ }
2422
+ //#endregion
2423
+ //#region src/plugins/litexml/table-row-diff.ts
2424
+ function $areTableRowStructuresCompatible(previousRow, nextRow) {
2425
+ const previousCells = previousRow.getChildren();
2426
+ const nextCells = nextRow.getChildren();
2427
+ if (previousCells.length !== nextCells.length || !previousCells.every($isTableCellNode) || !nextCells.every($isTableCellNode)) return false;
2428
+ return previousCells.every((previousCell, index) => {
2429
+ const nextCell = nextCells[index];
2430
+ return $isTableCellNode(nextCell) && previousCell.getColSpan() === nextCell.getColSpan() && previousCell.getRowSpan() === nextCell.getRowSpan();
2431
+ });
2432
+ }
2433
+ function $createTableRowDiffFromRow(editor, row, diffType, changeId) {
2434
+ const source = $cloneNode(row, editor);
2435
+ const diffRow = $createTableRowDiffNode(diffType, changeId, row.getHeight());
2436
+ diffRow.append(...source.getChildren());
2437
+ return diffRow;
2438
+ }
2439
+ function $createPlainTableRowFromDiff(editor, row) {
2440
+ const plainRow = $createTableRowNode(row.getHeight());
2441
+ plainRow.append(...row.getChildren().map((child) => $cloneNode(child, editor)));
2442
+ return plainRow;
2443
+ }
2444
+ function $getTableRowDiffPair(node) {
2445
+ const changeId = node.getChangeId();
2446
+ if (!changeId) return null;
2447
+ const matchingRows = $nodesOfType(TableRowDiffNode).filter((candidate) => candidate.getChangeId() === changeId);
2448
+ if (matchingRows.length !== 2) return null;
2449
+ const remove = matchingRows.find((candidate) => candidate.getDiffType() === "remove");
2450
+ const add = matchingRows.find((candidate) => candidate.getDiffType() === "add");
2451
+ if (!remove || !add) return null;
2452
+ const parent = remove.getParent();
2453
+ if (!$isTableNode(parent) || add.getParent() !== parent || remove.getNextSibling() !== add || !$areTableRowStructuresCompatible(remove, add)) return null;
2454
+ return {
2455
+ add,
2456
+ remove
2457
+ };
2458
+ }
2459
+ function $normalizeTableRowDiffPairs() {
2460
+ const rows = $nodesOfType(TableRowDiffNode);
2461
+ const groups = /* @__PURE__ */ new Map();
2462
+ rows.forEach((row) => {
2463
+ const changeId = row.getChangeId();
2464
+ if (!changeId) return;
2465
+ const group = groups.get(changeId) || [];
2466
+ group.push(row);
2467
+ groups.set(changeId, group);
2468
+ });
2469
+ let changed = false;
2470
+ groups.forEach((group) => {
2471
+ if (group.length === 2 ? $getTableRowDiffPair(group[0]) : null) return;
2472
+ group.forEach((row) => row.setChangeId());
2473
+ changed = true;
2474
+ });
2475
+ return changed;
2476
+ }
2477
+ function registerTableRowDiffNormalization(editor) {
2478
+ let destroyed = false;
2479
+ let scheduled = false;
2480
+ const unregister = editor.registerUpdateListener(() => {
2481
+ if (scheduled || destroyed) return;
2482
+ scheduled = true;
2483
+ queueMicrotask(() => {
2484
+ scheduled = false;
2485
+ if (destroyed) return;
2486
+ if (!editor.getEditorState().read(() => {
2487
+ return $nodesOfType(TableRowDiffNode).some((row) => row.getChangeId() && !$getTableRowDiffPair(row));
2488
+ })) return;
2489
+ editor.update(() => {
2490
+ $normalizeTableRowDiffPairs();
2491
+ });
2492
+ });
2493
+ });
2494
+ return () => {
2495
+ destroyed = true;
2496
+ unregister();
2497
+ };
2498
+ }
2499
+ //#endregion
1751
2500
  //#region src/plugins/litexml/command/index.ts
1752
2501
  const logger$7 = createDebugLogger("plugin", "litexml");
1753
2502
  function toArrayXml(litexml) {
1754
2503
  return Array.isArray(litexml) ? litexml : [litexml];
1755
2504
  }
2505
+ function hasNewIllegalDiffs(previous, projected) {
2506
+ return findNewIllegalDiffPaths(previous, projected);
2507
+ }
2508
+ function projectOperation(dataSource, document, operation) {
2509
+ try {
2510
+ const projected = projectLiteXmlOperation(document, operation, (xml) => dataSource.readLiteXMLToInode(xml));
2511
+ const newIllegalDiffs = hasNewIllegalDiffs(document, projected);
2512
+ if (newIllegalDiffs.length > 0) {
2513
+ logger$7.warn("⚠️ Skipping operation with illegal nested diff", newIllegalDiffs);
2514
+ return null;
2515
+ }
2516
+ return projected;
2517
+ } catch (error) {
2518
+ logger$7.error("❌ Failed to preflight LiteXML operation:", error);
2519
+ return null;
2520
+ }
2521
+ }
2522
+ function toProjectionOperation(operation) {
2523
+ if (operation.action === "remove") return {
2524
+ ...operation,
2525
+ id: charToId(operation.id)
2526
+ };
2527
+ if (operation.action === "insert") return {
2528
+ ...operation,
2529
+ ..."beforeId" in operation ? { beforeId: operation.beforeId === "root" ? "root" : charToId(operation.beforeId) } : { afterId: operation.afterId === "root" ? "root" : charToId(operation.afterId) }
2530
+ };
2531
+ return operation;
2532
+ }
1756
2533
  function tryParseChild(child, editor) {
1757
2534
  try {
1758
2535
  const oldNode = $getNodeByKey(child.id);
@@ -1769,15 +2546,56 @@ function tryParseChild(child, editor) {
1769
2546
  }
1770
2547
  }
1771
2548
  function handleReplaceForApplyDelay(oldNode, newNode, modifyBlockNodes, diffNodeMap, editor) {
1772
- const oldBlock = $closest(oldNode, (node) => node.isInline() === false);
1773
- if (!oldBlock) throw new Error("Old block node not found for diffing.");
1774
- if ($closest(oldNode, (node) => node.getType() === DiffNode.getType())) {
1775
- oldNode.replace(newNode, false);
2549
+ if ($isTableRowNode(oldNode) || $isTableRowNode(newNode)) {
2550
+ if (!$isTableRowNode(oldNode) || !$isTableRowNode(newNode) || !$isTableNode(oldNode.getParent()) || !$areTableRowStructuresCompatible(oldNode, newNode)) {
2551
+ logger$7.error(`❌ Invalid table row modification for row ${oldNode.getKey()}.`);
2552
+ return;
2553
+ }
2554
+ const changeId = `${oldNode.getKey()}:${newNode.getKey()}`;
2555
+ const removeRow = $createTableRowDiffFromRow(editor, oldNode, "remove", changeId);
2556
+ const addRow = $createTableRowDiffFromRow(editor, newNode, "add", changeId);
2557
+ oldNode.replace(removeRow, false);
2558
+ removeRow.insertAfter(addRow);
1776
2559
  return;
1777
2560
  }
1778
- if (oldNode === oldBlock) {
1779
- const diffNode = $createDiffNode("modify");
1780
- diffNode.append($cloneNode(oldBlock, editor), newNode);
2561
+ if ($isTableCellNode(oldNode) && $isTableCellNode(newNode)) {
2562
+ const existingDiff = oldNode.getChildren().find($isDiffNode);
2563
+ if (existingDiff) {
2564
+ if ($isTableCellDiffNode(oldNode)) {
2565
+ existingDiff.clear();
2566
+ newNode.getChildren().forEach((child) => {
2567
+ existingDiff.append($cloneNode(child, editor));
2568
+ });
2569
+ return;
2570
+ }
2571
+ const after = existingDiff.getChildren().find((child) => $isDiffContentNode(child) && child.side === "after");
2572
+ if (existingDiff.diffType === "modify" && after && $isDiffContentNode(after)) {
2573
+ after.clear();
2574
+ newNode.getChildren().forEach((child) => {
2575
+ after.append($cloneNode(child, editor));
2576
+ });
2577
+ return;
2578
+ }
2579
+ }
2580
+ const before = $createDiffContentNode("before");
2581
+ const after = $createDiffContentNode("after");
2582
+ oldNode.getChildren().forEach((child) => before.append($cloneNode(child, editor)));
2583
+ newNode.getChildren().forEach((child) => after.append($cloneNode(child, editor)));
2584
+ const diffNode = $createDiffNode("modify");
2585
+ diffNode.append(before, after);
2586
+ oldNode.clear();
2587
+ oldNode.append(diffNode);
2588
+ return;
2589
+ }
2590
+ const oldBlock = $closest(oldNode, (node) => node.isInline() === false);
2591
+ if (!oldBlock) throw new Error("Old block node not found for diffing.");
2592
+ if ($closest(oldNode, (node) => node.getType() === DiffNode.getType())) {
2593
+ oldNode.replace(newNode, false);
2594
+ return;
2595
+ }
2596
+ if (oldNode === oldBlock) {
2597
+ const diffNode = $createDiffNode("modify");
2598
+ diffNode.append($cloneNode(oldBlock, editor), newNode);
1781
2599
  oldNode.replace(diffNode, false);
1782
2600
  } else {
1783
2601
  if (!modifyBlockNodes.has(oldBlock.getKey())) {
@@ -1852,8 +2670,15 @@ function registerLiteXMLCommand(editor, dataSource) {
1852
2670
  else acc.push(cur);
1853
2671
  return acc;
1854
2672
  }, []);
2673
+ let projectedDocument = getKernelFromEditor(editor).getDocument("json");
2674
+ const safePayload = resultPayload.filter((item) => {
2675
+ const nextProjection = projectOperation(dataSource, projectedDocument, toProjectionOperation(item));
2676
+ if (!nextProjection) return false;
2677
+ projectedDocument = nextProjection;
2678
+ return true;
2679
+ });
1855
2680
  try {
1856
- resultPayload.forEach((item) => {
2681
+ safePayload.forEach((item) => {
1857
2682
  const { action } = item;
1858
2683
  switch (action) {
1859
2684
  case "modify": {
@@ -1882,14 +2707,39 @@ function registerLiteXMLCommand(editor, dataSource) {
1882
2707
  }
1883
2708
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_APPLY_COMMAND, (payload) => {
1884
2709
  const { litexml, delay } = payload;
1885
- handleModify(editor, dataSource, toArrayXml(litexml), delay);
2710
+ const arrayXml = toArrayXml(litexml);
2711
+ if (!delay) {
2712
+ handleModify(editor, dataSource, arrayXml, delay);
2713
+ return false;
2714
+ }
2715
+ const operation = {
2716
+ action: "modify",
2717
+ litexml
2718
+ };
2719
+ if (projectOperation(dataSource, getKernelFromEditor(editor).getDocument("json"), toProjectionOperation(operation))) handleModify(editor, dataSource, arrayXml, delay);
1886
2720
  return false;
1887
2721
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_REMOVE_COMMAND, (payload) => {
1888
2722
  const { id, delay } = payload;
1889
- handleRemove(editor, charToId(id), delay);
2723
+ const key = charToId(id);
2724
+ if (!delay) {
2725
+ handleRemove(editor, key, delay);
2726
+ return false;
2727
+ }
2728
+ const operation = {
2729
+ action: "remove",
2730
+ id
2731
+ };
2732
+ if (projectOperation(dataSource, getKernelFromEditor(editor).getDocument("json"), toProjectionOperation(operation))) handleRemove(editor, key, delay);
1890
2733
  return false;
1891
2734
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_INSERT_COMMAND, (payload) => {
1892
- handleInsert(editor, payload, dataSource);
2735
+ if (!payload.delay) {
2736
+ handleInsert(editor, payload, dataSource);
2737
+ return false;
2738
+ }
2739
+ if (projectOperation(dataSource, getKernelFromEditor(editor).getDocument("json"), toProjectionOperation({
2740
+ action: "insert",
2741
+ ...payload
2742
+ }))) handleInsert(editor, payload, dataSource);
1893
2743
  return false;
1894
2744
  }, COMMAND_PRIORITY_EDITOR));
1895
2745
  }
@@ -1942,9 +2792,32 @@ function handleRemove(editor, key, delay) {
1942
2792
  const node = $getNodeByKey(key);
1943
2793
  if (!node) return;
1944
2794
  if (!delay) {
2795
+ if ($isTableCellNode(node)) {
2796
+ const table = $getTableForCell(node);
2797
+ const columnIndex = $getTableCellColumnIndex(node);
2798
+ const span = node.getColSpan();
2799
+ node.remove();
2800
+ if (table && columnIndex >= 0) $shrinkTableWidthsAfterCellRemoval(table, columnIndex, span);
2801
+ return;
2802
+ }
1945
2803
  node.remove();
1946
2804
  return;
1947
2805
  }
2806
+ if ($isTableCellNode(node)) {
2807
+ const table = $getTableForCell(node);
2808
+ const columnIndex = $getTableCellColumnIndex(node);
2809
+ if (!table || columnIndex < 0) {
2810
+ logger$7.error(`❌ Table cell ${node.getKey()} is not attached to a valid table row.`);
2811
+ return;
2812
+ }
2813
+ const changeId = `${table.getKey()}:column:${columnIndex}`;
2814
+ node.replace($createTableCellDiffFromCell(editor, node, "remove", changeId), false);
2815
+ return;
2816
+ }
2817
+ if ($isTableRowNode(node) && $isTableNode(node.getParent())) {
2818
+ node.replace($createTableRowDiffFromRow(editor, node, "remove"), false);
2819
+ return;
2820
+ }
1948
2821
  if (node.isInline() === false) {
1949
2822
  const originDiffNode = $closest(node, (node) => node.getType() === DiffNode.getType());
1950
2823
  if (originDiffNode) {
@@ -2006,6 +2879,37 @@ function handleInsert(editor, payload, dataSource) {
2006
2879
  else referenceNode = $getNodeByKey(charToId(payload.afterId));
2007
2880
  if (!referenceNode) throw new Error("Reference node not found for insertion.");
2008
2881
  const newNodes = inode.root.children.map((child) => $parseSerializedNodeImpl(child, editor));
2882
+ const referencesTableCell = $isTableCellNode(referenceNode);
2883
+ const insertsOnlyTableCells = newNodes.length > 0 && newNodes.every($isTableCellNode);
2884
+ if (referencesTableCell || insertsOnlyTableCells) {
2885
+ if (!referencesTableCell || !insertsOnlyTableCells) {
2886
+ logger$7.error("❌ Table cells can only be inserted next to another cell in the same row.");
2887
+ return;
2888
+ }
2889
+ const cellReference = referenceNode;
2890
+ const table = $getTableForCell(cellReference);
2891
+ const referenceIndex = $getTableCellColumnIndex(cellReference);
2892
+ if (!table || referenceIndex < 0) {
2893
+ logger$7.error("❌ Table cell insertion requires a valid table parent.");
2894
+ return;
2895
+ }
2896
+ const rowWidthBefore = $getLogicalRowWidth(cellReference);
2897
+ const insertionIndex = isBefore ? referenceIndex : referenceIndex + cellReference.getColSpan();
2898
+ $updateTableWidthsForCellInsertion(table, rowWidthBefore, insertionIndex, newNodes.reduce((total, node) => total + ($isTableCellNode(node) ? node.getColSpan() : 0), 0));
2899
+ let spanOffset = 0;
2900
+ const cells = newNodes.map((node) => {
2901
+ const result = delay ? $createTableCellDiffFromCell(editor, node, "add", `${table.getKey()}:column:${insertionIndex + spanOffset}`) : node;
2902
+ spanOffset += node.getColSpan();
2903
+ return result;
2904
+ });
2905
+ if (isBefore) cells.reverse().forEach((cell) => {
2906
+ referenceNode = referenceNode.insertBefore(cell);
2907
+ });
2908
+ else cells.forEach((cell) => {
2909
+ referenceNode = referenceNode.insertAfter(cell);
2910
+ });
2911
+ return;
2912
+ }
2009
2913
  if (!delay) {
2010
2914
  if (isBefore) newNodes.reverse().forEach((node) => {
2011
2915
  referenceNode = referenceNode.insertBefore(node);
@@ -2015,6 +2919,25 @@ function handleInsert(editor, payload, dataSource) {
2015
2919
  });
2016
2920
  return;
2017
2921
  }
2922
+ const referencesTableRow = $isTableRowNode(referenceNode);
2923
+ const insertsOnlyTableRows = newNodes.every($isTableRowNode);
2924
+ if (referencesTableRow || insertsOnlyTableRows) {
2925
+ if (!referencesTableRow || !insertsOnlyTableRows || !$isTableNode(referenceNode.getParent())) {
2926
+ logger$7.error("❌ Table rows can only be inserted next to another row in the same table.");
2927
+ return;
2928
+ }
2929
+ if (isBefore) newNodes.reverse().forEach((node) => {
2930
+ if (!$isTableRowNode(node)) return;
2931
+ const diffRow = $createTableRowDiffFromRow(editor, node, "add");
2932
+ referenceNode = referenceNode.insertBefore(diffRow);
2933
+ });
2934
+ else newNodes.forEach((node) => {
2935
+ if (!$isTableRowNode(node)) return;
2936
+ const diffRow = $createTableRowDiffFromRow(editor, node, "add");
2937
+ referenceNode = referenceNode.insertAfter(diffRow);
2938
+ });
2939
+ return;
2940
+ }
2018
2941
  if (isBefore) {
2019
2942
  if (referenceNode.isInline() === false) {
2020
2943
  const originDiffNode = $closest(referenceNode, (node) => node.getType() === DiffNode.getType());
@@ -2076,11 +2999,69 @@ function handleInsert(editor, payload, dataSource) {
2076
2999
  }
2077
3000
  //#endregion
2078
3001
  //#region src/plugins/litexml/command/diffCommand.ts
2079
- function doAction(node, action) {
3002
+ function doTableRowAction(editor, node, action) {
3003
+ const pair = $getTableRowDiffPair(node);
3004
+ if (pair) {
3005
+ const survivingRow = action === 1 ? pair.add : pair.remove;
3006
+ const discardedRow = action === 1 ? pair.remove : pair.add;
3007
+ const plainRow = $createPlainTableRowFromDiff(editor, survivingRow);
3008
+ discardedRow.remove();
3009
+ survivingRow.replace(plainRow, false);
3010
+ plainRow.selectStart();
3011
+ return;
3012
+ }
3013
+ if (node.getDiffType() === "remove") {
3014
+ if (action === 1) node.remove();
3015
+ else {
3016
+ const plainRow = $createPlainTableRowFromDiff(editor, node);
3017
+ node.replace(plainRow, false);
3018
+ plainRow.selectStart();
3019
+ }
3020
+ return;
3021
+ }
3022
+ if (action === 1) {
3023
+ const plainRow = $createPlainTableRowFromDiff(editor, node);
3024
+ node.replace(plainRow, false);
3025
+ plainRow.selectStart();
3026
+ } else node.remove();
3027
+ }
3028
+ function doTableCellAction(editor, node, action) {
3029
+ const group = $getTableCellDiffGroup(node);
3030
+ const table = $getTableForCell(node);
3031
+ const columnIndex = $getTableCellColumnIndex(node);
3032
+ const span = node.getColSpan();
3033
+ if (node.getDiffType() === "add" && action === 1 || node.getDiffType() === "remove" && action === 0) {
3034
+ group.forEach((cell) => cell.replace($createPlainTableCellFromDiff(editor, cell), false));
3035
+ return;
3036
+ }
3037
+ if (table && columnIndex >= 0) {
3038
+ const widthCount = table.getColWidths()?.length;
3039
+ $removeTableWidthsForCompleteCellGroup(table, group, columnIndex, span);
3040
+ group.forEach((cell) => cell.remove());
3041
+ if (table.getColWidths()?.length === widthCount) $shrinkTableWidthsAfterCellRemoval(table, columnIndex, span);
3042
+ return;
3043
+ }
3044
+ group.forEach((cell) => cell.remove());
3045
+ }
3046
+ function doAction(editor, node, action) {
3047
+ if ($isTableRowDiffNode(node)) {
3048
+ doTableRowAction(editor, node, action);
3049
+ return;
3050
+ }
3051
+ const parent = node.getParent();
3052
+ if ($isTableCellDiffNode(parent)) {
3053
+ doTableCellAction(editor, parent, action);
3054
+ return;
3055
+ }
2080
3056
  if (node.diffType === "modify") {
2081
3057
  const children = node.getChildren();
2082
- if (action === 1) node.replace(children[1], false).selectEnd();
2083
- else if (action === 0) node.replace(children[0], false).selectEnd();
3058
+ const selectedChild = action === 1 ? children[1] : children[0];
3059
+ if ($isDiffContentNode(selectedChild)) {
3060
+ const parent = node.getParentOrThrow();
3061
+ selectedChild.getChildren().forEach((child) => node.insertBefore(child));
3062
+ node.remove();
3063
+ if ($isElementNode(parent)) parent.selectEnd();
3064
+ } else if (selectedChild) node.replace(selectedChild, false).selectEnd();
2084
3065
  }
2085
3066
  if (node.diffType === "remove") {
2086
3067
  if (action === 1) node.remove();
@@ -2092,7 +3073,10 @@ function doAction(node, action) {
2092
3073
  if (node.diffType === "add") {
2093
3074
  if (action === 1) {
2094
3075
  const children = node.getChildren();
2095
- node.replace(children[0], false).selectEnd();
3076
+ const parent = node.getParentOrThrow();
3077
+ children.forEach((child) => node.insertBefore(child));
3078
+ node.remove();
3079
+ if ($isElementNode(parent)) parent.selectEnd();
2096
3080
  } else if (action === 0) node.remove();
2097
3081
  }
2098
3082
  if (node.diffType === "listItemModify") {
@@ -2138,23 +3122,37 @@ function doAction(node, action) {
2138
3122
  function registerLiteXMLDiffCommand(editor) {
2139
3123
  return mergeRegister(editor.registerCommand(LITEXML_DIFFNODE_COMMAND, (payload) => {
2140
3124
  const { action, nodeKey } = payload;
2141
- const node = editor.getEditorState().read(() => {
2142
- return $getNodeByKey(nodeKey);
2143
- });
2144
- if (!node) return false;
2145
3125
  editor.update(() => {
2146
- doAction(node, action);
3126
+ const node = $getNodeByKey(nodeKey);
3127
+ if (!(node instanceof DiffNode) && !$isTableRowDiffNode(node)) return;
3128
+ doAction(editor, node, action);
2147
3129
  });
2148
3130
  return false;
2149
3131
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_DIFFNODE_ALL_COMMAND, (payload) => {
2150
3132
  const { action } = payload;
2151
- const nodes = editor.getEditorState().read(() => {
2152
- return Array.from(editor._editorState._nodeMap.values()).filter((n) => n instanceof DiffNode && !!n.getParent());
3133
+ const nodeKeys = editor.getEditorState().read(() => {
3134
+ return Array.from(editor._editorState._nodeMap.values()).filter((node) => (node instanceof DiffNode || $isTableRowDiffNode(node)) && !!node.getParent()).map((node) => node.getKey());
2153
3135
  });
2154
- if (!nodes.length) return false;
3136
+ if (!nodeKeys.length) return false;
2155
3137
  editor.update(() => {
2156
- nodes.forEach((node) => {
2157
- doAction(node, action);
3138
+ const handled = /* @__PURE__ */ new Set();
3139
+ nodeKeys.forEach((nodeKey) => {
3140
+ if (handled.has(nodeKey)) return;
3141
+ const node = $getNodeByKey(nodeKey);
3142
+ if (!(node instanceof DiffNode) && !$isTableRowDiffNode(node)) return;
3143
+ const pair = $isTableRowDiffNode(node) ? $getTableRowDiffPair(node) : null;
3144
+ if (pair) {
3145
+ handled.add(pair.remove.getKey());
3146
+ handled.add(pair.add.getKey());
3147
+ } else {
3148
+ const parent = node instanceof DiffNode ? node.getParent() : null;
3149
+ if ($isTableCellDiffNode(parent)) $getTableCellDiffGroup(parent).forEach((cell) => {
3150
+ const diff = cell.getFirstChild();
3151
+ if (diff instanceof DiffNode) handled.add(diff.getKey());
3152
+ });
3153
+ handled.add(nodeKey);
3154
+ }
3155
+ doAction(editor, node, action);
2158
3156
  });
2159
3157
  });
2160
3158
  return false;
@@ -2587,8 +3585,29 @@ function convertMdastToLexical(node, index, ctx, markdownReaders = {}, parentTyp
2587
3585
  return ret;
2588
3586
  }
2589
3587
  if (child.value.startsWith("<!--") && child.value.endsWith("-->")) return ret;
2590
- const tag = child.value.replaceAll(/^<\/?|>$/g, "");
3588
+ const tag = getHtmlTagName$1(child.value);
2591
3589
  const isEndTag = child.value.startsWith("</");
3590
+ const pairedTag = child.value.match(/^<\s*([a-z0-9-]+)\b[^>]*>([\s\S]*)<\/\s*\1\s*>$/i);
3591
+ if (!isEndTag && pairedTag) {
3592
+ const reader = markdownReaders["html"];
3593
+ const htmlChildren = pairedTag[2] ? [INodeHelper.createTextNode(pairedTag[2])] : [];
3594
+ if (Array.isArray(reader)) for (const element of reader) {
3595
+ const inode = element(child, htmlChildren, index);
3596
+ if (inode) {
3597
+ ret.push(inode);
3598
+ return ret;
3599
+ }
3600
+ }
3601
+ else if (typeof reader === "function") {
3602
+ const inode = reader(child, htmlChildren, index);
3603
+ if (inode) {
3604
+ ret.push(inode);
3605
+ return ret;
3606
+ }
3607
+ }
3608
+ ret.push(INodeHelper.createTextNode(child.value));
3609
+ return ret;
3610
+ }
2592
3611
  if (selfClosingHtmlTags.has(tag)) {
2593
3612
  const reader = markdownReaders["html"];
2594
3613
  if (Array.isArray(reader)) for (const element of reader) {
@@ -2675,6 +3694,9 @@ function convertMdastToLexical(node, index, ctx, markdownReaders = {}, parentTyp
2675
3694
  }
2676
3695
  }
2677
3696
  }
3697
+ function getHtmlTagName$1(value) {
3698
+ return value.match(/^<\/?\s*([a-z0-9-]+)/i)?.[1]?.toLowerCase() || value.replaceAll(/^<\/?|>$/g, "");
3699
+ }
2678
3700
  function registerDefaultReaders(markdownReaders) {
2679
3701
  if (!markdownReaders["root"]) markdownReaders["root"] = (node, children) => {
2680
3702
  return {
@@ -3552,6 +4574,7 @@ function detectCodeLanguage(code) {
3552
4574
  //#region src/plugins/markdown/plugin/index.ts
3553
4575
  const DEFAULT_PASTE_MARKDOWN_AUTO_CONVERT_THRESHOLD = 5;
3554
4576
  const RICH_HTML_SELECTOR = "strong,em,b,i,h1,h2,h3,h4,h5,h6,ul,ol,table,img,blockquote,pre>code,a[href]";
4577
+ const normalizeMarkdownForClipboard = (markdown) => markdown.replace(/\r?\n$/, "");
3555
4578
  const MARKDOWN_DETECTION_RULES = [
3556
4579
  {
3557
4580
  name: "headers",
@@ -3748,8 +4771,9 @@ const MarkdownPlugin = class extends KernelPlugin {
3748
4771
  if (!clipboardData) return false;
3749
4772
  const selection = $getSelection();
3750
4773
  if (!selection || selection.isCollapsed()) return false;
3751
- const markdown = this.markdownDataSource.write(editor, { selection: true });
3752
- if (!markdown) return false;
4774
+ const rawMarkdown = this.markdownDataSource.write(editor, { selection: true });
4775
+ if (!rawMarkdown) return false;
4776
+ const markdown = normalizeMarkdownForClipboard(rawMarkdown);
3753
4777
  event.preventDefault();
3754
4778
  const data = $getClipboardDataFromSelection(selection);
3755
4779
  setLexicalClipboardDataTransfer(clipboardData, data);
@@ -3835,14 +4859,23 @@ const LitexmlPlugin = class extends KernelPlugin {
3835
4859
  super();
3836
4860
  this.kernel = kernel;
3837
4861
  this.config = config;
3838
- kernel.registerThemes({ diffNode: config?.theme || "editor_diffNode" });
4862
+ kernel.registerThemes({
4863
+ diffNode: config?.theme || "editor_diffNode",
4864
+ tableRowDiff: config?.tableRowDiffTheme || "editor_tableRowDiff",
4865
+ tableCellDiff: config?.tableCellDiffTheme || "editor_tableCellDiff"
4866
+ });
3839
4867
  const litexmlService = new LitexmlService();
3840
4868
  this.service = litexmlService;
3841
4869
  kernel.registerService(ILitexmlService, litexmlService);
3842
4870
  this.datasource = new LitexmlDataSource("litexml", (serviceId) => {
3843
4871
  return kernel.requireService(serviceId);
3844
4872
  }, litexmlService);
3845
- kernel.registerNodes([DiffNode]);
4873
+ kernel.registerNodes([
4874
+ DiffNode,
4875
+ DiffContentNode,
4876
+ TableRowDiffNode,
4877
+ TableCellDiffNode
4878
+ ]);
3846
4879
  kernel.registerDecorator(DiffNode.getType(), {
3847
4880
  queryDOM: (el) => el.querySelector(".toolbar"),
3848
4881
  render: (node, editor) => {
@@ -3854,6 +4887,8 @@ const LitexmlPlugin = class extends KernelPlugin {
3854
4887
  onInit(editor) {
3855
4888
  this.register(registerLiteXMLCommand(editor, this.datasource));
3856
4889
  this.register(registerLiteXMLDiffCommand(editor));
4890
+ this.register(registerLegacyTableCellDiffNormalization(editor));
4891
+ this.register(registerTableRowDiffNormalization(editor));
3857
4892
  this.register(editor.registerNodeTransform(DiffNode, (node) => {
3858
4893
  if (node.diffType === "modify" && node.getChildrenSize() === 1) node.setDiffType("remove");
3859
4894
  }));
@@ -3878,11 +4913,14 @@ const LitexmlPlugin = class extends KernelPlugin {
3878
4913
  const diffNode = node;
3879
4914
  const lines = [];
3880
4915
  switch (diffNode.diffType) {
3881
- case "modify":
3882
- nodeToXML(diffNode.getChildAtIndex(1), lines, indent);
4916
+ case "modify": {
4917
+ const after = diffNode.getChildAtIndex(1);
4918
+ if ($isDiffContentNode(after)) after.getChildren().forEach((child) => nodeToXML(child, lines, indent));
4919
+ else nodeToXML(after, lines, indent);
3883
4920
  break;
4921
+ }
3884
4922
  case "add":
3885
- nodeToXML(diffNode.getChildAtIndex(0), lines, indent);
4923
+ diffNode.getChildren().forEach((child) => nodeToXML(child, lines, indent));
3886
4924
  break;
3887
4925
  case "remove": break;
3888
4926
  case "listItemModify": diffNode.getChildAtIndex(1).getChildren().forEach((child) => {
@@ -3891,16 +4929,49 @@ const LitexmlPlugin = class extends KernelPlugin {
3891
4929
  }
3892
4930
  return { lines };
3893
4931
  });
4932
+ this.service.registerXMLWriter(TableRowDiffNode.getType(), (node, ctx) => {
4933
+ if (!$isTableRowDiffNode(node)) return false;
4934
+ if (node.getDiffType() === "remove") return { lines: [] };
4935
+ return ctx.createXmlNode("tr", {});
4936
+ });
4937
+ this.service.registerXMLWriter(TableCellDiffNode.getType(), (node, ctx) => {
4938
+ if (!$isTableCellDiffNode(node)) return false;
4939
+ if (node.getDiffType() === "remove") return { lines: [] };
4940
+ const attributes = {};
4941
+ if (node.getColSpan() > 1) attributes.colSpan = String(node.getColSpan());
4942
+ if (node.getRowSpan() > 1) attributes.rowSpan = String(node.getRowSpan());
4943
+ if (node.getBackgroundColor()) attributes.backgroundColor = node.getBackgroundColor();
4944
+ return ctx.createXmlNode("td", attributes);
4945
+ });
3894
4946
  }
3895
4947
  registerMarkdown() {
3896
- this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(DiffNode.getType(), (ctx, node) => {
4948
+ const markdownService = this.kernel.requireService(IMarkdownShortCutService);
4949
+ markdownService?.registerMarkdownWriter(TableCellDiffNode.getType(), (ctx, node) => {
4950
+ if (!$isTableCellDiffNode(node)) return false;
4951
+ if (node.getDiffType() === "remove") return true;
4952
+ if (!node.getNextSibling()) ctx.wrap("|", "|");
4953
+ else ctx.wrap("|", "");
4954
+ return true;
4955
+ });
4956
+ markdownService?.registerMarkdownWriter(TableRowDiffNode.getType(), (ctx, node) => {
4957
+ if (!$isTableRowDiffNode(node)) return false;
4958
+ if (node.getDiffType() === "remove") return true;
4959
+ const parent = node.getParent();
4960
+ if (!$isTableNode(parent)) return true;
4961
+ if (node.getPreviousSiblings().some((sibling) => !$isTableRowDiffNode(sibling) || sibling.getDiffType() !== "remove")) ctx.wrap("", "\n");
4962
+ else ctx.wrap("", `\n${parent.getColWidths()?.map(() => "|:--").join("")}|\n`);
4963
+ });
4964
+ markdownService?.registerMarkdownWriter(DiffNode.getType(), (ctx, node) => {
3897
4965
  const diffNode = node;
3898
4966
  switch (diffNode.diffType) {
3899
- case "modify":
3900
- ctx.processChild(ctx, diffNode.getChildAtIndex(1));
4967
+ case "modify": {
4968
+ const after = diffNode.getChildAtIndex(1);
4969
+ if ($isDiffContentNode(after)) after.getChildren().forEach((child) => ctx.processChild(ctx, child));
4970
+ else ctx.processChild(ctx, after);
3901
4971
  break;
4972
+ }
3902
4973
  case "add":
3903
- ctx.processChild(ctx, diffNode.getChildAtIndex(0));
4974
+ diffNode.getChildren().forEach((child) => ctx.processChild(ctx, child));
3904
4975
  break;
3905
4976
  case "remove": break;
3906
4977
  case "listItemModify": diffNode.getChildAtIndex(1).getChildren().forEach((child) => {
@@ -4414,6 +5485,84 @@ function runPasteHandlers(ctx, handlers) {
4414
5485
  return false;
4415
5486
  }
4416
5487
  //#endregion
5488
+ //#region src/plugins/common/plugin/progressive-select-all.ts
5489
+ const COLLAPSIBLE_NODE_TYPE = "collapsible";
5490
+ function registerProgressiveSelectAll(editor) {
5491
+ return editor.registerCommand(SELECT_ALL_COMMAND, () => {
5492
+ const selection = $getSelection();
5493
+ if ($isTableSelection(selection)) return $expandTableSelection(selection);
5494
+ if (!$isRangeSelection(selection)) return false;
5495
+ const anchorCell = $findNearestNode(selection.anchor.getNode(), $isTableCellNode);
5496
+ const focusCell = $findNearestNode(selection.focus.getNode(), $isTableCellNode);
5497
+ if (anchorCell && focusCell && anchorCell.is(focusCell)) {
5498
+ const table = $findNearestNode(anchorCell, $isTableNode);
5499
+ if (table) {
5500
+ const tableSelection = $createTableSelection();
5501
+ tableSelection.set(table.getKey(), anchorCell.getKey(), anchorCell.getKey());
5502
+ $setSelection(tableSelection);
5503
+ return true;
5504
+ }
5505
+ }
5506
+ const anchorCollapsible = $findCollapsibleAncestor(selection.anchor.getNode());
5507
+ const focusCollapsible = $findCollapsibleAncestor(selection.focus.getNode());
5508
+ if (!anchorCollapsible || !focusCollapsible || !anchorCollapsible.is(focusCollapsible)) return false;
5509
+ if ($isEntireElementSelected(selection, anchorCollapsible)) $selectDocument();
5510
+ else anchorCollapsible.select(0, anchorCollapsible.getChildrenSize());
5511
+ return true;
5512
+ }, COMMAND_PRIORITY_HIGH);
5513
+ }
5514
+ function $expandTableSelection(selection) {
5515
+ const table = $getNodeByKey(selection.tableKey);
5516
+ if (!$isTableNode(table)) return false;
5517
+ const cells = $getTableCells(table);
5518
+ if (!cells) return false;
5519
+ const selectedCellKeys = new Set(selection.getNodes().filter($isTableCellNode).map((cell) => cell.getKey()));
5520
+ if (cells.keys.every((key) => selectedCellKeys.has(key))) {
5521
+ $selectDocument();
5522
+ return true;
5523
+ }
5524
+ const tableSelection = $createTableSelection();
5525
+ tableSelection.set(table.getKey(), cells.first.getKey(), cells.last.getKey());
5526
+ $setSelection(tableSelection);
5527
+ return true;
5528
+ }
5529
+ function $getTableCells(table) {
5530
+ if (!$isTableNode(table)) return null;
5531
+ const [tableMap] = $computeTableMapSkipCellCheck(table, null, null);
5532
+ const entries = tableMap.flat().filter(Boolean);
5533
+ const first = entries[0]?.cell;
5534
+ const last = entries.at(-1)?.cell;
5535
+ if (!first || !last) return null;
5536
+ return {
5537
+ first,
5538
+ keys: Array.from(new Set(entries.map(({ cell }) => cell.getKey()))),
5539
+ last
5540
+ };
5541
+ }
5542
+ function $findNearestNode(node, predicate) {
5543
+ let current = node;
5544
+ while (current) {
5545
+ if (predicate(current)) return current;
5546
+ current = current.getParent();
5547
+ }
5548
+ return null;
5549
+ }
5550
+ function $findCollapsibleAncestor(node) {
5551
+ return $findNearestNode(node, (candidate) => $isElementNode(candidate) && candidate.getType() === COLLAPSIBLE_NODE_TYPE);
5552
+ }
5553
+ function $isEntireElementSelected(selection, element) {
5554
+ const startOffset = 0;
5555
+ const endOffset = element.getChildrenSize();
5556
+ return $isPointAt(selection.anchor, element, startOffset) && $isPointAt(selection.focus, element, endOffset) || $isPointAt(selection.focus, element, startOffset) && $isPointAt(selection.anchor, element, endOffset);
5557
+ }
5558
+ function $isPointAt(point, element, offset) {
5559
+ return point.key === element.getKey() && point.offset === offset && point.type === "element";
5560
+ }
5561
+ function $selectDocument() {
5562
+ const root = $getRoot();
5563
+ root.select(0, root.getChildrenSize());
5564
+ }
5565
+ //#endregion
4417
5566
  //#region src/plugins/common/plugin/register.ts
4418
5567
  const logger$3 = createDebugLogger("plugin", "common");
4419
5568
  function resolveElement(element, isBackward, focusOffset) {
@@ -4429,7 +5578,9 @@ function resolveElement(element, isBackward, focusOffset) {
4429
5578
  block = parent;
4430
5579
  }
4431
5580
  }
4432
- return block.getChildAtIndex(isBackward ? offset - 1 : offset);
5581
+ const childIndex = isBackward ? offset - 1 : offset;
5582
+ if (childIndex < 0 || childIndex >= block.getChildrenSize()) return null;
5583
+ return block.getChildAtIndex(childIndex);
4433
5584
  }
4434
5585
  function isCodeNodeLastLine(focusNode) {
4435
5586
  if (!$isCodeHighlightNode(focusNode)) return false;
@@ -4474,7 +5625,55 @@ function $isSelectionAtEndOfRoot(selection) {
4474
5625
  const focus = selection.focus;
4475
5626
  return focus.key === "root" && focus.offset === $getRoot().getChildrenSize();
4476
5627
  }
4477
- function registerHeaderBackspace(editor) {
5628
+ function $isSelectionInList(selection) {
5629
+ return Boolean($closest(selection.focus.getNode(), (node) => $isListNode(node) || $isListItemNode(node)));
5630
+ }
5631
+ function $getLeadingQuoteNode(selection) {
5632
+ const anchor = selection.anchor;
5633
+ if (anchor.offset !== 0) return false;
5634
+ let node = anchor.getNode();
5635
+ let current = node;
5636
+ let quoteNode = null;
5637
+ while (current) {
5638
+ if ($isQuoteNode(current)) {
5639
+ quoteNode = current;
5640
+ break;
5641
+ }
5642
+ current = current.getParent();
5643
+ }
5644
+ if (!quoteNode) return false;
5645
+ while (node !== quoteNode) {
5646
+ if (node.getIndexWithinParent() !== 0) return false;
5647
+ const parent = node.getParent();
5648
+ if (!parent) return false;
5649
+ node = parent;
5650
+ }
5651
+ return quoteNode;
5652
+ }
5653
+ function $unwrapQuoteNode(quoteNode) {
5654
+ const children = quoteNode.getChildren();
5655
+ if (children.length === 0) {
5656
+ const paragraphNode = $createParagraphNode();
5657
+ quoteNode.replace(paragraphNode);
5658
+ paragraphNode.select(0, 0);
5659
+ return;
5660
+ }
5661
+ let firstNode = null;
5662
+ for (const child of children) {
5663
+ if ($isElementNode(child)) {
5664
+ quoteNode.insertBefore(child);
5665
+ firstNode ||= child;
5666
+ continue;
5667
+ }
5668
+ const paragraphNode = $createParagraphNode();
5669
+ paragraphNode.append(child);
5670
+ quoteNode.insertBefore(paragraphNode);
5671
+ firstNode ||= paragraphNode;
5672
+ }
5673
+ quoteNode.remove();
5674
+ if ($isElementNode(firstNode)) firstNode.select(0, 0);
5675
+ }
5676
+ function registerBlockBackspace(editor) {
4478
5677
  return editor.registerCommand(KEY_BACKSPACE_COMMAND, (payload) => {
4479
5678
  const headingNode = editor.getEditorState().read(() => {
4480
5679
  const selection = $getSelection();
@@ -4502,6 +5701,22 @@ function registerHeaderBackspace(editor) {
4502
5701
  });
4503
5702
  return true;
4504
5703
  }
5704
+ if (editor.getEditorState().read(() => {
5705
+ const selection = $getSelection();
5706
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false;
5707
+ return $getLeadingQuoteNode(selection);
5708
+ })) {
5709
+ payload.stopImmediatePropagation();
5710
+ payload.preventDefault();
5711
+ payload.stopPropagation();
5712
+ editor.update(() => {
5713
+ const selection = $getSelection();
5714
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) return;
5715
+ const leadingQuoteNode = $getLeadingQuoteNode(selection);
5716
+ if (leadingQuoteNode) $unwrapQuoteNode(leadingQuoteNode);
5717
+ });
5718
+ return true;
5719
+ }
4505
5720
  return false;
4506
5721
  }, COMMAND_PRIORITY_NORMAL);
4507
5722
  }
@@ -4556,6 +5771,7 @@ function registerRichKeydown(editor, kernel, options) {
4556
5771
  return true;
4557
5772
  }
4558
5773
  } else if ($isRangeSelection(selection)) {
5774
+ if ($isSelectionInList(selection)) return false;
4559
5775
  const possibleNode = $getAdjacentNode(selection.focus, true);
4560
5776
  const upblock = possibleNode || $getDownUpNode(selection.focus, true);
4561
5777
  if (!event.shiftKey && $isDecoratorNode(possibleNode)) {
@@ -4625,6 +5841,7 @@ function registerRichKeydown(editor, kernel, options) {
4625
5841
  event.preventDefault();
4626
5842
  return true;
4627
5843
  }
5844
+ if ($isSelectionInList(selection)) return false;
4628
5845
  const possibleNode = $getAdjacentNode(selection.focus, false);
4629
5846
  const upblock = possibleNode || $getDownUpNode(selection.focus, false);
4630
5847
  if (!event.shiftKey && $isDecoratorNode(possibleNode)) {
@@ -4919,7 +6136,18 @@ const CommonPlugin = class extends KernelPlugin {
4919
6136
  if (this.config?.pasteAsPlainText) return runPasteHandlers(ctx, [handleFilePaste, handlePlainTextPaste]);
4920
6137
  return false;
4921
6138
  }, COMMAND_PRIORITY_CRITICAL));
4922
- this.registerClears(registerRichText(editor), CAN_USE_DOM ? registerDragonSupport(editor) : noop, registerHistory(editor, this.kernel.getHistoryState(), 300), registerHeaderBackspace(editor), registerRichKeydown(editor, this.kernel, { enableHotkey: this.config?.enableHotkey }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor), editor.registerCommand(INSERT_LINE_BREAK_COMMAND, () => {
6139
+ let dragonCleanup;
6140
+ const clearDragon = () => {
6141
+ dragonCleanup?.();
6142
+ dragonCleanup = void 0;
6143
+ };
6144
+ this.register(this.kernel.registerRootListener((rootElement) => {
6145
+ clearDragon();
6146
+ if (!rootElement || !CAN_USE_DOM) return;
6147
+ dragonCleanup = registerDragonSupport(editor);
6148
+ }));
6149
+ this.register(clearDragon);
6150
+ this.registerClears(registerRichText(editor), registerProgressiveSelectAll(editor), registerHistory(editor, this.kernel.getHistoryState(), 300), registerBlockBackspace(editor), registerRichKeydown(editor, this.kernel, { enableHotkey: this.config?.enableHotkey }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor), editor.registerCommand(INSERT_LINE_BREAK_COMMAND, () => {
4923
6151
  const selection = $getSelection();
4924
6152
  if (!$isRangeSelection(selection)) return false;
4925
6153
  const { inCell, inTable } = $isCursorInTable(selection);
@@ -5793,6 +7021,62 @@ function modeMatch(mode = "") {
5793
7021
  return LANGUAGES.find((m) => m.value === mode || m.ext?.includes(mode))?.value || "plain";
5794
7022
  }
5795
7023
  //#endregion
7024
+ //#region src/plugins/codemirror-block/service/index.ts
7025
+ const ICodemirrorEditLockService = genServiceId("CodemirrorEditLockService");
7026
+ var CodemirrorEditLockService = class {
7027
+ constructor() {
7028
+ this.providerUnsubscribers = /* @__PURE__ */ new Map();
7029
+ this.providers = /* @__PURE__ */ new Set();
7030
+ this.subscribers = /* @__PURE__ */ new Set();
7031
+ }
7032
+ acquireLock(key, label) {
7033
+ if (this.getRemoteLockOwner(key)) return false;
7034
+ const acquiredProviders = [];
7035
+ for (const provider of this.providers) {
7036
+ if (!provider.acquireLock(key, label)) {
7037
+ acquiredProviders.forEach((acquiredProvider) => acquiredProvider.releaseLock(key));
7038
+ this.notify();
7039
+ return false;
7040
+ }
7041
+ acquiredProviders.push(provider);
7042
+ }
7043
+ this.notify();
7044
+ return true;
7045
+ }
7046
+ getRemoteLockOwner(key) {
7047
+ for (const provider of this.providers) {
7048
+ const owner = provider.getRemoteLockOwner(key);
7049
+ if (owner) return owner;
7050
+ }
7051
+ return null;
7052
+ }
7053
+ registerProvider(provider) {
7054
+ this.providers.add(provider);
7055
+ this.providerUnsubscribers.set(provider, provider.subscribe(() => this.notify()));
7056
+ this.notify();
7057
+ return () => {
7058
+ this.providerUnsubscribers.get(provider)?.();
7059
+ this.providerUnsubscribers.delete(provider);
7060
+ this.providers.delete(provider);
7061
+ this.notify();
7062
+ };
7063
+ }
7064
+ releaseLock(key) {
7065
+ this.providers.forEach((provider) => provider.releaseLock(key));
7066
+ this.notify();
7067
+ }
7068
+ subscribe(listener) {
7069
+ this.subscribers.add(listener);
7070
+ listener();
7071
+ return () => {
7072
+ this.subscribers.delete(listener);
7073
+ };
7074
+ }
7075
+ notify() {
7076
+ this.subscribers.forEach((listener) => listener());
7077
+ }
7078
+ };
7079
+ //#endregion
5796
7080
  //#region src/plugins/codemirror-block/plugin/index.ts
5797
7081
  const CodemirrorPlugin = class extends KernelPlugin {
5798
7082
  static {
@@ -5803,6 +7087,7 @@ const CodemirrorPlugin = class extends KernelPlugin {
5803
7087
  this.kernel = kernel;
5804
7088
  kernel.registerNodes([CodeMirrorNode]);
5805
7089
  kernel.registerThemes({ hr: config?.theme || "" });
7090
+ kernel.registerServiceHotReload(ICodemirrorEditLockService, new CodemirrorEditLockService());
5806
7091
  this.registerDecorator(kernel, "codemirror", (node, editor) => {
5807
7092
  return config?.decorator ? config.decorator(node, editor) : null;
5808
7093
  });
@@ -6762,62 +8047,320 @@ const ImagePlugin = class extends KernelPlugin {
6762
8047
  }
6763
8048
  };
6764
8049
  //#endregion
6765
- //#region src/plugins/link/node/LinkNode.ts
6766
- const logger = createDebugLogger("plugin", "link");
6767
- const SUPPORTED_URL_PROTOCOLS$1 = /* @__PURE__ */ new Set([
6768
- "http:",
6769
- "https:",
6770
- "mailto:",
6771
- "sms:",
6772
- "tel:"
6773
- ]);
6774
- const HOVER_LINK_COMMAND = createCommand("HOVER_LINK_COMMAND");
6775
- const HOVER_OUT_LINK_COMMAND = createCommand("HOVER_OUT_LINK_COMMAND");
6776
- /** @noInheritDoc */
6777
- var LinkNode = class LinkNode extends ElementNode {
6778
- static getType() {
6779
- return "link";
8050
+ //#region src/plugins/link/service/i-link-service.ts
8051
+ const ILinkService = genServiceId("LinkService");
8052
+ const DEFAULT_LABELS = {
8053
+ convertToBlockCard: "Convert to block card",
8054
+ convertToCard: "Convert to card",
8055
+ convertToIframe: "Convert to iframe",
8056
+ convertToLink: "Convert to link",
8057
+ convertToSchema: "Convert to schema"
8058
+ };
8059
+ var LinkProtocolPolicy = class {
8060
+ constructor() {
8061
+ this.allowedProtocols = /* @__PURE__ */ new Set([
8062
+ "http:",
8063
+ "https:",
8064
+ "mailto:",
8065
+ "sms:",
8066
+ "tel:"
8067
+ ]);
6780
8068
  }
6781
- static clone(node) {
6782
- return new LinkNode(node.__url, {
6783
- rel: node.__rel,
6784
- target: node.__target,
6785
- title: node.__title
6786
- }, node.__key);
8069
+ getAllowedProtocols() {
8070
+ return this.allowedProtocols;
6787
8071
  }
6788
- constructor(url = "", attributes = {}, key) {
6789
- super(key);
6790
- const { target = null, rel = null, title = null } = attributes;
6791
- this.__url = url;
6792
- this.__target = target;
6793
- this.__rel = rel;
6794
- this.__title = title;
8072
+ setAllowedProtocols(protocols) {
8073
+ this.allowedProtocols = new Set(protocols.map(normalizeProtocol));
6795
8074
  }
6796
- createDOM(config, editor) {
6797
- logger.debug("🔍 config", config);
6798
- const element = document.createElement("a");
6799
- this.updateLinkDOM(null, element, config);
6800
- addClassNamesToElement(element, config.theme.link);
6801
- element.addEventListener("mouseenter", (event) => {
6802
- if (event.target instanceof HTMLElement) {
6803
- event.target.classList.add("hover");
6804
- editor.dispatchCommand(HOVER_LINK_COMMAND, {
6805
- event,
6806
- linkNode: this
6807
- });
6808
- }
6809
- });
6810
- element.addEventListener("mouseleave", (event) => {
6811
- if (event.target instanceof HTMLElement) {
6812
- event.target.classList.remove("hover");
6813
- editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
6814
- }
6815
- });
6816
- return element;
8075
+ };
8076
+ var LinkRuleRegistry = class {
8077
+ constructor() {
8078
+ this.embedRules = [];
8079
+ this.schemaRules = [];
6817
8080
  }
6818
- updateLinkDOM(prevNode, anchor, _config) {
6819
- if (isHTMLAnchorElement(anchor)) {
6820
- if (!prevNode || prevNode.__url !== this.__url) anchor.href = this.sanitizeUrl(this.__url);
8081
+ getEmbedRule(url, context) {
8082
+ return this.embedRules.find((rule) => rule.match(url, context)) || null;
8083
+ }
8084
+ getSchemaRule(url, context, parseSchemaUrl) {
8085
+ const schema = context.schema === void 0 ? parseSchemaUrl(url) : context.schema;
8086
+ return this.schemaRules.find((rule) => rule.match(url, {
8087
+ ...context,
8088
+ schema
8089
+ })) || null;
8090
+ }
8091
+ getSchemaRuleById(schemaType) {
8092
+ return this.schemaRules.find((rule) => rule.id === schemaType) || null;
8093
+ }
8094
+ setEmbedRules(rules = []) {
8095
+ this.embedRules = rules;
8096
+ }
8097
+ setSchemaRules(rules = []) {
8098
+ this.schemaRules = rules;
8099
+ }
8100
+ };
8101
+ var LegacySchemaLinkRegistry = class {
8102
+ constructor() {
8103
+ this.protocols = /* @__PURE__ */ new Set();
8104
+ }
8105
+ hasSchemaLinkRenderer(url) {
8106
+ try {
8107
+ const { protocol } = new URL(url);
8108
+ return this.protocols.has(protocol);
8109
+ } catch {
8110
+ return false;
8111
+ }
8112
+ }
8113
+ setSchemaLinkRenderers(renderers = []) {
8114
+ this.protocols = new Set(renderers.map(({ protocol }) => normalizeProtocol(protocol)));
8115
+ }
8116
+ };
8117
+ var LinkToolbarController = class {
8118
+ constructor() {
8119
+ this.baseEnabled = true;
8120
+ this.labels = DEFAULT_LABELS;
8121
+ this.toolbarActions = [];
8122
+ this.toolbarSuppressions = /* @__PURE__ */ new Set();
8123
+ }
8124
+ get enabled() {
8125
+ return this.baseEnabled && this.toolbarSuppressions.size === 0;
8126
+ }
8127
+ getLabels() {
8128
+ return this.labels;
8129
+ }
8130
+ getToolbarActions(context) {
8131
+ return this.toolbarActions.filter((action) => action.visible?.(context) ?? true);
8132
+ }
8133
+ setLabels(labels) {
8134
+ this.labels = {
8135
+ ...DEFAULT_LABELS,
8136
+ ...labels
8137
+ };
8138
+ }
8139
+ setToolbarActions(actions = []) {
8140
+ this.toolbarActions = actions;
8141
+ }
8142
+ setEnabled(enable) {
8143
+ const previousEnabled = this.enabled;
8144
+ this.baseEnabled = enable;
8145
+ return previousEnabled !== this.enabled;
8146
+ }
8147
+ suppress(reason = "unknown") {
8148
+ const previousEnabled = this.enabled;
8149
+ const token = Symbol(reason);
8150
+ this.toolbarSuppressions.add(token);
8151
+ return {
8152
+ changed: previousEnabled !== this.enabled,
8153
+ token
8154
+ };
8155
+ }
8156
+ restore(token) {
8157
+ const previousEnabled = this.enabled;
8158
+ if (!this.toolbarSuppressions.delete(token)) return false;
8159
+ return previousEnabled !== this.enabled;
8160
+ }
8161
+ };
8162
+ var LinkService = class extends EventEmitter {
8163
+ constructor(..._args) {
8164
+ super(..._args);
8165
+ this.cardMetadataLoadingKeys = /* @__PURE__ */ new Set();
8166
+ this.protocolPolicy = new LinkProtocolPolicy();
8167
+ this.legacySchemaLinkRegistry = new LegacySchemaLinkRegistry();
8168
+ this.ruleRegistry = new LinkRuleRegistry();
8169
+ this.toolbarController = new LinkToolbarController();
8170
+ this.toolbarItems = /* @__PURE__ */ new Map();
8171
+ }
8172
+ get enableLinkToolbar() {
8173
+ return this.toolbarController.enabled;
8174
+ }
8175
+ getAllowedProtocols() {
8176
+ return this.protocolPolicy.getAllowedProtocols();
8177
+ }
8178
+ getEmbedRule(url, context) {
8179
+ return this.ruleRegistry.getEmbedRule(url, context);
8180
+ }
8181
+ getLabels() {
8182
+ return this.toolbarController.getLabels();
8183
+ }
8184
+ hasSchemaLinkRenderer(url) {
8185
+ return this.legacySchemaLinkRegistry.hasSchemaLinkRenderer(url);
8186
+ }
8187
+ isCardMetadataLoading(nodeKey) {
8188
+ return this.cardMetadataLoadingKeys.has(nodeKey);
8189
+ }
8190
+ setCardMetadataLoading(nodeKey, loading) {
8191
+ if (!(loading ? !this.cardMetadataLoadingKeys.has(nodeKey) : this.cardMetadataLoadingKeys.has(nodeKey))) return;
8192
+ if (loading) this.cardMetadataLoadingKeys.add(nodeKey);
8193
+ else this.cardMetadataLoadingKeys.delete(nodeKey);
8194
+ this.emit("cardMetadataChange");
8195
+ }
8196
+ getSchemaRule(url, context) {
8197
+ return this.ruleRegistry.getSchemaRule(url, context, (url) => this.parseSchemaUrl(url));
8198
+ }
8199
+ getToolbarActions(context) {
8200
+ return this.toolbarController.getToolbarActions(context);
8201
+ }
8202
+ parseSchemaUrl(url) {
8203
+ try {
8204
+ const parsedUrl = new URL(url);
8205
+ return {
8206
+ hash: parsedUrl.hash,
8207
+ host: parsedUrl.host,
8208
+ params: Object.fromEntries(parsedUrl.searchParams.entries()),
8209
+ pathname: parsedUrl.pathname,
8210
+ protocol: parsedUrl.protocol,
8211
+ raw: url,
8212
+ search: parsedUrl.search
8213
+ };
8214
+ } catch {
8215
+ return null;
8216
+ }
8217
+ }
8218
+ updateConfig(config) {
8219
+ this.setAllowedProtocols([
8220
+ "http:",
8221
+ "https:",
8222
+ "mailto:",
8223
+ "sms:",
8224
+ "tel:",
8225
+ ...config?.schemaLinkRenderers?.map(({ protocol }) => protocol) || [],
8226
+ ...config?.allowedProtocols || []
8227
+ ]);
8228
+ this.setEmbedRules(config?.linkEmbedRules);
8229
+ this.setLabels(config?.labels);
8230
+ this.setSchemaLinkRenderers(config?.schemaLinkRenderers);
8231
+ this.setSchemaRules([...config?.schemaRules || [], ...createLegacySchemaRules(config?.schemaLinkRenderers)]);
8232
+ this.setToolbarActions(config?.toolbarActions);
8233
+ }
8234
+ setAllowedProtocols(protocols) {
8235
+ this.protocolPolicy.setAllowedProtocols(protocols);
8236
+ }
8237
+ setEmbedRules(rules = []) {
8238
+ this.ruleRegistry.setEmbedRules(rules);
8239
+ }
8240
+ setLabels(labels) {
8241
+ this.toolbarController.setLabels(labels);
8242
+ }
8243
+ setSchemaLinkRenderers(renderers = []) {
8244
+ this.legacySchemaLinkRegistry.setSchemaLinkRenderers(renderers);
8245
+ }
8246
+ setSchemaRules(rules = []) {
8247
+ this.ruleRegistry.setSchemaRules(rules);
8248
+ }
8249
+ setToolbarActions(actions = []) {
8250
+ this.toolbarController.setToolbarActions(actions);
8251
+ this.emit("change");
8252
+ }
8253
+ getToolbarItems(context) {
8254
+ return Array.from(this.toolbarItems.values()).filter((item) => item.when ? item.when(context) : true).sort((a, b) => (a.order || 0) - (b.order || 0));
8255
+ }
8256
+ registerToolbarItem(item) {
8257
+ this.toolbarItems.set(item.key, item);
8258
+ this.emit("change");
8259
+ return () => {
8260
+ this.toolbarItems.delete(item.key);
8261
+ this.emit("change");
8262
+ };
8263
+ }
8264
+ setLinkToolbar(enable) {
8265
+ if (this.toolbarController.setEnabled(enable)) this.emit("linkToolbarChange", enable);
8266
+ }
8267
+ suppressLinkToolbar(reason = "unknown") {
8268
+ const { changed, token } = this.toolbarController.suppress(reason);
8269
+ if (changed) this.emit("linkToolbarChange", this.toolbarController.enabled);
8270
+ return token;
8271
+ }
8272
+ restoreLinkToolbar(token) {
8273
+ if (this.toolbarController.restore(token)) this.emit("linkToolbarChange", this.toolbarController.enabled);
8274
+ }
8275
+ subscribe(listener) {
8276
+ this.on("change", listener);
8277
+ return () => {
8278
+ this.off("change", listener);
8279
+ };
8280
+ }
8281
+ subscribeCardMetadata(listener) {
8282
+ this.on("cardMetadataChange", listener);
8283
+ return () => {
8284
+ this.off("cardMetadataChange", listener);
8285
+ };
8286
+ }
8287
+ };
8288
+ function normalizeProtocol(protocol) {
8289
+ return `${protocol.split(":")[0]}:`;
8290
+ }
8291
+ function createLegacySchemaRules(renderers = []) {
8292
+ return renderers.map(({ protocol }) => {
8293
+ const normalizedProtocol = normalizeProtocol(protocol);
8294
+ return {
8295
+ id: normalizedProtocol.replace(":", ""),
8296
+ match: (url) => {
8297
+ try {
8298
+ return new URL(url).protocol === normalizedProtocol;
8299
+ } catch {
8300
+ return false;
8301
+ }
8302
+ }
8303
+ };
8304
+ });
8305
+ }
8306
+ //#endregion
8307
+ //#region src/plugins/link/node/LinkNode.ts
8308
+ const logger = createDebugLogger("plugin", "link");
8309
+ const SUPPORTED_URL_PROTOCOLS$1 = /* @__PURE__ */ new Set([
8310
+ "http:",
8311
+ "https:",
8312
+ "mailto:",
8313
+ "sms:",
8314
+ "tel:"
8315
+ ]);
8316
+ const HOVER_LINK_COMMAND = createCommand("HOVER_LINK_COMMAND");
8317
+ const HOVER_OUT_LINK_COMMAND = createCommand("HOVER_OUT_LINK_COMMAND");
8318
+ /** @noInheritDoc */
8319
+ var LinkNode = class LinkNode extends ElementNode {
8320
+ static getType() {
8321
+ return "link";
8322
+ }
8323
+ static clone(node) {
8324
+ return new LinkNode(node.__url, {
8325
+ rel: node.__rel,
8326
+ target: node.__target,
8327
+ title: node.__title
8328
+ }, node.__key);
8329
+ }
8330
+ constructor(url = "", attributes = {}, key) {
8331
+ super(key);
8332
+ const { target = null, rel = null, title = null } = attributes;
8333
+ this.__url = url;
8334
+ this.__target = target;
8335
+ this.__rel = rel;
8336
+ this.__title = title;
8337
+ }
8338
+ createDOM(config, editor) {
8339
+ logger.debug("🔍 config", config);
8340
+ const element = document.createElement("a");
8341
+ this.updateLinkDOM(null, element, config);
8342
+ addClassNamesToElement(element, config.theme.link);
8343
+ element.addEventListener("mouseenter", (event) => {
8344
+ if (event.target instanceof HTMLElement) {
8345
+ event.target.classList.add("hover");
8346
+ editor.dispatchCommand(HOVER_LINK_COMMAND, {
8347
+ event,
8348
+ node: this
8349
+ });
8350
+ }
8351
+ });
8352
+ element.addEventListener("mouseleave", (event) => {
8353
+ if (event.target instanceof HTMLElement) {
8354
+ event.target.classList.remove("hover");
8355
+ editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
8356
+ }
8357
+ });
8358
+ return element;
8359
+ }
8360
+ updateLinkDOM(prevNode, anchor, _config) {
8361
+ if (isHTMLAnchorElement(anchor)) {
8362
+ const linkService = getLinkServiceFromConfig(_config);
8363
+ if (!prevNode || prevNode.__url !== this.__url) anchor.href = this.sanitizeUrl(this.__url, linkService?.getAllowedProtocols());
6821
8364
  for (const attr of [
6822
8365
  "target",
6823
8366
  "rel",
@@ -6848,11 +8391,11 @@ var LinkNode = class LinkNode extends ElementNode {
6848
8391
  updateFromJSON(serializedNode) {
6849
8392
  return super.updateFromJSON(serializedNode).setURL(serializedNode.url).setRel(serializedNode.rel || null).setTarget(serializedNode.target || null).setTitle(serializedNode.title || null);
6850
8393
  }
6851
- sanitizeUrl(url) {
8394
+ sanitizeUrl(url, allowedProtocols = SUPPORTED_URL_PROTOCOLS$1) {
6852
8395
  url = formatUrl(url);
6853
8396
  try {
6854
8397
  const parsedUrl = new URL(formatUrl(url));
6855
- if (!SUPPORTED_URL_PROTOCOLS$1.has(parsedUrl.protocol)) return "about:blank";
8398
+ if (!allowedProtocols.has(parsedUrl.protocol)) return "about:blank";
6856
8399
  } catch {
6857
8400
  return url;
6858
8401
  }
@@ -6932,7 +8475,13 @@ var LinkNode = class LinkNode extends ElementNode {
6932
8475
  isWebSiteURI() {
6933
8476
  return this.__url.startsWith("https://") || this.__url.startsWith("http://");
6934
8477
  }
8478
+ getDOMSlot(element) {
8479
+ return super.getDOMSlot(element);
8480
+ }
6935
8481
  };
8482
+ function getLinkServiceFromConfig(config) {
8483
+ return getKernelFromEditorConfig(config)?.requireService(ILinkService) || null;
8484
+ }
6936
8485
  function $convertAnchorElement(domNode) {
6937
8486
  let node = null;
6938
8487
  if (isHTMLAnchorElement(domNode)) {
@@ -6960,7 +8509,7 @@ function $createLinkNode(url = "", attributes) {
6960
8509
  * @returns true if node is a LinkNode, false otherwise.
6961
8510
  */
6962
8511
  function $isLinkNode(node) {
6963
- return node instanceof LinkNode;
8512
+ return node instanceof LinkNode || node?.getType?.() === "link" || node?.getType?.() === "autolink";
6964
8513
  }
6965
8514
  var AutoLinkNode = class AutoLinkNode extends LinkNode {
6966
8515
  constructor(url = "", attributes = {}, key) {
@@ -7208,6 +8757,7 @@ function formatUrl(url) {
7208
8757
  //#region src/plugins/link/command/index.ts
7209
8758
  const INSERT_LINK_COMMAND = createCommand("INSERT_LINK_COMMAND");
7210
8759
  const UPDATE_LINK_TEXT_COMMAND = createCommand("UPDATE_LINK_TEXT_COMMAND");
8760
+ const UNLINK_LINK_COMMAND = createCommand("UNLINK_LINK_COMMAND");
7211
8761
  function registerLinkCommand(editor) {
7212
8762
  return mergeRegister(editor.registerCommand(INSERT_LINK_COMMAND, (payload) => {
7213
8763
  const { url, title = url } = payload;
@@ -7217,38 +8767,564 @@ function registerLinkCommand(editor) {
7217
8767
  linkNode.append(textNode);
7218
8768
  $insertNodes([linkNode]);
7219
8769
  });
7220
- return false;
7221
- }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(UPDATE_LINK_TEXT_COMMAND, (payload) => {
7222
- const { key, text } = payload;
7223
- editor.update(() => {
7224
- const linkNode = $getNodeByKey(key);
7225
- if (linkNode) {
7226
- const newLinkNode = $createLinkNode(linkNode.getURL(), { title: text });
7227
- const textNode = $createTextNode(text);
7228
- newLinkNode.append(textNode);
7229
- linkNode?.replace(newLinkNode);
7230
- newLinkNode.select(1);
8770
+ return false;
8771
+ }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(UPDATE_LINK_TEXT_COMMAND, (payload) => {
8772
+ const { key, text } = payload;
8773
+ editor.update(() => {
8774
+ const linkNode = $getNodeByKey(key);
8775
+ if (linkNode) {
8776
+ const newLinkNode = $createLinkNode(linkNode.getURL(), { title: text });
8777
+ const textNode = $createTextNode(text);
8778
+ newLinkNode.append(textNode);
8779
+ linkNode?.replace(newLinkNode);
8780
+ newLinkNode.select(1);
8781
+ }
8782
+ });
8783
+ return false;
8784
+ }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(UNLINK_LINK_COMMAND, ({ key }) => {
8785
+ editor.update(() => {
8786
+ const node = $getNodeByKey(key);
8787
+ if (!$isLinkNode(node)) return;
8788
+ let selection = $getSelection();
8789
+ if (!selection || !$isRangeSelection(selection)) {
8790
+ $setSelection($createRangeSelection());
8791
+ selection = $getSelection();
8792
+ }
8793
+ const first = node.getFirstDescendant();
8794
+ const last = node.getLastDescendant();
8795
+ if (selection && $isRangeSelection(selection) && $isTextNode(first) && $isTextNode(last)) {
8796
+ selection.anchor.set(first.getKey(), 0, "text");
8797
+ selection.focus.set(last.getKey(), last.getTextContentSize(), "text");
8798
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
8799
+ return;
8800
+ }
8801
+ const children = node.getChildren();
8802
+ for (const child of children) node.insertBefore(child);
8803
+ node.remove();
8804
+ });
8805
+ return true;
8806
+ }, COMMAND_PRIORITY_EDITOR));
8807
+ }
8808
+ createCommand();
8809
+ var LinkCardNode = class LinkCardNode extends DecoratorNode {
8810
+ static getType() {
8811
+ return "link-card";
8812
+ }
8813
+ static clone(node) {
8814
+ return new LinkCardNode(node.__url, node.__title, node.__icon, node.__description, node.__openTarget, node.__key);
8815
+ }
8816
+ static importJSON(serializedNode) {
8817
+ return $createLinkCardNode({
8818
+ description: serializedNode.description,
8819
+ icon: serializedNode.icon,
8820
+ openTarget: serializedNode.openTarget,
8821
+ title: serializedNode.title,
8822
+ url: serializedNode.url
8823
+ }).updateFromJSON(serializedNode);
8824
+ }
8825
+ static importDOM() {
8826
+ return { a: (node) => {
8827
+ if (node instanceof HTMLAnchorElement && node.dataset.linkCard === "true" && node.dataset.linkCardLayout !== "block") return {
8828
+ conversion: $convertLinkCardElement,
8829
+ priority: 2
8830
+ };
8831
+ return null;
8832
+ } };
8833
+ }
8834
+ constructor(url, title, icon, description, openTarget, key) {
8835
+ super(key);
8836
+ this.__url = url;
8837
+ this.__title = title || url;
8838
+ this.__icon = icon || "";
8839
+ this.__description = description || "";
8840
+ this.__openTarget = openTarget ?? "_blank";
8841
+ }
8842
+ createDOM(config, editor) {
8843
+ const element = document.createElement("span");
8844
+ element.dataset.linkCard = "true";
8845
+ addClassNamesToElement(element, config.theme.linkCard);
8846
+ element.addEventListener("mouseenter", (event) => {
8847
+ if (event.target instanceof HTMLElement) {
8848
+ event.target.classList.add("hover");
8849
+ editor.dispatchCommand(HOVER_LINK_COMMAND, {
8850
+ event,
8851
+ node: this
8852
+ });
8853
+ }
8854
+ });
8855
+ element.addEventListener("mouseleave", (event) => {
8856
+ if (event.target instanceof HTMLElement) {
8857
+ event.target.classList.remove("hover");
8858
+ editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
8859
+ }
8860
+ });
8861
+ return element;
8862
+ }
8863
+ updateDOM() {
8864
+ return false;
8865
+ }
8866
+ isInline() {
8867
+ return true;
8868
+ }
8869
+ isKeyboardSelectable() {
8870
+ return true;
8871
+ }
8872
+ exportDOM() {
8873
+ const element = document.createElement("a");
8874
+ element.dataset.linkCard = "true";
8875
+ element.dataset.icon = this.getIcon();
8876
+ element.dataset.description = this.getDescription();
8877
+ if (this.getOpenTarget()) element.target = this.getOpenTarget();
8878
+ element.href = this.getURL();
8879
+ element.textContent = this.getTitle();
8880
+ return { element };
8881
+ }
8882
+ exportJSON() {
8883
+ return {
8884
+ ...super.exportJSON(),
8885
+ description: this.getDescription(),
8886
+ icon: this.getIcon(),
8887
+ openTarget: this.getOpenTarget(),
8888
+ title: this.getTitle(),
8889
+ url: this.getURL()
8890
+ };
8891
+ }
8892
+ updateFromJSON(serializedNode) {
8893
+ return super.updateFromJSON(serializedNode).setURL(serializedNode.url).setTitle(serializedNode.title).setIcon(serializedNode.icon).setDescription(serializedNode.description).setOpenTarget(serializedNode.openTarget ?? "_blank");
8894
+ }
8895
+ getTextContent() {
8896
+ return this.getTitle();
8897
+ }
8898
+ getURL() {
8899
+ return this.getLatest().__url;
8900
+ }
8901
+ setURL(url) {
8902
+ const writable = this.getWritable();
8903
+ writable.__url = url;
8904
+ return writable;
8905
+ }
8906
+ getTitle() {
8907
+ return this.getLatest().__title || this.getURL();
8908
+ }
8909
+ setTitle(title) {
8910
+ const writable = this.getWritable();
8911
+ writable.__title = title || this.getURL();
8912
+ return writable;
8913
+ }
8914
+ getIcon() {
8915
+ return this.getLatest().__icon;
8916
+ }
8917
+ setIcon(icon) {
8918
+ const writable = this.getWritable();
8919
+ writable.__icon = icon || "";
8920
+ return writable;
8921
+ }
8922
+ getDescription() {
8923
+ return this.getLatest().__description;
8924
+ }
8925
+ setDescription(description) {
8926
+ const writable = this.getWritable();
8927
+ writable.__description = description || "";
8928
+ return writable;
8929
+ }
8930
+ getOpenTarget() {
8931
+ return this.getLatest().__openTarget;
8932
+ }
8933
+ setOpenTarget(openTarget) {
8934
+ const writable = this.getWritable();
8935
+ writable.__openTarget = openTarget ?? "_blank";
8936
+ return writable;
8937
+ }
8938
+ decorate(editor) {
8939
+ const decorator = getKernelFromEditor(editor)?.getDecorator(LinkCardNode.getType());
8940
+ if (!decorator) return null;
8941
+ if (typeof decorator === "function") return decorator(this, editor);
8942
+ return decorator.render(this, editor);
8943
+ }
8944
+ };
8945
+ function $createLinkCardNode(payload) {
8946
+ return $applyNodeReplacement(new LinkCardNode(payload.url, payload.title, payload.icon, payload.description, payload.openTarget));
8947
+ }
8948
+ function $convertLinkCardElement(domNode) {
8949
+ const element = domNode;
8950
+ return { node: $createLinkCardNode({
8951
+ description: element.dataset.description,
8952
+ icon: element.dataset.icon,
8953
+ openTarget: element.getAttribute("target"),
8954
+ title: element.textContent || element.href,
8955
+ url: element.getAttribute("href") || ""
8956
+ }) };
8957
+ }
8958
+ function $isLinkCardNode(node) {
8959
+ return node instanceof LinkCardNode || node?.getType?.() === LinkCardNode.getType();
8960
+ }
8961
+ //#endregion
8962
+ //#region src/plugins/link/node/LinkBlockCardNode.ts
8963
+ var LinkBlockCardNode = class LinkBlockCardNode extends LinkCardNode {
8964
+ static getType() {
8965
+ return "link-block-card";
8966
+ }
8967
+ static clone(node) {
8968
+ return new LinkBlockCardNode(node.__url, node.__title, node.__icon, node.__description, node.__openTarget, node.__key);
8969
+ }
8970
+ static importJSON(serializedNode) {
8971
+ return $createLinkBlockCardNode({
8972
+ description: serializedNode.description,
8973
+ icon: serializedNode.icon,
8974
+ openTarget: serializedNode.openTarget,
8975
+ title: serializedNode.title,
8976
+ url: serializedNode.url
8977
+ }).updateFromJSON(serializedNode);
8978
+ }
8979
+ static importDOM() {
8980
+ return { a: (node) => {
8981
+ if (node instanceof HTMLAnchorElement && node.dataset.linkCard === "true" && node.dataset.linkCardLayout === "block") return {
8982
+ conversion: $convertLinkBlockCardElement,
8983
+ priority: 3
8984
+ };
8985
+ return null;
8986
+ } };
8987
+ }
8988
+ constructor(url, title, icon, description, openTarget, key) {
8989
+ super(url, title, icon, description, openTarget, key);
8990
+ }
8991
+ createDOM(config, editor) {
8992
+ const element = super.createDOM(config, editor);
8993
+ element.dataset.linkCardLayout = "block";
8994
+ return element;
8995
+ }
8996
+ exportDOM() {
8997
+ const output = super.exportDOM();
8998
+ if (output.element instanceof HTMLElement) output.element.dataset.linkCardLayout = "block";
8999
+ return output;
9000
+ }
9001
+ updateFromJSON(serializedNode) {
9002
+ return super.updateFromJSON(serializedNode);
9003
+ }
9004
+ isInline() {
9005
+ return false;
9006
+ }
9007
+ };
9008
+ function $createLinkBlockCardNode(payload) {
9009
+ return $applyNodeReplacement(new LinkBlockCardNode(payload.url, payload.title, payload.icon, payload.description, payload.openTarget));
9010
+ }
9011
+ function $convertLinkBlockCardElement(domNode) {
9012
+ const element = domNode;
9013
+ return { node: $createLinkBlockCardNode({
9014
+ description: element.dataset.description,
9015
+ icon: element.dataset.icon,
9016
+ openTarget: element.getAttribute("target"),
9017
+ title: element.textContent || element.href,
9018
+ url: element.getAttribute("href") || ""
9019
+ }) };
9020
+ }
9021
+ function $isLinkBlockCardNode(node) {
9022
+ return node instanceof LinkBlockCardNode || node?.getType?.() === LinkBlockCardNode.getType();
9023
+ }
9024
+ //#endregion
9025
+ //#region src/plugins/link/node/LinkIframeNode.ts
9026
+ var LinkIframeNode = class LinkIframeNode extends DecoratorNode {
9027
+ static getType() {
9028
+ return "link-iframe";
9029
+ }
9030
+ static clone(node) {
9031
+ return new LinkIframeNode(node.__url, node.__src, node.__title, node.__key);
9032
+ }
9033
+ static importJSON(serializedNode) {
9034
+ return $createLinkIframeNode({
9035
+ src: serializedNode.src,
9036
+ title: serializedNode.title,
9037
+ url: serializedNode.url
9038
+ }).updateFromJSON(serializedNode);
9039
+ }
9040
+ static importDOM() {
9041
+ return { a: (node) => {
9042
+ if (node instanceof HTMLAnchorElement && node.dataset.linkIframe === "true") return {
9043
+ conversion: $convertLinkIframeElement,
9044
+ priority: 2
9045
+ };
9046
+ return null;
9047
+ } };
9048
+ }
9049
+ constructor(url, src, title, key) {
9050
+ super(key);
9051
+ this.__url = url;
9052
+ this.__src = src || url;
9053
+ this.__title = title || url;
9054
+ }
9055
+ createDOM(config, editor) {
9056
+ const element = document.createElement("div");
9057
+ element.dataset.linkIframe = "true";
9058
+ addClassNamesToElement(element, config.theme.linkIframe);
9059
+ element.addEventListener("mouseenter", (event) => {
9060
+ if (event.target instanceof HTMLElement) {
9061
+ event.target.classList.add("hover");
9062
+ editor.dispatchCommand(HOVER_LINK_COMMAND, {
9063
+ event,
9064
+ node: this
9065
+ });
9066
+ }
9067
+ });
9068
+ element.addEventListener("mouseleave", (event) => {
9069
+ if (event.target instanceof HTMLElement) {
9070
+ event.target.classList.remove("hover");
9071
+ editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
9072
+ }
9073
+ });
9074
+ return element;
9075
+ }
9076
+ updateDOM() {
9077
+ return false;
9078
+ }
9079
+ isInline() {
9080
+ return false;
9081
+ }
9082
+ exportDOM() {
9083
+ const element = document.createElement("a");
9084
+ element.dataset.linkIframe = "true";
9085
+ element.dataset.src = this.getSrc();
9086
+ element.href = this.getURL();
9087
+ element.textContent = this.getTitle();
9088
+ return { element };
9089
+ }
9090
+ exportJSON() {
9091
+ return {
9092
+ ...super.exportJSON(),
9093
+ src: this.getSrc(),
9094
+ title: this.getTitle(),
9095
+ url: this.getURL()
9096
+ };
9097
+ }
9098
+ updateFromJSON(serializedNode) {
9099
+ return super.updateFromJSON(serializedNode).setURL(serializedNode.url).setSrc(serializedNode.src).setTitle(serializedNode.title);
9100
+ }
9101
+ getTextContent() {
9102
+ return this.getTitle();
9103
+ }
9104
+ getURL() {
9105
+ return this.getLatest().__url;
9106
+ }
9107
+ setURL(url) {
9108
+ const writable = this.getWritable();
9109
+ writable.__url = url;
9110
+ return writable;
9111
+ }
9112
+ getSrc() {
9113
+ return this.getLatest().__src || this.getURL();
9114
+ }
9115
+ setSrc(src) {
9116
+ const writable = this.getWritable();
9117
+ writable.__src = src || this.getURL();
9118
+ return writable;
9119
+ }
9120
+ getTitle() {
9121
+ return this.getLatest().__title || this.getURL();
9122
+ }
9123
+ setTitle(title) {
9124
+ const writable = this.getWritable();
9125
+ writable.__title = title || this.getURL();
9126
+ return writable;
9127
+ }
9128
+ decorate(editor) {
9129
+ const decorator = getKernelFromEditor(editor)?.getDecorator(LinkIframeNode.getType());
9130
+ if (!decorator) return null;
9131
+ if (typeof decorator === "function") return decorator(this, editor);
9132
+ return decorator.render(this, editor);
9133
+ }
9134
+ };
9135
+ function $createLinkIframeNode(payload) {
9136
+ return $applyNodeReplacement(new LinkIframeNode(payload.url, payload.src, payload.title));
9137
+ }
9138
+ function $convertLinkIframeElement(domNode) {
9139
+ const element = domNode;
9140
+ return { node: $createLinkIframeNode({
9141
+ src: element.dataset.src,
9142
+ title: element.textContent || element.href,
9143
+ url: element.getAttribute("href") || ""
9144
+ }) };
9145
+ }
9146
+ function $isLinkIframeNode(node) {
9147
+ return node instanceof LinkIframeNode || node?.getType?.() === LinkIframeNode.getType();
9148
+ }
9149
+ //#endregion
9150
+ //#region src/plugins/link/node/SchemaNode.ts
9151
+ var SchemaNode = class SchemaNode extends DecoratorNode {
9152
+ static getType() {
9153
+ return "schema-link";
9154
+ }
9155
+ static clone(node) {
9156
+ return new SchemaNode(node.__url, node.__schemaType, node.__payload, node.__title, node.__key);
9157
+ }
9158
+ static importJSON(serializedNode) {
9159
+ return $createSchemaNode({
9160
+ payload: serializedNode.payload,
9161
+ schemaType: serializedNode.schemaType,
9162
+ title: serializedNode.title,
9163
+ url: serializedNode.url
9164
+ }).updateFromJSON(serializedNode);
9165
+ }
9166
+ static importDOM() {
9167
+ return { a: (node) => {
9168
+ if (node instanceof HTMLAnchorElement && node.dataset.schemaLink === "true") return {
9169
+ conversion: $convertSchemaElement,
9170
+ priority: 2
9171
+ };
9172
+ return null;
9173
+ } };
9174
+ }
9175
+ constructor(url, schemaType, payload, title, key) {
9176
+ super(key);
9177
+ this.__url = url;
9178
+ this.__schemaType = schemaType || "";
9179
+ this.__payload = payload;
9180
+ this.__title = title || url;
9181
+ }
9182
+ createDOM(config, editor) {
9183
+ const element = document.createElement("div");
9184
+ element.dataset.schemaLink = "true";
9185
+ addClassNamesToElement(element, config.theme.schemaLink);
9186
+ element.addEventListener("mouseenter", (event) => {
9187
+ if (event.target instanceof HTMLElement) {
9188
+ event.target.classList.add("hover");
9189
+ editor.dispatchCommand(HOVER_LINK_COMMAND, {
9190
+ event,
9191
+ node: this
9192
+ });
9193
+ }
9194
+ });
9195
+ element.addEventListener("mouseleave", (event) => {
9196
+ if (event.target instanceof HTMLElement) {
9197
+ event.target.classList.remove("hover");
9198
+ editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
7231
9199
  }
7232
9200
  });
9201
+ return element;
9202
+ }
9203
+ updateDOM() {
7233
9204
  return false;
7234
- }, COMMAND_PRIORITY_EDITOR));
7235
- }
7236
- //#endregion
7237
- //#region src/plugins/link/service/i-link-service.ts
7238
- const ILinkService = genServiceId("LinkService");
7239
- var LinkService = class extends EventEmitter {
7240
- constructor(..._args) {
7241
- super(..._args);
7242
- this._enableLinkToolbar = true;
7243
9205
  }
7244
- get enableLinkToolbar() {
7245
- return this._enableLinkToolbar;
9206
+ exportDOM() {
9207
+ const element = document.createElement("a");
9208
+ element.dataset.schemaLink = "true";
9209
+ element.dataset.schemaType = this.getSchemaType();
9210
+ element.dataset.payload = JSON.stringify(this.getPayload() ?? null);
9211
+ element.href = this.getURL();
9212
+ element.textContent = this.getTitle();
9213
+ return { element };
7246
9214
  }
7247
- setLinkToolbar(enable) {
7248
- this._enableLinkToolbar = enable;
7249
- this.emit("linkToolbarChange", enable);
9215
+ exportJSON() {
9216
+ return {
9217
+ ...super.exportJSON(),
9218
+ payload: this.getPayload(),
9219
+ schemaType: this.getSchemaType(),
9220
+ title: this.getTitle(),
9221
+ url: this.getURL()
9222
+ };
9223
+ }
9224
+ updateFromJSON(serializedNode) {
9225
+ return super.updateFromJSON(serializedNode).setURL(serializedNode.url).setSchemaType(serializedNode.schemaType).setPayload(serializedNode.payload).setTitle(serializedNode.title);
9226
+ }
9227
+ getTextContent() {
9228
+ return this.getTitle();
9229
+ }
9230
+ getURL() {
9231
+ return this.getLatest().__url;
9232
+ }
9233
+ setURL(url) {
9234
+ const writable = this.getWritable();
9235
+ writable.__url = url;
9236
+ return writable;
9237
+ }
9238
+ getSchemaType() {
9239
+ return this.getLatest().__schemaType;
9240
+ }
9241
+ setSchemaType(schemaType) {
9242
+ const writable = this.getWritable();
9243
+ writable.__schemaType = schemaType || "";
9244
+ return writable;
9245
+ }
9246
+ getPayload() {
9247
+ return this.getLatest().__payload;
9248
+ }
9249
+ setPayload(payload) {
9250
+ const writable = this.getWritable();
9251
+ writable.__payload = payload;
9252
+ return writable;
9253
+ }
9254
+ getTitle() {
9255
+ return this.getLatest().__title || this.getURL();
9256
+ }
9257
+ setTitle(title) {
9258
+ const writable = this.getWritable();
9259
+ writable.__title = title || this.getURL();
9260
+ return writable;
9261
+ }
9262
+ decorate(editor) {
9263
+ const decorator = getKernelFromEditor(editor)?.getDecorator(SchemaNode.getType());
9264
+ if (!decorator) return null;
9265
+ if (typeof decorator === "function") return decorator(this, editor);
9266
+ return decorator.render(this, editor);
7250
9267
  }
7251
9268
  };
9269
+ function $createSchemaNode(payload) {
9270
+ return $applyNodeReplacement(new SchemaNode(payload.url, payload.schemaType, payload.payload, payload.title));
9271
+ }
9272
+ function $convertSchemaElement(domNode) {
9273
+ const element = domNode;
9274
+ return { node: $createSchemaNode({
9275
+ payload: parsePayload(element.dataset.payload),
9276
+ schemaType: element.dataset.schemaType,
9277
+ title: element.textContent || element.href,
9278
+ url: element.getAttribute("href") || ""
9279
+ }) };
9280
+ }
9281
+ function parsePayload(value) {
9282
+ if (!value) return void 0;
9283
+ try {
9284
+ return JSON.parse(value);
9285
+ } catch {
9286
+ return value;
9287
+ }
9288
+ }
9289
+ function $isSchemaNode(node) {
9290
+ return node instanceof SchemaNode || node?.getType?.() === SchemaNode.getType();
9291
+ }
9292
+ //#endregion
9293
+ //#region src/plugins/link/normalization/index.ts
9294
+ function normalizeSchemaLinkNode(node, editor, linkService) {
9295
+ const url = node.getURL();
9296
+ const title = node.getTitle() || node.getTextContent() || url;
9297
+ const schema = linkService.parseSchemaUrl(url);
9298
+ const rule = linkService.getSchemaRule(url, {
9299
+ editor,
9300
+ schema,
9301
+ text: node.getTextContent(),
9302
+ title
9303
+ });
9304
+ if (!rule) return false;
9305
+ const parsed = rule.parse?.(url, schema);
9306
+ const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? normalizeSchemaPayload(parsed) : { payload: parsed };
9307
+ if (linkService.hasSchemaLinkRenderer(url)) payload.payload = {
9308
+ ...payload.payload && typeof payload.payload === "object" ? payload.payload : {},
9309
+ __legacyLink: {
9310
+ rel: node.getRel(),
9311
+ target: node.getTarget(),
9312
+ text: node.getTextContent(),
9313
+ title: node.getTitle()
9314
+ }
9315
+ };
9316
+ node.replace($createSchemaNode({
9317
+ payload: payload.payload,
9318
+ schemaType: payload.schemaType || rule.id,
9319
+ title: payload.title || title,
9320
+ url: payload.url || url
9321
+ }));
9322
+ return true;
9323
+ }
9324
+ function normalizeSchemaPayload(payload) {
9325
+ if ("payload" in payload || "schemaType" in payload || "title" in payload || "url" in payload) return payload;
9326
+ return { payload };
9327
+ }
7252
9328
  //#endregion
7253
9329
  //#region src/plugins/link/utils/index.ts
7254
9330
  const SUPPORTED_URL_PROTOCOLS = /* @__PURE__ */ new Set([
@@ -7377,10 +9453,22 @@ const LinkPlugin = class extends KernelPlugin {
7377
9453
  super();
7378
9454
  this.kernel = kernel;
7379
9455
  this.config = config;
7380
- this.linkRegex = /^https?:\/\/\S+$/;
9456
+ this.linkRegex = /^[a-z][\d+.a-z-]*:\/\/\S+$/i;
7381
9457
  this.service = new LinkService();
7382
- kernel.registerNodes([LinkNode, AutoLinkNode]);
9458
+ kernel.registerNodes([
9459
+ LinkNode,
9460
+ AutoLinkNode,
9461
+ LinkCardNode,
9462
+ LinkBlockCardNode,
9463
+ LinkIframeNode,
9464
+ SchemaNode
9465
+ ]);
9466
+ this.service.updateConfig(config);
7383
9467
  kernel.registerService(ILinkService, this.service);
9468
+ this.registerDecorator(kernel, LinkCardNode.getType(), (node, editor) => config?.decoratorCard ? config.decoratorCard(node, editor) : null);
9469
+ this.registerDecorator(kernel, LinkBlockCardNode.getType(), (node, editor) => config?.decoratorCard ? config.decoratorCard(node, editor) : null);
9470
+ this.registerDecorator(kernel, LinkIframeNode.getType(), (node, editor) => config?.decoratorIframe ? config.decoratorIframe(node, editor) : null);
9471
+ this.registerDecorator(kernel, SchemaNode.getType(), (node, editor) => config?.decoratorSchema ? config.decoratorSchema(node, editor) : null);
7384
9472
  if (config?.theme) kernel.registerThemes(config.theme);
7385
9473
  if (config?.linkRegex) this.linkRegex = config.linkRegex;
7386
9474
  }
@@ -7404,6 +9492,9 @@ const LinkPlugin = class extends KernelPlugin {
7404
9492
  }
7405
9493
  return false;
7406
9494
  }, COMMAND_PRIORITY_NORMAL));
9495
+ if (this.config?.normalizeSchemaLinks !== false) this.register(editor.registerNodeTransform(LinkNode, (node) => {
9496
+ normalizeSchemaLinkNode(node, editor, this.service);
9497
+ }));
7407
9498
  this.registerMarkdown();
7408
9499
  this.registerLiteXml();
7409
9500
  }
@@ -7428,6 +9519,74 @@ const LinkPlugin = class extends KernelPlugin {
7428
9519
  version: 1
7429
9520
  })];
7430
9521
  });
9522
+ litexmlService.registerXMLWriter(LinkCardNode.getType(), (node, ctx) => {
9523
+ if (!$isLinkCardNode(node)) return false;
9524
+ return ctx.createXmlNode("link-card", {
9525
+ description: node.getDescription(),
9526
+ href: node.getURL(),
9527
+ icon: node.getIcon(),
9528
+ openTarget: node.getOpenTarget() || "_blank",
9529
+ title: node.getTitle()
9530
+ });
9531
+ });
9532
+ litexmlService.registerXMLWriter(LinkBlockCardNode.getType(), (node, ctx) => {
9533
+ if (!$isLinkBlockCardNode(node)) return false;
9534
+ return ctx.createXmlNode("link-card", {
9535
+ description: node.getDescription(),
9536
+ href: node.getURL(),
9537
+ icon: node.getIcon(),
9538
+ layout: "block",
9539
+ openTarget: node.getOpenTarget() || "_blank",
9540
+ title: node.getTitle()
9541
+ });
9542
+ });
9543
+ litexmlService.registerXMLReader("link-card", (xmlNode) => {
9544
+ return [INodeHelper.createTypeNode(xmlNode.getAttribute("layout") === "block" ? LinkBlockCardNode.getType() : LinkCardNode.getType(), {
9545
+ description: xmlNode.getAttribute("description") || "",
9546
+ icon: xmlNode.getAttribute("icon") || "",
9547
+ openTarget: xmlNode.getAttribute("openTarget") || "_blank",
9548
+ title: xmlNode.getAttribute("title") || xmlNode.getAttribute("href") || "",
9549
+ type: xmlNode.getAttribute("layout") === "block" ? LinkBlockCardNode.getType() : LinkCardNode.getType(),
9550
+ url: xmlNode.getAttribute("href") || "",
9551
+ version: 1
9552
+ })];
9553
+ });
9554
+ litexmlService.registerXMLWriter(LinkIframeNode.getType(), (node, ctx) => {
9555
+ if (!$isLinkIframeNode(node)) return false;
9556
+ return ctx.createXmlNode("link-iframe", {
9557
+ href: node.getURL(),
9558
+ src: node.getSrc(),
9559
+ title: node.getTitle()
9560
+ });
9561
+ });
9562
+ litexmlService.registerXMLReader("link-iframe", (xmlNode) => {
9563
+ return [INodeHelper.createTypeNode(LinkIframeNode.getType(), {
9564
+ src: xmlNode.getAttribute("src") || xmlNode.getAttribute("href") || "",
9565
+ title: xmlNode.getAttribute("title") || xmlNode.getAttribute("href") || "",
9566
+ type: LinkIframeNode.getType(),
9567
+ url: xmlNode.getAttribute("href") || "",
9568
+ version: 1
9569
+ })];
9570
+ });
9571
+ litexmlService.registerXMLWriter(SchemaNode.getType(), (node, ctx) => {
9572
+ if (!$isSchemaNode(node)) return false;
9573
+ return ctx.createXmlNode("schema-link", {
9574
+ href: node.getURL(),
9575
+ payload: JSON.stringify(node.getPayload() ?? null),
9576
+ schemaType: node.getSchemaType(),
9577
+ title: node.getTitle()
9578
+ });
9579
+ });
9580
+ litexmlService.registerXMLReader("schema-link", (xmlNode) => {
9581
+ return [INodeHelper.createTypeNode(SchemaNode.getType(), {
9582
+ payload: parseJsonAttribute(xmlNode.getAttribute("payload")),
9583
+ schemaType: xmlNode.getAttribute("schemaType") || "",
9584
+ title: xmlNode.getAttribute("title") || xmlNode.getAttribute("href") || "",
9585
+ type: SchemaNode.getType(),
9586
+ url: xmlNode.getAttribute("href") || "",
9587
+ version: 1
9588
+ })];
9589
+ });
7431
9590
  }
7432
9591
  registerMarkdown() {
7433
9592
  this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownShortCut({
@@ -7447,6 +9606,30 @@ const LinkPlugin = class extends KernelPlugin {
7447
9606
  this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(LinkNode.getType(), (ctx, node) => {
7448
9607
  if ($isLinkNode(node)) ctx.wrap("[", `](${node.getURL()})`);
7449
9608
  });
9609
+ this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(LinkCardNode.getType(), (ctx, node) => {
9610
+ if ($isLinkCardNode(node)) {
9611
+ ctx.appendLine(`[${node.getTitle()}](${node.getURL()})\n`);
9612
+ return true;
9613
+ }
9614
+ });
9615
+ this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(LinkBlockCardNode.getType(), (ctx, node) => {
9616
+ if ($isLinkBlockCardNode(node)) {
9617
+ ctx.appendLine(`[${node.getTitle()}](${node.getURL()})\n`);
9618
+ return true;
9619
+ }
9620
+ });
9621
+ this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(LinkIframeNode.getType(), (ctx, node) => {
9622
+ if ($isLinkIframeNode(node)) {
9623
+ ctx.appendLine(`[${node.getTitle()}](${node.getURL()})\n`);
9624
+ return true;
9625
+ }
9626
+ });
9627
+ this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(SchemaNode.getType(), (ctx, node) => {
9628
+ if ($isSchemaNode(node)) {
9629
+ ctx.appendLine(`[${node.getTitle()}](${node.getURL()})\n`);
9630
+ return true;
9631
+ }
9632
+ });
7450
9633
  this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownReader("link", (node, children) => {
7451
9634
  return [INodeHelper.createElementNode("link", {
7452
9635
  children,
@@ -7461,6 +9644,14 @@ const LinkPlugin = class extends KernelPlugin {
7461
9644
  });
7462
9645
  }
7463
9646
  };
9647
+ function parseJsonAttribute(value) {
9648
+ if (!value) return void 0;
9649
+ try {
9650
+ return JSON.parse(value);
9651
+ } catch {
9652
+ return value;
9653
+ }
9654
+ }
7464
9655
  //#endregion
7465
9656
  //#region src/utils/cx.ts
7466
9657
  const cx = (...classNames) => classNames.filter(Boolean).join(" ");
@@ -7819,7 +10010,11 @@ const ListPlugin = class extends KernelPlugin {
7819
10010
  type: "element"
7820
10011
  });
7821
10012
  markdownService.registerMarkdownWriter(ListNode.getType(), (ctx, node) => {
7822
- if ($isListNode(node)) ctx.wrap("", "\n");
10013
+ if ($isListNode(node)) {
10014
+ const parent = node.getParent();
10015
+ const needsLeadingNewline = $isListItemNode(parent) && node.getPreviousSibling() !== null;
10016
+ ctx.wrap(needsLeadingNewline ? "\n" : "", "\n");
10017
+ }
7823
10018
  });
7824
10019
  const getLevel = (node) => {
7825
10020
  if (!node) return 0;
@@ -7850,7 +10045,10 @@ const ListPlugin = class extends KernelPlugin {
7850
10045
  let start = node.start || 1;
7851
10046
  return INodeHelper.createElementNode("list", {
7852
10047
  children: children.map((v) => {
7853
- if (v.type === "listitem") v.value = start++;
10048
+ if (v.type === "listitem") {
10049
+ v.value = start;
10050
+ if (v.children?.[0]?.type !== "list") start++;
10051
+ }
7854
10052
  return v;
7855
10053
  }),
7856
10054
  direction: "ltr",
@@ -8922,10 +11120,88 @@ var TableControllerMenuService = class {
8922
11120
  }
8923
11121
  };
8924
11122
  //#endregion
11123
+ //#region src/plugins/table/plugin/single-cell-paste.ts
11124
+ const LEXICAL_CLIPBOARD_TYPE = "application/x-lexical-editor";
11125
+ const countNodesByType = (node, type) => {
11126
+ return (node.type === type ? 1 : 0) + (Array.isArray(node.children) ? node.children.reduce((count, child) => count + countNodesByType(child, type), 0) : 0);
11127
+ };
11128
+ const parseLexicalTableClipboard = (serializedClipboard) => {
11129
+ if (!serializedClipboard) return void 0;
11130
+ try {
11131
+ const clipboard = JSON.parse(serializedClipboard);
11132
+ if (!Array.isArray(clipboard.nodes) || clipboard.nodes.length !== 1) return { isSingleCell: false };
11133
+ const [rootNode] = clipboard.nodes;
11134
+ if (rootNode?.type !== "table") return { isSingleCell: false };
11135
+ const cells = [];
11136
+ const collectCells = (node) => {
11137
+ if (node.type === "tablecell") cells.push(node);
11138
+ node.children?.forEach(collectCells);
11139
+ };
11140
+ collectCells(rootNode);
11141
+ const isSingleCell = countNodesByType(rootNode, "table") === 1 && cells.length === 1;
11142
+ return {
11143
+ cell: isSingleCell ? cells[0] : void 0,
11144
+ isSingleCell
11145
+ };
11146
+ } catch {
11147
+ return;
11148
+ }
11149
+ };
11150
+ const getSerializedNodeText = (node) => {
11151
+ if (node.type === "text") return node.text || "";
11152
+ if (node.type === "linebreak") return "\n";
11153
+ if (node.type === "tab") return " ";
11154
+ return node.children?.map(getSerializedNodeText).join("") || "";
11155
+ };
11156
+ const getSerializedCellText = (cell) => {
11157
+ return cell.children?.map(getSerializedNodeText).join("\n") || "";
11158
+ };
11159
+ const getSingleCellHTMLText = (html) => {
11160
+ if (!isSingleCellHTMLTable(html) || typeof DOMParser === "undefined") return void 0;
11161
+ const cell = new DOMParser().parseFromString(html, "text/html").body.querySelector("td, th");
11162
+ if (!cell) return void 0;
11163
+ cell.querySelectorAll("br").forEach((node) => node.replaceWith("\n"));
11164
+ return Array.from(cell.children).map((node) => node.textContent || "").join("\n") || cell.textContent || "";
11165
+ };
11166
+ const isSingleCellHTMLTable = (html) => {
11167
+ if (!html || typeof DOMParser === "undefined") return false;
11168
+ const document = new DOMParser().parseFromString(html, "text/html");
11169
+ const tables = document.body.querySelectorAll("table");
11170
+ if (tables.length !== 1 || tables[0].querySelectorAll("td, th").length !== 1) return false;
11171
+ const remainder = document.body.cloneNode(true);
11172
+ remainder.querySelector("table")?.remove();
11173
+ remainder.querySelectorAll("link, meta, script, style").forEach((node) => node.remove());
11174
+ return !remainder.querySelector("audio, canvas, iframe, img, object, svg, video") && remainder.textContent?.trim() === "";
11175
+ };
11176
+ const isSingleCellTableClipboard = (clipboardData) => {
11177
+ const lexicalResult = parseLexicalTableClipboard(clipboardData.getData(LEXICAL_CLIPBOARD_TYPE));
11178
+ if (lexicalResult !== void 0) return lexicalResult.isSingleCell;
11179
+ return isSingleCellHTMLTable(clipboardData.getData("text/html"));
11180
+ };
11181
+ const getSingleCellPlainText = (clipboardData) => {
11182
+ const lexicalResult = parseLexicalTableClipboard(clipboardData.getData(LEXICAL_CLIPBOARD_TYPE));
11183
+ if (lexicalResult?.cell) return getSerializedCellText(lexicalResult.cell);
11184
+ const htmlText = getSingleCellHTMLText(clipboardData.getData("text/html"));
11185
+ if (htmlText !== void 0) return htmlText;
11186
+ return clipboardData.getData("text/plain").replace(/(?:\r\n|\n)$/, "");
11187
+ };
11188
+ const handleSingleCellTablePaste = (editor, event) => {
11189
+ if (!("clipboardData" in event)) return false;
11190
+ const clipboardData = event.clipboardData;
11191
+ if (!clipboardData || !isSingleCellTableClipboard(clipboardData)) return false;
11192
+ event.preventDefault();
11193
+ editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, getSingleCellPlainText(clipboardData));
11194
+ return true;
11195
+ };
11196
+ const registerSingleCellTablePaste = (editor) => {
11197
+ return editor.registerCommand(PASTE_COMMAND, (event) => handleSingleCellTablePaste(editor, event), COMMAND_PRIORITY_HIGH);
11198
+ };
11199
+ //#endregion
8925
11200
  //#region src/plugins/table/plugin/index.ts
8926
11201
  const tableCellProcessor = (before, content, after) => {
8927
11202
  return before + content.replace(/\n+$/, "").replaceAll(/\n+/g, "<br />") + after;
8928
11203
  };
11204
+ const isRemovedTableRowDiff = (node) => node.getType() === "table-row-diff" && "getDiffType" in node && typeof node.getDiffType === "function" && node.getDiffType() === "remove";
8929
11205
  function isHeadlessEditor(editor) {
8930
11206
  return editor._headless === true;
8931
11207
  }
@@ -8974,6 +11250,7 @@ const TablePlugin = class extends KernelPlugin {
8974
11250
  }
8975
11251
  onInit(editor) {
8976
11252
  this.register(registerTableCellUnmergeTransform(editor));
11253
+ this.register(registerSingleCellTablePaste(editor));
8977
11254
  if (!isHeadlessEditor(editor)) {
8978
11255
  setScrollableTablesActive(editor, true);
8979
11256
  this.register(registerTablePlugin(editor));
@@ -9220,7 +11497,7 @@ const TablePlugin = class extends KernelPlugin {
9220
11497
  markdownService.registerMarkdownWriter(TableRowNode.getType(), (ctx, node) => {
9221
11498
  const parent = node.getParent();
9222
11499
  if (!$isTableNode(parent)) return;
9223
- if (!node.getPreviousSibling()) ctx.wrap("", `\n${parent.getColWidths()?.map(() => {
11500
+ if (!node.getPreviousSiblings().some((sibling) => !isRemovedTableRowDiff(sibling))) ctx.wrap("", `\n${parent.getColWidths()?.map(() => {
9224
11501
  return "|:--";
9225
11502
  }).join("")}|\n`);
9226
11503
  else ctx.wrap("", "\n");
@@ -9267,6 +11544,207 @@ const TablePlugin = class extends KernelPlugin {
9267
11544
  }
9268
11545
  };
9269
11546
  //#endregion
11547
+ //#region src/plugins/collapsible/node/CollapsibleNode.ts
11548
+ const CONTENT_SELECTOR = "[data-collapsible-content=\"true\"]";
11549
+ const TOGGLE_SELECTOR = "[data-collapsible-toggle=\"true\"]";
11550
+ var CollapsibleNode = class CollapsibleNode extends ElementNode {
11551
+ static getType() {
11552
+ return "collapsible";
11553
+ }
11554
+ static clone(node) {
11555
+ return new CollapsibleNode(node.__title, node.__collapsed, node.__key);
11556
+ }
11557
+ static importDOM() {
11558
+ return { details: () => ({
11559
+ conversion: $convertDetailsElement,
11560
+ priority: 1
11561
+ }) };
11562
+ }
11563
+ static importJSON(serializedNode) {
11564
+ const node = $createCollapsibleNode(serializedNode.title || "Details").updateFromJSON(serializedNode);
11565
+ if (shouldPrependTitle(serializedNode)) {
11566
+ const titleParagraph = $createTitleParagraph(serializedNode.title || "Details");
11567
+ const firstChild = node.getFirstChild();
11568
+ if (firstChild) firstChild.insertBefore(titleParagraph);
11569
+ else node.append(titleParagraph);
11570
+ }
11571
+ return node;
11572
+ }
11573
+ constructor(title = "Details", collapsed = false, key) {
11574
+ super(key);
11575
+ this.__title = title;
11576
+ this.__collapsed = collapsed;
11577
+ }
11578
+ createDOM(config, editor) {
11579
+ const element = document.createElement("section");
11580
+ addClassNamesToElement(element, config.theme.collapsible);
11581
+ element.dataset.collapsible = "true";
11582
+ element.dataset.collapsibleCollapsed = String(this.__collapsed);
11583
+ const toggle = document.createElement("button");
11584
+ toggle.type = "button";
11585
+ toggle.contentEditable = "false";
11586
+ toggle.dataset.collapsibleToggle = "true";
11587
+ toggle.setAttribute("aria-expanded", String(!this.__collapsed));
11588
+ toggle.setAttribute("aria-label", this.__collapsed ? "Expand collapsible block" : "Collapse collapsible block");
11589
+ toggle.addEventListener("click", () => {
11590
+ editor.update(() => {
11591
+ const latest = this.getLatest();
11592
+ if ($isCollapsibleNode(latest)) latest.setCollapsed(!latest.isCollapsed());
11593
+ });
11594
+ });
11595
+ const content = document.createElement("div");
11596
+ content.dataset.collapsibleContent = "true";
11597
+ element.append(toggle, content);
11598
+ return element;
11599
+ }
11600
+ updateDOM(prevNode, dom) {
11601
+ const toggle = dom.querySelector(TOGGLE_SELECTOR);
11602
+ dom.dataset.collapsibleCollapsed = String(this.__collapsed);
11603
+ if (toggle && prevNode.__collapsed !== this.__collapsed) {
11604
+ toggle.setAttribute("aria-expanded", String(!this.__collapsed));
11605
+ toggle.setAttribute("aria-label", this.__collapsed ? "Expand collapsible block" : "Collapse collapsible block");
11606
+ }
11607
+ return false;
11608
+ }
11609
+ exportDOM() {
11610
+ const element = document.createElement("details");
11611
+ if (!this.__collapsed) element.setAttribute("open", "");
11612
+ const summary = document.createElement("summary");
11613
+ summary.textContent = this.getTitle();
11614
+ element.append(summary);
11615
+ return { element };
11616
+ }
11617
+ exportJSON() {
11618
+ return {
11619
+ ...super.exportJSON(),
11620
+ collapsed: this.isCollapsed(),
11621
+ title: this.getTitle()
11622
+ };
11623
+ }
11624
+ getDOMSlot(element) {
11625
+ const content = element.querySelector(CONTENT_SELECTOR);
11626
+ if (content instanceof HTMLElement) return super.getDOMSlot(element).withElement(content);
11627
+ return super.getDOMSlot(element);
11628
+ }
11629
+ getTitle() {
11630
+ const latest = this.getLatest();
11631
+ const firstChild = latest.getFirstChild();
11632
+ return firstChild ? firstChild.getTextContent().trim() : latest.__title;
11633
+ }
11634
+ isCollapsed() {
11635
+ return this.getLatest().__collapsed;
11636
+ }
11637
+ isShadowRoot() {
11638
+ return true;
11639
+ }
11640
+ setCollapsed(collapsed) {
11641
+ const writable = this.getWritable();
11642
+ writable.__collapsed = collapsed;
11643
+ return writable;
11644
+ }
11645
+ setTitle(title) {
11646
+ const writable = this.getWritable();
11647
+ writable.__title = title;
11648
+ return writable;
11649
+ }
11650
+ updateFromJSON(serializedNode) {
11651
+ return super.updateFromJSON(serializedNode).setTitle(serializedNode.title || "Details").setCollapsed(Boolean(serializedNode.collapsed));
11652
+ }
11653
+ };
11654
+ function $createCollapsibleNode(title = "Details", collapsed = false) {
11655
+ return $applyNodeReplacement(new CollapsibleNode(title, collapsed));
11656
+ }
11657
+ function $isCollapsibleNode(node) {
11658
+ return node instanceof CollapsibleNode;
11659
+ }
11660
+ function $convertDetailsElement(domNode) {
11661
+ if (!(domNode instanceof HTMLDetailsElement)) return { node: null };
11662
+ return { node: $createCollapsibleNode(domNode.querySelector("summary")?.textContent?.trim() || "Details", !domNode.open) };
11663
+ }
11664
+ function $createTitleParagraph(title) {
11665
+ const paragraph = $createParagraphNode();
11666
+ paragraph.append($createTextNode(title));
11667
+ return paragraph;
11668
+ }
11669
+ function getSerializedText(node) {
11670
+ if (!node || typeof node !== "object") return "";
11671
+ if ("text" in node && typeof node.text === "string") return node.text;
11672
+ if ("children" in node && Array.isArray(node.children)) return node.children.map((child) => getSerializedText(child)).join("");
11673
+ return "";
11674
+ }
11675
+ function shouldPrependTitle(serializedNode) {
11676
+ const title = serializedNode.title?.trim();
11677
+ if (!title) return false;
11678
+ const firstChild = (Array.isArray(serializedNode.children) ? serializedNode.children : [])[0];
11679
+ return getSerializedText(firstChild).trim() !== title;
11680
+ }
11681
+ //#endregion
11682
+ //#region src/headless/collapsible-plugin.ts
11683
+ const HeadlessCollapsiblePlugin = class extends KernelPlugin {
11684
+ static {
11685
+ this.pluginName = "HeadlessCollapsiblePlugin";
11686
+ }
11687
+ constructor(kernel) {
11688
+ super();
11689
+ this.kernel = kernel;
11690
+ kernel.registerNodes([CollapsibleNode]);
11691
+ }
11692
+ onInit(_editor) {
11693
+ const markdownService = this.kernel.requireService(IMarkdownShortCutService);
11694
+ markdownService?.registerMarkdownWriter(CollapsibleNode.getType(), (ctx, node) => {
11695
+ if (!$isCollapsibleNode(node)) return false;
11696
+ ctx.appendLine(`<details${node.isCollapsed() ? "" : " open"}>\n`);
11697
+ ctx.appendLine(`<summary>${escapeHtml(node.getTitle())}</summary>\n\n`);
11698
+ node.getChildren().slice(1).forEach((child) => ctx.processChild(ctx, child));
11699
+ ctx.appendLine("\n</details>\n");
11700
+ return true;
11701
+ });
11702
+ markdownService?.registerMarkdownReader("html", (node, children) => {
11703
+ const tag = getHtmlTagName(node.value);
11704
+ if (tag === "summary") return INodeHelper.createTypeNode("__collapsible_summary", { text: getNodeText(children) });
11705
+ if (tag !== "details") return false;
11706
+ const summaryNode = children.find((child) => child.type === "__collapsible_summary");
11707
+ const contentChildren = children.filter((child) => child.type !== "__collapsible_summary");
11708
+ const title = summaryNode && "text" in summaryNode && typeof summaryNode.text === "string" ? summaryNode.text : extractSummaryTitle(node.value) || "Details";
11709
+ return INodeHelper.createElementNode(CollapsibleNode.getType(), {
11710
+ children: ensureTitleChildren(contentChildren, title),
11711
+ collapsed: !/\sopen(?:\s|>|$)/i.test(node.value),
11712
+ title
11713
+ });
11714
+ });
11715
+ const litexmlService = this.kernel.requireService(ILitexmlService);
11716
+ litexmlService?.registerXMLWriter(CollapsibleNode.getType(), (node, ctx) => {
11717
+ if (!$isCollapsibleNode(node)) return false;
11718
+ return ctx.createXmlNode("collapsible", {
11719
+ collapsed: String(node.isCollapsed()),
11720
+ title: node.getTitle()
11721
+ });
11722
+ });
11723
+ litexmlService?.registerXMLReader("collapsible", (xmlNode, children) => {
11724
+ const title = xmlNode.getAttribute("title") || "Details";
11725
+ return INodeHelper.createElementNode(CollapsibleNode.getType(), {
11726
+ children: ensureTitleChildren(children, title),
11727
+ collapsed: xmlNode.getAttribute("collapsed") === "true",
11728
+ title
11729
+ });
11730
+ });
11731
+ }
11732
+ };
11733
+ const getHtmlTagName = (value) => value.match(/^<\/?\s*([\da-z-]+)/i)?.[1]?.toLowerCase() || "";
11734
+ const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
11735
+ const extractSummaryTitle = (value) => value.match(/<summary\b[^>]*>([\S\s]*?)<\/summary>/i)?.[1]?.trim() || "";
11736
+ const getNodeText = (children) => children.map((child) => {
11737
+ if (!child || typeof child !== "object") return "";
11738
+ if ("text" in child && typeof child.text === "string") return child.text;
11739
+ if ("children" in child && Array.isArray(child.children)) return getNodeText(child.children);
11740
+ return "";
11741
+ }).join("");
11742
+ const ensureTitleChildren = (children, title) => {
11743
+ const trimmedTitle = title.trim();
11744
+ if (!trimmedTitle || getNodeText(children.slice(0, 1)).trim() === trimmedTitle) return children;
11745
+ return [INodeHelper.createParagraph({ children: [INodeHelper.createTextNode(title)] }), ...children];
11746
+ };
11747
+ //#endregion
9270
11748
  //#region src/headless/extract-media-from-editor-state.ts
9271
11749
  const IMAGE_TYPES = /* @__PURE__ */ new Set(["image", "block-image"]);
9272
11750
  const FILE_TYPE = "file";
@@ -9416,6 +11894,7 @@ const DEFAULT_HEADLESS_EDITOR_PLUGINS = [
9416
11894
  HRPlugin,
9417
11895
  ListPlugin,
9418
11896
  TablePlugin,
11897
+ HeadlessCollapsiblePlugin,
9419
11898
  LitexmlPlugin
9420
11899
  ];
9421
11900
  var HeadlessEditor = class {