@trafica/editor 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -806,25 +806,33 @@ declare function execCommand(engine: EditorEngineInterface, command: string, ...
806
806
  declare function registerCommand(name: string, fn: CommandFn): void;
807
807
 
808
808
  /**
809
- * PastePlugin
809
+ * PastePlugin / ClipboardPlugin
810
810
  *
811
- * Intercepts paste events on the editor container, sanitizes the pasted HTML,
812
- * converts it to our document model, and inserts it at the current cursor.
811
+ * CKEditor-style clipboard pipeline:
813
812
  *
814
- * Paste sanitization is CRITICAL for security and correctness:
815
- * - Strip scripts, iframes, event handlers
816
- * - Normalize formatting (convert <b> → mark, etc.)
817
- * - Reject unknown elements (fall back to plain text)
813
+ * Copy/Cut intercept event, serialize selection from model to clean HTML,
814
+ * write text/html + text/plain to clipboard. Cut also deletes selection.
818
815
  *
819
- * This plugin attaches a 'paste' DOM event listener in its init() and
820
- * removes it on cleanup. It produces a replace_doc transaction so the
821
- * history manager captures the state before the paste as an undo point.
816
+ * Paste normalize HTML from external sources (Word, Google Docs, web),
817
+ * deserialize to model, merge at cursor with block-split/merge logic.
818
+ *
819
+ * Pipeline stages:
820
+ * 1. normalizeHTML — strip MSO/GDocs junk, preserve useful inline styles
821
+ * 2. deserialize — HTML → model (BlockNode[])
822
+ * 3. mergeAtCursor — split current block, insert pasted blocks, merge edges
823
+ * 4. set cursor — place after last pasted character
824
+ *
825
+ * Never uses document.execCommand. Uses Clipboard API, Selection API, DOM APIs only.
822
826
  */
823
827
 
824
828
  declare function createPastePlugin(): EditorPlugin;
825
829
  /**
826
- * Attach paste handling to the editor container.
827
- * Called by the Editor component after the container ref is available.
830
+ * Attach copy, cut, and paste handlers to the editor container.
831
+ * Returns a cleanup function that removes all listeners.
832
+ */
833
+ declare function attachClipboardHandlers(container: HTMLElement, engine: EditorEngineInterface): () => void;
834
+ /**
835
+ * Attach only the paste handler (kept for backward compat).
828
836
  */
829
837
  declare function attachPasteHandler(container: HTMLElement, engine: EditorEngineInterface): () => void;
830
838
 
@@ -971,4 +979,4 @@ declare const DEFAULT_TOOLBAR: EditorToolbarItem[];
971
979
  declare const MINIMAL_TOOLBAR: EditorToolbarItem[];
972
980
  declare const BASIC_TOOLBAR: EditorToolbarItem[];
973
981
 
974
- export { type AddMarkStep, type AlignmentType, BASIC_TOOLBAR, type BlockNode, type BlockNodeType, type CellPosition, type Command, DEFAULT_TOOLBAR, type DeleteNodeStep, type DeleteRangeStep, type DeleteTextStep, type Document, Editor, type EditorAPI, EditorCore, type EditorCoreProps, EditorEngine, type EditorEngineInterface, type EditorNode, type EditorPlugin, type EditorProps, type EditorSelection, type EditorState, type EditorToolbarConfig, type EditorToolbarItem, type InlineNodeType, type InsertNodeStep, type InsertTextStep, type JoinBlocksStep, MINIMAL_TOOLBAR, type Mark, type MarkType, type NodeAttrs, type NodePosition, type NodeType, type RemoveMarkStep, type ReplaceDocStep, type SearchMatch, type SearchOptions, type Serializer, type SetMarkStep, type SetNodeAttrsStep, type SetNodeTypeStep, type SetSelectionStep, type SplitBlockStep, type StateListener, type Step, type StepType, type TableDimensions, TablePlugin, type TextNode, type ThemeConfig, type ThemeMode, type ThemeTokens, Toolbar, type ToolbarItem, type Transaction, addColumnLeft, addColumnRight, addMarkToNode, addRowAbove, addRowBelow, applyMarkToRange, applySearchHighlights, applyTransaction, attachPasteHandler, captureSelection, clearSearchHighlights, collectText, comparePaths, comparePositions, createEmptyDocument, createHistoryManager, createHistoryPlugin, createParagraph, createPastePlugin, createTableCell, createTableNode, createTransaction, deleteColumn, deleteImageAtPath, deleteRange, deleteRow, deleteTable, deleteTableColumn, deleteTableRow, deleteTextAtPath, execCommand, findCellPosition, findContentBlockPath, findMatches, findTablePath, getActiveAlignment, getActiveBlockType, getActiveFontFamily, getActiveFontSize, getActiveHighlightColor, getActiveLinkHref, getActiveLinkRange, getActiveMarks, getActiveTextColor, getCellFirstPosition, getCellLastPosition, getDocumentLength, getDocumentMarkAttrValues, getNodeAtPath, getTableDimensions, getTextNodesBetween, htmlSerializer, insertImage, insertLink, insertTable, insertTableColumnAfter, insertTableColumnBefore, insertTableRowAfter, insertTableRowBefore, insertText, insertTextAtPath, isBlockNode, isContainerBlock, isSelectionInContainer, isTextNode, joinBlocks, jsonSerializer, makeCollapsedSelection, makePosition, markdownSerializer, marksEqual, mergeAdjacentTextNodesInDoc, mergeCells, mergeTableCells, normalizeRange, redo, registerCommand, removeMarkFromNode, removeMarkFromRange, renderDocument, replaceAllMatches, replaceMatch, restoreSelection, scrollMatchIntoView, setAlignment, setBlockType, setCodeBlockLanguage, setColumnWidth, setFontFamily, setFontSize, setHighlightColor, setImageAttr, setMarkOnRange, setTextColor, splitBlock, splitCell, splitTableCell, toggleCheckItemAt, toggleHeaderRow, toggleMark, undo, updateColumnWidth, useEditorEngine, useEditorState, walkDocument };
982
+ export { type AddMarkStep, type AlignmentType, BASIC_TOOLBAR, type BlockNode, type BlockNodeType, type CellPosition, type Command, DEFAULT_TOOLBAR, type DeleteNodeStep, type DeleteRangeStep, type DeleteTextStep, type Document, Editor, type EditorAPI, EditorCore, type EditorCoreProps, EditorEngine, type EditorEngineInterface, type EditorNode, type EditorPlugin, type EditorProps, type EditorSelection, type EditorState, type EditorToolbarConfig, type EditorToolbarItem, type InlineNodeType, type InsertNodeStep, type InsertTextStep, type JoinBlocksStep, MINIMAL_TOOLBAR, type Mark, type MarkType, type NodeAttrs, type NodePosition, type NodeType, type RemoveMarkStep, type ReplaceDocStep, type SearchMatch, type SearchOptions, type Serializer, type SetMarkStep, type SetNodeAttrsStep, type SetNodeTypeStep, type SetSelectionStep, type SplitBlockStep, type StateListener, type Step, type StepType, type TableDimensions, TablePlugin, type TextNode, type ThemeConfig, type ThemeMode, type ThemeTokens, Toolbar, type ToolbarItem, type Transaction, addColumnLeft, addColumnRight, addMarkToNode, addRowAbove, addRowBelow, applyMarkToRange, applySearchHighlights, applyTransaction, attachClipboardHandlers, attachPasteHandler, captureSelection, clearSearchHighlights, collectText, comparePaths, comparePositions, createEmptyDocument, createHistoryManager, createHistoryPlugin, createParagraph, createPastePlugin, createTableCell, createTableNode, createTransaction, deleteColumn, deleteImageAtPath, deleteRange, deleteRow, deleteTable, deleteTableColumn, deleteTableRow, deleteTextAtPath, execCommand, findCellPosition, findContentBlockPath, findMatches, findTablePath, getActiveAlignment, getActiveBlockType, getActiveFontFamily, getActiveFontSize, getActiveHighlightColor, getActiveLinkHref, getActiveLinkRange, getActiveMarks, getActiveTextColor, getCellFirstPosition, getCellLastPosition, getDocumentLength, getDocumentMarkAttrValues, getNodeAtPath, getTableDimensions, getTextNodesBetween, htmlSerializer, insertImage, insertLink, insertTable, insertTableColumnAfter, insertTableColumnBefore, insertTableRowAfter, insertTableRowBefore, insertText, insertTextAtPath, isBlockNode, isContainerBlock, isSelectionInContainer, isTextNode, joinBlocks, jsonSerializer, makeCollapsedSelection, makePosition, markdownSerializer, marksEqual, mergeAdjacentTextNodesInDoc, mergeCells, mergeTableCells, normalizeRange, redo, registerCommand, removeMarkFromNode, removeMarkFromRange, renderDocument, replaceAllMatches, replaceMatch, restoreSelection, scrollMatchIntoView, setAlignment, setBlockType, setCodeBlockLanguage, setColumnWidth, setFontFamily, setFontSize, setHighlightColor, setImageAttr, setMarkOnRange, setTextColor, splitBlock, splitCell, splitTableCell, toggleCheckItemAt, toggleHeaderRow, toggleMark, undo, updateColumnWidth, useEditorEngine, useEditorState, walkDocument };
package/dist/index.d.ts CHANGED
@@ -806,25 +806,33 @@ declare function execCommand(engine: EditorEngineInterface, command: string, ...
806
806
  declare function registerCommand(name: string, fn: CommandFn): void;
807
807
 
808
808
  /**
809
- * PastePlugin
809
+ * PastePlugin / ClipboardPlugin
810
810
  *
811
- * Intercepts paste events on the editor container, sanitizes the pasted HTML,
812
- * converts it to our document model, and inserts it at the current cursor.
811
+ * CKEditor-style clipboard pipeline:
813
812
  *
814
- * Paste sanitization is CRITICAL for security and correctness:
815
- * - Strip scripts, iframes, event handlers
816
- * - Normalize formatting (convert <b> → mark, etc.)
817
- * - Reject unknown elements (fall back to plain text)
813
+ * Copy/Cut intercept event, serialize selection from model to clean HTML,
814
+ * write text/html + text/plain to clipboard. Cut also deletes selection.
818
815
  *
819
- * This plugin attaches a 'paste' DOM event listener in its init() and
820
- * removes it on cleanup. It produces a replace_doc transaction so the
821
- * history manager captures the state before the paste as an undo point.
816
+ * Paste normalize HTML from external sources (Word, Google Docs, web),
817
+ * deserialize to model, merge at cursor with block-split/merge logic.
818
+ *
819
+ * Pipeline stages:
820
+ * 1. normalizeHTML — strip MSO/GDocs junk, preserve useful inline styles
821
+ * 2. deserialize — HTML → model (BlockNode[])
822
+ * 3. mergeAtCursor — split current block, insert pasted blocks, merge edges
823
+ * 4. set cursor — place after last pasted character
824
+ *
825
+ * Never uses document.execCommand. Uses Clipboard API, Selection API, DOM APIs only.
822
826
  */
823
827
 
824
828
  declare function createPastePlugin(): EditorPlugin;
825
829
  /**
826
- * Attach paste handling to the editor container.
827
- * Called by the Editor component after the container ref is available.
830
+ * Attach copy, cut, and paste handlers to the editor container.
831
+ * Returns a cleanup function that removes all listeners.
832
+ */
833
+ declare function attachClipboardHandlers(container: HTMLElement, engine: EditorEngineInterface): () => void;
834
+ /**
835
+ * Attach only the paste handler (kept for backward compat).
828
836
  */
829
837
  declare function attachPasteHandler(container: HTMLElement, engine: EditorEngineInterface): () => void;
830
838
 
@@ -971,4 +979,4 @@ declare const DEFAULT_TOOLBAR: EditorToolbarItem[];
971
979
  declare const MINIMAL_TOOLBAR: EditorToolbarItem[];
972
980
  declare const BASIC_TOOLBAR: EditorToolbarItem[];
973
981
 
974
- export { type AddMarkStep, type AlignmentType, BASIC_TOOLBAR, type BlockNode, type BlockNodeType, type CellPosition, type Command, DEFAULT_TOOLBAR, type DeleteNodeStep, type DeleteRangeStep, type DeleteTextStep, type Document, Editor, type EditorAPI, EditorCore, type EditorCoreProps, EditorEngine, type EditorEngineInterface, type EditorNode, type EditorPlugin, type EditorProps, type EditorSelection, type EditorState, type EditorToolbarConfig, type EditorToolbarItem, type InlineNodeType, type InsertNodeStep, type InsertTextStep, type JoinBlocksStep, MINIMAL_TOOLBAR, type Mark, type MarkType, type NodeAttrs, type NodePosition, type NodeType, type RemoveMarkStep, type ReplaceDocStep, type SearchMatch, type SearchOptions, type Serializer, type SetMarkStep, type SetNodeAttrsStep, type SetNodeTypeStep, type SetSelectionStep, type SplitBlockStep, type StateListener, type Step, type StepType, type TableDimensions, TablePlugin, type TextNode, type ThemeConfig, type ThemeMode, type ThemeTokens, Toolbar, type ToolbarItem, type Transaction, addColumnLeft, addColumnRight, addMarkToNode, addRowAbove, addRowBelow, applyMarkToRange, applySearchHighlights, applyTransaction, attachPasteHandler, captureSelection, clearSearchHighlights, collectText, comparePaths, comparePositions, createEmptyDocument, createHistoryManager, createHistoryPlugin, createParagraph, createPastePlugin, createTableCell, createTableNode, createTransaction, deleteColumn, deleteImageAtPath, deleteRange, deleteRow, deleteTable, deleteTableColumn, deleteTableRow, deleteTextAtPath, execCommand, findCellPosition, findContentBlockPath, findMatches, findTablePath, getActiveAlignment, getActiveBlockType, getActiveFontFamily, getActiveFontSize, getActiveHighlightColor, getActiveLinkHref, getActiveLinkRange, getActiveMarks, getActiveTextColor, getCellFirstPosition, getCellLastPosition, getDocumentLength, getDocumentMarkAttrValues, getNodeAtPath, getTableDimensions, getTextNodesBetween, htmlSerializer, insertImage, insertLink, insertTable, insertTableColumnAfter, insertTableColumnBefore, insertTableRowAfter, insertTableRowBefore, insertText, insertTextAtPath, isBlockNode, isContainerBlock, isSelectionInContainer, isTextNode, joinBlocks, jsonSerializer, makeCollapsedSelection, makePosition, markdownSerializer, marksEqual, mergeAdjacentTextNodesInDoc, mergeCells, mergeTableCells, normalizeRange, redo, registerCommand, removeMarkFromNode, removeMarkFromRange, renderDocument, replaceAllMatches, replaceMatch, restoreSelection, scrollMatchIntoView, setAlignment, setBlockType, setCodeBlockLanguage, setColumnWidth, setFontFamily, setFontSize, setHighlightColor, setImageAttr, setMarkOnRange, setTextColor, splitBlock, splitCell, splitTableCell, toggleCheckItemAt, toggleHeaderRow, toggleMark, undo, updateColumnWidth, useEditorEngine, useEditorState, walkDocument };
982
+ export { type AddMarkStep, type AlignmentType, BASIC_TOOLBAR, type BlockNode, type BlockNodeType, type CellPosition, type Command, DEFAULT_TOOLBAR, type DeleteNodeStep, type DeleteRangeStep, type DeleteTextStep, type Document, Editor, type EditorAPI, EditorCore, type EditorCoreProps, EditorEngine, type EditorEngineInterface, type EditorNode, type EditorPlugin, type EditorProps, type EditorSelection, type EditorState, type EditorToolbarConfig, type EditorToolbarItem, type InlineNodeType, type InsertNodeStep, type InsertTextStep, type JoinBlocksStep, MINIMAL_TOOLBAR, type Mark, type MarkType, type NodeAttrs, type NodePosition, type NodeType, type RemoveMarkStep, type ReplaceDocStep, type SearchMatch, type SearchOptions, type Serializer, type SetMarkStep, type SetNodeAttrsStep, type SetNodeTypeStep, type SetSelectionStep, type SplitBlockStep, type StateListener, type Step, type StepType, type TableDimensions, TablePlugin, type TextNode, type ThemeConfig, type ThemeMode, type ThemeTokens, Toolbar, type ToolbarItem, type Transaction, addColumnLeft, addColumnRight, addMarkToNode, addRowAbove, addRowBelow, applyMarkToRange, applySearchHighlights, applyTransaction, attachClipboardHandlers, attachPasteHandler, captureSelection, clearSearchHighlights, collectText, comparePaths, comparePositions, createEmptyDocument, createHistoryManager, createHistoryPlugin, createParagraph, createPastePlugin, createTableCell, createTableNode, createTransaction, deleteColumn, deleteImageAtPath, deleteRange, deleteRow, deleteTable, deleteTableColumn, deleteTableRow, deleteTextAtPath, execCommand, findCellPosition, findContentBlockPath, findMatches, findTablePath, getActiveAlignment, getActiveBlockType, getActiveFontFamily, getActiveFontSize, getActiveHighlightColor, getActiveLinkHref, getActiveLinkRange, getActiveMarks, getActiveTextColor, getCellFirstPosition, getCellLastPosition, getDocumentLength, getDocumentMarkAttrValues, getNodeAtPath, getTableDimensions, getTextNodesBetween, htmlSerializer, insertImage, insertLink, insertTable, insertTableColumnAfter, insertTableColumnBefore, insertTableRowAfter, insertTableRowBefore, insertText, insertTextAtPath, isBlockNode, isContainerBlock, isSelectionInContainer, isTextNode, joinBlocks, jsonSerializer, makeCollapsedSelection, makePosition, markdownSerializer, marksEqual, mergeAdjacentTextNodesInDoc, mergeCells, mergeTableCells, normalizeRange, redo, registerCommand, removeMarkFromNode, removeMarkFromRange, renderDocument, replaceAllMatches, replaceMatch, restoreSelection, scrollMatchIntoView, setAlignment, setBlockType, setCodeBlockLanguage, setColumnWidth, setFontFamily, setFontSize, setHighlightColor, setImageAttr, setMarkOnRange, setTextColor, splitBlock, splitCell, splitTableCell, toggleCheckItemAt, toggleHeaderRow, toggleMark, undo, updateColumnWidth, useEditorEngine, useEditorState, walkDocument };
package/dist/index.js CHANGED
@@ -1655,10 +1655,14 @@ function serializeBlock(node, idCounts = /* @__PURE__ */ new Map()) {
1655
1655
  case "list_item":
1656
1656
  return `<li>${serializeChildren(node.children)}</li>`;
1657
1657
  case "check_list":
1658
- return `<ul data-type="checklist">${serializeChildren(node.children)}</ul>`;
1658
+ return `<ul class="todo-list" data-type="checklist">${serializeChildren(node.children)}</ul>`;
1659
1659
  case "check_list_item": {
1660
- const checked = ((_g = node.attrs) == null ? void 0 : _g.checked) ? ' data-checked="true"' : "";
1661
- return `<li${checked}>${serializeChildren(node.children)}</li>`;
1660
+ const checked = !!((_g = node.attrs) == null ? void 0 : _g.checked);
1661
+ const dataChecked = checked ? ' data-checked="true"' : "";
1662
+ const itemClass = `todo-list__item${checked ? " todo-list__item_checked" : ""}`;
1663
+ const checkedAttr = checked ? ' checked="checked"' : "";
1664
+ const innerContent = serializeChildren(node.children);
1665
+ return `<li class="${itemClass}"${dataChecked}><label class="todo-list__label"><input type="checkbox" disabled="disabled"${checkedAttr}><span class="todo-list__label__description">${innerContent}</span></label></li>`;
1662
1666
  }
1663
1667
  case "code_block": {
1664
1668
  const lang = ((_h = node.attrs) == null ? void 0 : _h.language) ? ` class="language-${node.attrs.language}"` : "";
@@ -1931,6 +1935,7 @@ function parseInlineChildren(el) {
1931
1935
  results.push({ type: "text", text: "\n", marks: [...marks] });
1932
1936
  return;
1933
1937
  }
1938
+ if (tag === "input") return;
1934
1939
  const newMarks = [...marks, ...getMarksForTag(tag, elem)];
1935
1940
  for (const child of Array.from(elem.childNodes)) {
1936
1941
  walk(child, newMarks);
@@ -1980,10 +1985,16 @@ function getMarksForTag(tag, el) {
1980
1985
 
1981
1986
  // src/editor/plugins/PastePlugin.ts
1982
1987
  function createPastePlugin() {
1983
- return {
1984
- name: "paste"
1985
- // Paste handling is attached to the DOM via attachPasteHandler; the plugin
1986
- // exists only so the engine knows the feature is registered.
1988
+ return { name: "clipboard" };
1989
+ }
1990
+ function attachClipboardHandlers(container, engine) {
1991
+ const removeCopy = attachCopyOrCutHandler(container, engine, false);
1992
+ const removeCut = attachCopyOrCutHandler(container, engine, true);
1993
+ const removePaste = attachPasteHandler(container, engine);
1994
+ return () => {
1995
+ removeCopy();
1996
+ removeCut();
1997
+ removePaste();
1987
1998
  };
1988
1999
  }
1989
2000
  function attachPasteHandler(container, engine) {
@@ -1999,11 +2010,9 @@ function attachPasteHandler(container, engine) {
1999
2010
  if (file) {
2000
2011
  const blobUrl = URL.createObjectURL(file);
2001
2012
  const state2 = engine.getState();
2002
- ({ ...state2.doc });
2013
+ const sel2 = state2.selection;
2014
+ const insertIdx = sel2 ? sel2.anchor.path[0] + 1 : state2.doc.children.length;
2003
2015
  const tr2 = createTransaction();
2004
- const sel = state2.selection;
2005
- const blockPath = sel ? [sel.anchor.path[0]] : [state2.doc.children.length - 1];
2006
- const insertIdx = blockPath[0] + 1;
2007
2016
  tr2.steps.push({
2008
2017
  type: "insert_node",
2009
2018
  parentPath: [],
@@ -2016,56 +2025,346 @@ function attachPasteHandler(container, engine) {
2016
2025
  }
2017
2026
  const html = clipboardData.getData("text/html");
2018
2027
  const plainText = clipboardData.getData("text/plain");
2019
- let newDoc;
2028
+ let pastedDoc;
2020
2029
  if (html) {
2021
- const sanitized = sanitizeHTML(html);
2022
- newDoc = htmlSerializer.deserialize(sanitized);
2030
+ const normalized = normalizeHTML(html);
2031
+ pastedDoc = htmlSerializer.deserialize(normalized);
2023
2032
  } else if (plainText) {
2024
- const trimmed = plainText.trim();
2025
- if (isURL(trimmed)) {
2026
- const href = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
2027
- const escaped = escapeHTML2(trimmed);
2028
- newDoc = htmlSerializer.deserialize(
2029
- `<p><a href="${escapeAttr2(href)}">${escaped}</a></p>`
2030
- );
2031
- } else {
2032
- newDoc = htmlSerializer.deserialize(`<p>${escapeHTML2(plainText)}</p>`);
2033
- }
2033
+ pastedDoc = parsePlainText(plainText);
2034
2034
  } else {
2035
2035
  return;
2036
2036
  }
2037
+ const pastedBlocks = pastedDoc.children.filter(
2038
+ (b) => !(b.type === "paragraph" && b.children.length === 0)
2039
+ );
2040
+ if (pastedBlocks.length === 0) return;
2037
2041
  const state = engine.getState();
2038
- const merged = {
2039
- ...state.doc,
2040
- children: [...state.doc.children, ...newDoc.children]
2041
- };
2042
+ let sel = state.selection;
2043
+ if (sel && !sel.isCollapsed) {
2044
+ const { from, to } = normalizeRange(sel.anchor, sel.focus);
2045
+ const delTr = createTransaction();
2046
+ delTr.steps.push(tr_deleteRange(from, to));
2047
+ engine.dispatch(delTr);
2048
+ sel = engine.getState().selection;
2049
+ }
2050
+ const currentState = engine.getState();
2051
+ const { mergedDoc, cursorPos } = mergeAtCursor(
2052
+ currentState,
2053
+ { children: pastedBlocks },
2054
+ sel
2055
+ );
2042
2056
  const tr = createTransaction();
2043
- tr.steps.push(tr_replaceDoc(merged));
2057
+ tr.steps.push(tr_replaceDoc(mergedDoc));
2058
+ if (cursorPos) {
2059
+ tr.steps.push(tr_setSelection({ anchor: cursorPos, focus: cursorPos, isCollapsed: true }));
2060
+ }
2044
2061
  engine.dispatch(tr);
2045
2062
  };
2046
2063
  container.addEventListener("paste", handler);
2047
2064
  return () => container.removeEventListener("paste", handler);
2048
2065
  }
2049
- function sanitizeHTML(html) {
2066
+ function attachCopyOrCutHandler(container, engine, isCut) {
2067
+ const handler = (e) => {
2068
+ const state = engine.getState();
2069
+ const sel = state.selection;
2070
+ if (!sel || sel.isCollapsed) return;
2071
+ e.preventDefault();
2072
+ const selectedDoc = extractSelection(state);
2073
+ const selectedHTML = htmlSerializer.serialize(selectedDoc);
2074
+ const selectedText = docToPlainText(selectedDoc);
2075
+ try {
2076
+ e.clipboardData.setData("text/html", selectedHTML);
2077
+ e.clipboardData.setData("text/plain", selectedText);
2078
+ } catch (e2) {
2079
+ }
2080
+ if (isCut) {
2081
+ const { from, to } = normalizeRange(sel.anchor, sel.focus);
2082
+ const tr = createTransaction();
2083
+ tr.steps.push(tr_deleteRange(from, to));
2084
+ engine.dispatch(tr);
2085
+ }
2086
+ };
2087
+ const event = isCut ? "cut" : "copy";
2088
+ container.addEventListener(event, handler);
2089
+ return () => container.removeEventListener(event, handler);
2090
+ }
2091
+ function extractSelection(state) {
2092
+ var _a, _b;
2093
+ const sel = state.selection;
2094
+ if (!sel || sel.isCollapsed) return { type: "doc", children: [createParagraph()] };
2095
+ const { from, to } = normalizeRange(sel.anchor, sel.focus);
2096
+ const blocks = state.doc.children;
2097
+ const fromBlockIdx = (_a = from.path[0]) != null ? _a : 0;
2098
+ const toBlockIdx = (_b = to.path[0]) != null ? _b : 0;
2099
+ if (fromBlockIdx === toBlockIdx) {
2100
+ const sliced = sliceBlock(blocks[fromBlockIdx], from.path.slice(1), from.offset, to.path.slice(1), to.offset);
2101
+ return { type: "doc", children: sliced.length > 0 ? sliced : [createParagraph()] };
2102
+ }
2103
+ const firstBlock = sliceBlockFrom(blocks[fromBlockIdx], from.path.slice(1), from.offset);
2104
+ const middleBlocks = blocks.slice(fromBlockIdx + 1, toBlockIdx);
2105
+ const lastBlock = sliceBlockTo(blocks[toBlockIdx], to.path.slice(1), to.offset);
2106
+ return { type: "doc", children: [firstBlock, ...middleBlocks, lastBlock] };
2107
+ }
2108
+ function sliceBlock(block, fromRelPath, fromOffset, toRelPath, toOffset) {
2109
+ var _a, _b;
2110
+ const fromIdx = (_a = fromRelPath[0]) != null ? _a : 0;
2111
+ const toIdx = (_b = toRelPath[0]) != null ? _b : Math.max(0, block.children.length - 1);
2112
+ if (!block.children.every((c) => isTextNode(c))) {
2113
+ return [block];
2114
+ }
2115
+ const children = [];
2116
+ for (let i = fromIdx; i <= toIdx && i < block.children.length; i++) {
2117
+ const t = block.children[i];
2118
+ const s = i === fromIdx ? fromOffset : 0;
2119
+ const end = i === toIdx ? toOffset : t.text.length;
2120
+ const slice = t.text.slice(s, end);
2121
+ if (slice) children.push({ type: "text", text: slice, marks: t.marks });
2122
+ }
2123
+ return children.length > 0 ? [{ ...block, children }] : [];
2124
+ }
2125
+ function sliceBlockFrom(block, relPath, offset) {
2126
+ var _a;
2127
+ const fromIdx = (_a = relPath[0]) != null ? _a : 0;
2128
+ if (!block.children.every((c) => isTextNode(c))) return block;
2129
+ const children = [];
2130
+ for (let i = fromIdx; i < block.children.length; i++) {
2131
+ const t = block.children[i];
2132
+ const s = i === fromIdx ? offset : 0;
2133
+ const slice = t.text.slice(s);
2134
+ if (slice) children.push({ type: "text", text: slice, marks: t.marks });
2135
+ }
2136
+ return { ...block, children };
2137
+ }
2138
+ function sliceBlockTo(block, relPath, offset) {
2139
+ var _a;
2140
+ const toIdx = (_a = relPath[0]) != null ? _a : Math.max(0, block.children.length - 1);
2141
+ if (!block.children.every((c) => isTextNode(c))) return block;
2142
+ const children = [];
2143
+ for (let i = 0; i <= toIdx && i < block.children.length; i++) {
2144
+ const t = block.children[i];
2145
+ const end = i === toIdx ? offset : t.text.length;
2146
+ const slice = t.text.slice(0, end);
2147
+ if (slice) children.push({ type: "text", text: slice, marks: t.marks });
2148
+ }
2149
+ return { ...block, children };
2150
+ }
2151
+ function mergeAtCursor(state, pastedDoc, sel) {
2152
+ var _a, _b;
2153
+ const pasted = pastedDoc.children;
2154
+ if (pasted.length === 0) return { mergedDoc: state.doc, cursorPos: (_a = sel == null ? void 0 : sel.anchor) != null ? _a : null };
2155
+ const existingBlocks = state.doc.children;
2156
+ if (!sel) {
2157
+ const mergedDoc = { ...state.doc, children: [...existingBlocks, ...pasted] };
2158
+ const lastIdx = mergedDoc.children.length - 1;
2159
+ const cursorPos = endOfBlock(mergedDoc.children[lastIdx], lastIdx);
2160
+ return { mergedDoc, cursorPos };
2161
+ }
2162
+ const blockIdx = (_b = sel.anchor.path[0]) != null ? _b : 0;
2163
+ const relPath = sel.anchor.path.slice(1);
2164
+ const charOffset = sel.anchor.offset;
2165
+ const currentBlock = existingBlocks[blockIdx];
2166
+ if (!currentBlock) {
2167
+ const mergedDoc = { ...state.doc, children: [...existingBlocks, ...pasted] };
2168
+ const lastIdx = mergedDoc.children.length - 1;
2169
+ return { mergedDoc, cursorPos: endOfBlock(mergedDoc.children[lastIdx], lastIdx) };
2170
+ }
2171
+ const [beforeChildren, afterChildren] = splitBlockChildren(currentBlock, relPath, charOffset);
2172
+ let newBlocks;
2173
+ let cursorBlockOffset;
2174
+ let cursorInBlock;
2175
+ if (pasted.length === 1) {
2176
+ const pastedChildren = pasted[0].children;
2177
+ const mergedChildren2 = [...beforeChildren, ...pastedChildren, ...afterChildren];
2178
+ const cleanChildren = mergedChildren2.length > 0 ? mergedChildren2 : [{ type: "text", text: "", marks: [] }];
2179
+ newBlocks = [{ ...currentBlock, children: cleanChildren }];
2180
+ cursorBlockOffset = 0;
2181
+ const cursorChildIdx = beforeChildren.length + pastedChildren.length;
2182
+ const nodeBeforeCursor = cleanChildren[cursorChildIdx - 1];
2183
+ if (nodeBeforeCursor && isTextNode(nodeBeforeCursor)) {
2184
+ cursorInBlock = {
2185
+ path: [blockIdx, cursorChildIdx - 1],
2186
+ offset: nodeBeforeCursor.text.length
2187
+ };
2188
+ } else {
2189
+ cursorInBlock = { path: [blockIdx], offset: 0 };
2190
+ }
2191
+ } else {
2192
+ const firstPasted = pasted[0];
2193
+ const lastPasted = pasted[pasted.length - 1];
2194
+ const middleBlocks = pasted.slice(1, -1);
2195
+ const firstMerged = {
2196
+ ...currentBlock,
2197
+ children: [...beforeChildren, ...firstPasted.children]
2198
+ };
2199
+ const lastMerged = {
2200
+ type: lastPasted.type,
2201
+ attrs: lastPasted.attrs,
2202
+ children: [...lastPasted.children, ...afterChildren]
2203
+ };
2204
+ newBlocks = [firstMerged, ...middleBlocks, lastMerged];
2205
+ cursorBlockOffset = newBlocks.length - 1;
2206
+ const lpChildren = lastPasted.children;
2207
+ const absoluteLastBlockIdx = blockIdx + cursorBlockOffset;
2208
+ if (lpChildren.length > 0) {
2209
+ const lastNode = lpChildren[lpChildren.length - 1];
2210
+ cursorInBlock = {
2211
+ path: [absoluteLastBlockIdx, lpChildren.length - 1],
2212
+ offset: isTextNode(lastNode) ? lastNode.text.length : 0
2213
+ };
2214
+ } else {
2215
+ cursorInBlock = { path: [absoluteLastBlockIdx], offset: 0 };
2216
+ }
2217
+ }
2218
+ const mergedChildren = [
2219
+ ...existingBlocks.slice(0, blockIdx),
2220
+ ...newBlocks,
2221
+ ...existingBlocks.slice(blockIdx + 1)
2222
+ ];
2223
+ return {
2224
+ mergedDoc: { ...state.doc, children: mergedChildren },
2225
+ cursorPos: cursorInBlock
2226
+ };
2227
+ }
2228
+ function splitBlockChildren(block, relPath, offset) {
2229
+ const children = block.children;
2230
+ if (children.length === 0) return [[], []];
2231
+ if (relPath.length === 0) return [[], [...children]];
2232
+ const textIdx = relPath[0];
2233
+ if (textIdx >= children.length) return [[...children], []];
2234
+ const node = children[textIdx];
2235
+ if (!isTextNode(node)) {
2236
+ return [[...children.slice(0, textIdx)], [...children.slice(textIdx)]];
2237
+ }
2238
+ const textNode = node;
2239
+ const before = [
2240
+ ...children.slice(0, textIdx),
2241
+ ...offset > 0 ? [{ type: "text", text: textNode.text.slice(0, offset), marks: textNode.marks }] : []
2242
+ ];
2243
+ const after = [
2244
+ ...offset < textNode.text.length ? [{ type: "text", text: textNode.text.slice(offset), marks: textNode.marks }] : [],
2245
+ ...children.slice(textIdx + 1)
2246
+ ];
2247
+ return [before, after];
2248
+ }
2249
+ function endOfBlock(block, blockIdx) {
2250
+ const children = block.children;
2251
+ for (let i = children.length - 1; i >= 0; i--) {
2252
+ const n = children[i];
2253
+ if (isTextNode(n)) {
2254
+ return { path: [blockIdx, i], offset: n.text.length };
2255
+ }
2256
+ }
2257
+ return { path: [blockIdx], offset: 0 };
2258
+ }
2259
+ function parsePlainText(text) {
2260
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
2261
+ if (isURL(normalized)) {
2262
+ const href = /^https?:\/\//i.test(normalized) ? normalized : `https://${normalized}`;
2263
+ return htmlSerializer.deserialize(
2264
+ `<p><a href="${escapeAttr2(href)}">${escapeHTML2(normalized)}</a></p>`
2265
+ );
2266
+ }
2267
+ const paragraphs = normalized.split(/\n{2,}/);
2268
+ if (paragraphs.length === 1) {
2269
+ const lines = normalized.split("\n");
2270
+ if (lines.length === 1) {
2271
+ return htmlSerializer.deserialize(`<p>${escapeHTML2(normalized)}</p>`);
2272
+ }
2273
+ const html2 = lines.map((l) => escapeHTML2(l)).join("<br>");
2274
+ return htmlSerializer.deserialize(`<p>${html2}</p>`);
2275
+ }
2276
+ const html = paragraphs.map((p) => `<p>${p.split("\n").map((l) => escapeHTML2(l)).join("<br>")}</p>`).join("");
2277
+ return htmlSerializer.deserialize(html);
2278
+ }
2279
+ function normalizeHTML(html) {
2280
+ let out = html.replace(/<!--[\s\S]*?-->/g, "");
2281
+ out = out.replace(/<!\[if[^\]]*\]>[\s\S]*?<!\[endif\]>/gi, "");
2282
+ out = out.replace(/<\/?(?:o|w|m|v|st\d?|x):[^>]*>/gi, "");
2050
2283
  const parser = new DOMParser();
2051
- const doc = parser.parseFromString(html, "text/html");
2052
- const dangerous = doc.querySelectorAll(
2053
- "script, style, iframe, object, embed, form, input, button, select, textarea, meta, link"
2054
- );
2055
- dangerous.forEach((el) => el.remove());
2056
- doc.querySelectorAll("*").forEach((el) => {
2057
- const attrs = Array.from(el.attributes);
2058
- attrs.forEach((attr) => {
2059
- if (attr.name.startsWith("on")) {
2060
- el.removeAttribute(attr.name);
2061
- }
2062
- });
2284
+ const doc = parser.parseFromString(out, "text/html");
2285
+ doc.querySelectorAll(
2286
+ "script, style, iframe, object, embed, form, button, select, textarea, meta, link"
2287
+ ).forEach((el) => el.remove());
2288
+ doc.querySelectorAll("input").forEach((el) => {
2289
+ var _a;
2290
+ const isCheckbox = ((_a = el.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "checkbox";
2291
+ const inTodoList = !!el.closest('ul[data-type="checklist"], ul.todo-list, li.todo-list__item, li[data-checked]');
2292
+ if (!isCheckbox || !inTodoList) el.remove();
2293
+ });
2294
+ doc.querySelectorAll("b").forEach((el) => {
2295
+ const fw = el.style.fontWeight;
2296
+ if (fw === "normal" || fw === "400" || fw === "inherit" || fw === "") {
2297
+ const parent = el.parentNode;
2298
+ if (!parent) return;
2299
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
2300
+ el.remove();
2301
+ }
2302
+ });
2303
+ doc.querySelectorAll("*").forEach((node) => {
2304
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
2305
+ const el = node;
2306
+ const tag = el.tagName.toLowerCase();
2307
+ const fontSize = (_b = (_a = el.style) == null ? void 0 : _a.fontSize) != null ? _b : "";
2308
+ const fontFamily = (_d = (_c = el.style) == null ? void 0 : _c.fontFamily) != null ? _d : "";
2309
+ const color = (_f = (_e = el.style) == null ? void 0 : _e.color) != null ? _f : "";
2310
+ const bgColor = (_h = (_g = el.style) == null ? void 0 : _g.backgroundColor) != null ? _h : "";
2063
2311
  el.removeAttribute("style");
2312
+ const safeStyle = [];
2313
+ if (fontSize) safeStyle.push(`font-size:${fontSize}`);
2314
+ if (fontFamily) safeStyle.push(`font-family:${fontFamily}`);
2315
+ if (color) safeStyle.push(`color:${color}`);
2316
+ if (bgColor && !["transparent", "rgba(0, 0, 0, 0)", "white", "rgb(255, 255, 255)", "#ffffff", "#fff"].includes(
2317
+ bgColor.toLowerCase().replace(/\s/g, "")
2318
+ )) {
2319
+ safeStyle.push(`background-color:${bgColor}`);
2320
+ }
2321
+ if (safeStyle.length > 0) el.setAttribute("style", safeStyle.join(";"));
2064
2322
  el.removeAttribute("class");
2065
2323
  el.removeAttribute("id");
2324
+ Array.from(el.attributes).forEach((attr) => {
2325
+ if (attr.name.startsWith("on")) el.removeAttribute(attr.name);
2326
+ });
2327
+ Array.from(el.attributes).forEach((attr) => {
2328
+ if (attr.name.startsWith("data-") && !["data-align", "data-type", "data-checked"].includes(attr.name)) {
2329
+ el.removeAttribute(attr.name);
2330
+ }
2331
+ });
2332
+ if (tag === "a") {
2333
+ const href = (_i = el.getAttribute("href")) != null ? _i : "";
2334
+ const isUnsafe = /^(javascript:|vbscript:|data:)/i.test(href.trim());
2335
+ Array.from(el.attributes).forEach((attr) => {
2336
+ if (!["href", "target", "rel", "style"].includes(attr.name)) el.removeAttribute(attr.name);
2337
+ });
2338
+ if (isUnsafe) el.removeAttribute("href");
2339
+ }
2340
+ if (tag === "img") {
2341
+ Array.from(el.attributes).forEach((attr) => {
2342
+ if (!["src", "alt", "width", "height", "style"].includes(attr.name)) el.removeAttribute(attr.name);
2343
+ });
2344
+ }
2345
+ if (tag === "td" || tag === "th") {
2346
+ Array.from(el.attributes).forEach((attr) => {
2347
+ if (!["colspan", "rowspan", "style"].includes(attr.name)) el.removeAttribute(attr.name);
2348
+ });
2349
+ }
2350
+ if (tag === "code") {
2351
+ Array.from(el.attributes).forEach((attr) => {
2352
+ if (!["class", "style"].includes(attr.name)) el.removeAttribute(attr.name);
2353
+ });
2354
+ }
2066
2355
  });
2067
2356
  return doc.body.innerHTML;
2068
2357
  }
2358
+ function docToPlainText(doc) {
2359
+ return doc.children.map(blockToPlainText).join("\n\n");
2360
+ }
2361
+ function blockToPlainText(block) {
2362
+ if (block.children.length === 0) return "";
2363
+ return block.children.map((c) => {
2364
+ if (isTextNode(c)) return c.text;
2365
+ return blockToPlainText(c);
2366
+ }).join("");
2367
+ }
2069
2368
  function escapeHTML2(str) {
2070
2369
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2071
2370
  }
@@ -7585,7 +7884,7 @@ function EditorCore({
7585
7884
  react.useEffect(() => {
7586
7885
  const container = containerRef.current;
7587
7886
  if (!container || readOnly) return;
7588
- return attachPasteHandler(container, engine);
7887
+ return attachClipboardHandlers(container, engine);
7589
7888
  }, [engine, readOnly]);
7590
7889
  react.useEffect(() => {
7591
7890
  const container = containerRef.current;
@@ -8735,6 +9034,7 @@ exports.addRowBelow = addRowBelow;
8735
9034
  exports.applyMarkToRange = applyMarkToRange;
8736
9035
  exports.applySearchHighlights = applySearchHighlights;
8737
9036
  exports.applyTransaction = applyTransaction;
9037
+ exports.attachClipboardHandlers = attachClipboardHandlers;
8738
9038
  exports.attachPasteHandler = attachPasteHandler;
8739
9039
  exports.captureSelection = captureSelection;
8740
9040
  exports.clearSearchHighlights = clearSearchHighlights;