UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

1,460 lines 70.4 kB
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid/v4';
import { transformContainerNodes } from '@atlaskit/adf-utils/transforms';
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { addLinkMetadata } from '@atlaskit/editor-common/card';
import { insideTable } from '@atlaskit/editor-common/core-utils';
import { getBlockMarkAttrs, getFirstParagraphBlockMarkAttrs, reconcileBlockMarkForContainerAtPos, reconcileBlockMarkForParagraphAtPos, reconcileBlockMarkInRange } from '@atlaskit/editor-common/lists';
import { anyMarkActive } from '@atlaskit/editor-common/mark';
import { getParentOfTypeCount, getPositionAfterTopParentNodeOfType } from '@atlaskit/editor-common/nesting';
import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection';
import { canLinkBeCreatedInRange, insideTableCell, isInListItem, isLinkMark, isListItemNode, isListNode, isNodeEmpty, isParagraph, isText, linkifyContent, mapSlice } from '@atlaskit/editor-common/utils';
import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils';
import { Fragment, Node as PMNode, Slice } from '@atlaskit/editor-prosemirror/model';
import { AllSelection, NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { canInsert, contains, findParentNodeOfType, findParentNodeOfTypeClosestToPos, hasParentNode, hasParentNodeOfType, safeInsert } from '@atlaskit/editor-prosemirror/utils';
import { replaceSelectedTable } from '@atlaskit/editor-tables/utils';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { fg } from '@atlaskit/platform-feature-flags';
import { closeHistory } from '@atlaskit/prosemirror-history';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
// TODO: ED-20519 - Needs Macro extraction

import { startTrackingPastedMacroPositions, stopTrackingPastedMacroPositions } from '../../editor-commands/commands';
import { getPluginState as getPastePluginState } from '../plugin-factory';
import { insertSliceForLists, insertSliceForTaskInsideList, insertSliceInsideBlockquote, updateSelectionAfterReplace } from './edge-cases';
import { insertSliceInsideOfPanelNodeSelected } from './edge-cases/lists';
import { addReplaceSelectedTableAnalytics, applyTextMarksToSlice, hasOnlyNodesOfType, isEmptyNode, isSelectionInsidePanel } from './index';
const insideExpand = state => {
  const {
    expand,
    nestedExpand
  } = state.schema.nodes;
  return hasParentNodeOfType([expand, nestedExpand])(state.selection);
};

/** Helper type for single arg function */

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Compose 1 to n functions.
 * @param func first function
 * @param funcs additional functions
 */
function compose(func, ...funcs) {
  const allFuncs = [func, ...funcs];
  return function composed(raw) {
    return allFuncs.reduceRight((memo, func) => func(memo), raw);
  };
}
/* eslint-enable @typescript-eslint/no-explicit-any */

// remove text attribute from mention for copy/paste (GDPR)
export function handleMention(slice, schema) {
  return mapSlice(slice, node => {
    var _schema$nodes$mention;
    // We should move this to the mention plugin when we refactor how paste works in the future
    // For now we can just null check mention exists in the schema to ensure we don't crash if it doesn't
    // exist.
    if (node.type.name === ((_schema$nodes$mention = schema.nodes.mention) === null || _schema$nodes$mention === void 0 ? void 0 : _schema$nodes$mention.name)) {
      const mention = node.attrs;
      const newMention = {
        ...mention,
        text: ''
      };
      return schema.nodes.mention.create(newMention, node.content, node.marks);
    }
    return node;
  });
}
export function handlePasteIntoTaskOrDecisionOrPanel(slice, queueCardsFromChangedTr) {
  return (state, dispatch) => {
    var _slice$content$firstC, _slice$content$firstC2, _panelNode$type, _transformedSlice$con, _transformedSlice$con2;
    const {
      schema,
      tr: {
        selection
      }
    } = state;
    const {
      marks: {
        code: codeMark
      },
      nodes: {
        decisionItem,
        emoji,
        hardBreak,
        mention,
        paragraph,
        taskItem,
        text,
        panel,
        panel_c1,
        bulletList,
        orderedList,
        taskList,
        listItem,
        expand,
        heading,
        codeBlock
      }
    } = schema;
    const selectionIsValidNode = state.selection instanceof NodeSelection && ['decisionList', 'decisionItem', 'taskList', 'taskItem'].includes(state.selection.node.type.name);
    // panel_c1 is a schema variant of panel (used when a table is nested inside a panel). When the
    // experiment is on we must handle it anywhere we special-case a panel, otherwise partial
    // selections copied from inside a table-in-panel are treated as a foreign container and
    // re-wrapped on paste.
    const selectionHasValidParentNode = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? hasParentNodeOfType([decisionItem, taskItem, panel, panel_c1].filter(Boolean))(state.selection) : hasParentNodeOfType([decisionItem, taskItem, panel])(state.selection);
    const selectionIsCodeBlock = hasParentNodeOfType([codeBlock])(state.selection);
    const selectionIsListItem = hasParentNodeOfType([listItem])(state.selection);
    const panelNode = isSelectionInsidePanel(selection);
    const selectionIsPanel = Boolean(panelNode);
    const sliceFirstChildIsPanel = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? !!slice.content.firstChild && getBaseNodeTypeName(slice.content.firstChild.type) === 'panel' : ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type) === panel;
    const isSliceWholePanel = sliceFirstChildIsPanel && slice.openStart === 0 && slice.openEnd === 0;

    // we avoid handling codeBlock-in-panel use case in this function
    // returning false will allow code to flow into `handleCodeBlock` function
    // Partial content copied from panels will have panel in the slice
    // Return false to avoid handling this situation when pasted into list in panel and let `handlePastePanelOrDecisionContentIntoList` handle it
    if (selectionIsPanel && (selectionIsCodeBlock || selectionIsListItem && !isSliceWholePanel)) {
      return false;
    }

    // Some types of content should be handled by the default handler, not this function.
    // Check through slice content to see if it contains an invalid node.
    let sliceIsInvalid = false;
    let sliceHasTask = false;
    slice.content.nodesBetween(0, slice.content.size, node => {
      if (node.type === bulletList || node.type === orderedList || node.type === expand || node.type === heading && !(selectionIsPanel && isExperimentEnabled('platform_editor_fix_header_paste_in_panel')) || node.type === listItem) {
        sliceIsInvalid = true;
      }
      if (selectionIsPanel && node.type === taskList) {
        sliceHasTask = true;
      }
    });
    // If the selection is a panel,
    // and the slice's first node is a paragraph
    // and it is not from a depth that would indicate it being from inside from another node (e.g. text from a decision)
    // then we can rely on the default behaviour.
    const selectionIsTaskOrDecision = hasParentNode(node => node.type === taskItem || node.type === decisionItem)(selection);
    const sliceIsAPanelReceivingLowDepthText = selectionIsPanel && !selectionIsTaskOrDecision && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.type) === paragraph && slice.openEnd < 2;
    if (sliceIsInvalid || sliceIsAPanelReceivingLowDepthText || !selectionIsValidNode && !selectionHasValidParentNode) {
      return false;
    }
    const filters = [linkifyContent(schema)];
    const selectionMarks = selection.$head.marks();
    if (selection instanceof TextSelection && Array.isArray(selectionMarks) && selectionMarks.length > 0 && hasOnlyNodesOfType(paragraph, text, emoji, mention, hardBreak)(slice) && (!codeMark.isInSet(selectionMarks) || anyMarkActive(state, codeMark)) // check if there is a code mark anywhere in the selection
    ) {
      filters.push(applyTextMarksToSlice(schema, selection.$head.marks()));
    }
    const transformedSlice = compose.apply(null, filters)(slice);
    const isFirstChildTaskNode = transformedSlice.content.firstChild.type === taskList || transformedSlice.content.firstChild.type === taskItem;
    const tr = closeHistory(state.tr);
    if (panelNode && sliceHasTask && sliceFirstChildIsPanel && isEmptyNode(panelNode) && selection.$from.node() === selection.$to.node()) {
      return Boolean(insertSliceInsideOfPanelNodeSelected(panelNode)({
        tr,
        slice
      }));
    }
    const transformedSliceIsValidNode = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? (transformedSlice.content.firstChild.type.inlineContent || ['decisionList', 'decisionItem', 'taskItem', 'taskList', 'panel'].includes(getBaseNodeTypeName(transformedSlice.content.firstChild.type))) && (!isInListItem(state) || isInListItem(state) && isFirstChildTaskNode) : (transformedSlice.content.firstChild.type.inlineContent || ['decisionList', 'decisionItem', 'taskItem', 'taskList', 'panel'].includes(transformedSlice.content.firstChild.type.name)) && (!isInListItem(state) || isInListItem(state) && isFirstChildTaskNode);
    // If the slice or the selection are valid nodes to handle,
    // and the slice is not a whole node (i.e. openStart is 1 and openEnd is 0)
    // or the slice's first node is a paragraph,
    // then we can replace the selection with our slice.
    // When the experiment is on, validate against the actual destination panel variant (panel or
    // panel_c1) since their content expressions differ (panel_c1 additionally allows a nested
    // table).
    const pastingIntoExtendedPanel = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? selectionIsPanel && ((_panelNode$type = panelNode === null || panelNode === void 0 ? void 0 : panelNode.type) !== null && _panelNode$type !== void 0 ? _panelNode$type : panel).validContent(transformedSlice.content) : selectionIsPanel && panel.validContent(transformedSlice.content);

    // A task slice carrying its own open taskList wrapper would, on a plain replaceSelection,
    // nest that wrapper inside the current item and add an extra taskList level. Unwrapping one
    // level keeps the pasted task(s) at the same level. Guarded by a kill switch.
    const isOpenTaskListSlice = ((_transformedSlice$con = transformedSlice.content.firstChild) === null || _transformedSlice$con === void 0 ? void 0 : _transformedSlice$con.type) === taskList && transformedSlice.openStart >= 2 && transformedSlice.openEnd >= 1;
    // Restrict the unwrap to a taskItem destination: its task content is schema-invalid inside a
    // decisionItem (decisionList only accepts decisionItem), so decisionItem must fall through to
    // the normal replaceSelection path.
    const pastingIntoTaskItem = hasParentNodeOfType([taskItem])(selection);
    if (expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true) && !fg('platform_editor_flexible_list_kill_switch_1') && isOpenTaskListSlice && pastingIntoTaskItem && !selectionIsPanel) {
      const unwrappedSlice = new Slice(transformedSlice.content.firstChild.content, transformedSlice.openStart - 1, transformedSlice.openEnd - 1);
      tr.replaceSelection(unwrappedSlice).scrollIntoView();
    } else if ((transformedSliceIsValidNode || selectionIsValidNode) && !pastingIntoExtendedPanel && !(transformedSlice.openStart === 1 && transformedSlice.openEnd === 0 ||
    // Whole codeblock node has reverse slice depths.
    transformedSlice.openStart === 0 && transformedSlice.openEnd === 1) || ((_transformedSlice$con2 = transformedSlice.content.firstChild) === null || _transformedSlice$con2 === void 0 ? void 0 : _transformedSlice$con2.type) === paragraph) {
      tr.replaceSelection(transformedSlice).scrollIntoView();
    } else {
      const isWholeContentSelected = selection.$from.pos === selection.$from.start() && selection.$to.end() === selection.$to.pos;
      if (pastingIntoExtendedPanel && selection.$from.pos !== selection.$to.pos && !isWholeContentSelected) {
        // Do a replaceSelection if the entire panel content isn't selected
        //tr.replaceSelection(transformedSlice).scrollIntoView();
        tr.replaceSelection(new Slice(transformedSlice.content, 0, transformedSlice.openEnd)).scrollIntoView();
      } else if (['mediaSingle'].includes(transformedSlice.content.firstChild.type.name) && selectionIsPanel) {
        const parentNode = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? findParentNodeOfType([panel, panel_c1].filter(Boolean))(selection) : findParentNodeOfType(panel)(selection);
        if (selectionIsPanel && parentNode && isNodeEmpty(parentNode.node)) {
          tr.insert(selection.$from.pos, transformedSlice.content).scrollIntoView();
          // Place the cursor at the the end of the insersertion
          const endPos = tr.selection.from + transformedSlice.size;
          tr.setSelection(new TextSelection(tr.doc.resolve(endPos)));
        } else {
          tr.replaceSelection(transformedSlice).scrollIntoView();
        }
      } else {
        var _transformedSlice$con3, _transformedSlice$con4;
        if (pastingIntoExtendedPanel && isWholeContentSelected) {
          // if the entire panel content is selected, doing a replaceSelection removes the panel as well. Hence we do delete followed by safeInsert
          tr.delete(selection.$from.pos, selection.$to.pos);
        }
        // This maintains both the selection (destination) and the slice (paste content).
        safeInsert(transformedSlice.content)(tr).scrollIntoView();
        if (((_transformedSlice$con3 = transformedSlice.content.lastChild) === null || _transformedSlice$con3 === void 0 ? void 0 : (_transformedSlice$con4 = _transformedSlice$con3.type) === null || _transformedSlice$con4 === void 0 ? void 0 : _transformedSlice$con4.name) === 'rule') {
          tr.setSelection(TextSelection.near(tr.doc.resolve(tr.selection.$from.pos + transformedSlice.content.size)));
        } else {
          // safeInsert doesn't set correct cursor position
          // it moves the cursor to beginning of the node
          // we manually shift the cursor to end of the node
          const nextPos = tr.doc.resolve(tr.selection.$from.end());
          tr.setSelection(new TextSelection(nextPos));
        }
      }
    }
    queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
    if (dispatch) {
      dispatch(tr);
    }
    return true;
  };
}
export function handlePasteNonNestableBlockNodesIntoList(slice) {
  return (state, dispatch) => {
    var _tr$doc$nodeAt, _slice$content$firstC3, _sliceContent$firstCh, _findParentNodeOfType;
    const {
      tr
    } = state;
    const {
      selection
    } = tr;
    const {
      $from,
      $to,
      from,
      to
    } = selection;
    const {
      orderedList,
      bulletList,
      listItem
    } = state.schema.nodes;

    // Selected nodes
    const selectionParentListItemNode = findParentNodeOfType(listItem)(selection);
    const selectionParentListNodeWithPos = findParentNodeOfType([bulletList, orderedList])(selection);
    const selectionParentListNode = selectionParentListNodeWithPos === null || selectionParentListNodeWithPos === void 0 ? void 0 : selectionParentListNodeWithPos.node;

    // Slice info
    const sliceContent = slice.content;
    const sliceIsListItems = isListNode(sliceContent.firstChild) && isListNode(sliceContent.lastChild);

    // Find case of slices that can be inserted into a list item
    // (eg. paragraphs, list items, code blocks, media single)
    // These scenarios already get handled elsewhere and don't need to split the list
    let sliceContainsBlockNodesOtherThanThoseAllowedInListItem = false;
    slice.content.forEach(child => {
      var _listItem$spec$conten;
      if (!listItem || child.isBlock && !((_listItem$spec$conten = listItem.spec.content) !== null && _listItem$spec$conten !== void 0 && _listItem$spec$conten.includes(child.type.name))) {
        sliceContainsBlockNodesOtherThanThoseAllowedInListItem = true;
      }
    });
    if (!selectionParentListItemNode || !sliceContent || canInsert($from, sliceContent) ||
    // eg. inline nodes that can be inserted in a list item
    !sliceContainsBlockNodesOtherThanThoseAllowedInListItem || sliceIsListItems || !selectionParentListNodeWithPos) {
      return false;
    }

    // Offsets
    const listWrappingOffset = $to.depth - selectionParentListNodeWithPos.depth + 1; // difference in depth between to position and list node
    const listItemWrappingOffset = $to.depth - selectionParentListNodeWithPos.depth; // difference in depth between to position and list item node

    // Anything to do with nested lists should safeInsert and not be handled here
    if (checkIfSelectionInNestedList(state)) {
      return false;
    }

    // Node after the insert position
    const nodeAfterInsertPositionIsListItem = ((_tr$doc$nodeAt = tr.doc.nodeAt(to + listItemWrappingOffset)) === null || _tr$doc$nodeAt === void 0 ? void 0 : _tr$doc$nodeAt.type.name) === 'listItem';

    // Get the next list items position (used later to find the split out ordered list)
    const indexOfNextListItem = $to.indexAfter($to.depth - listItemWrappingOffset);
    const positionOfNextListItem = tr.doc.resolve(selectionParentListNodeWithPos.pos + 1).posAtIndex(indexOfNextListItem);

    // These nodes paste as plain text by default so need to be handled differently
    const sliceContainsNodeThatPastesAsPlainText = sliceContent.firstChild && ['taskItem', 'taskList', 'heading', 'blockquote'].includes(sliceContent.firstChild.type.name);

    // Work out position to replace up to
    let replaceTo;
    if (sliceContainsNodeThatPastesAsPlainText && nodeAfterInsertPositionIsListItem) {
      replaceTo = to + listItemWrappingOffset;
    } else if (sliceContainsNodeThatPastesAsPlainText || !nodeAfterInsertPositionIsListItem) {
      replaceTo = to;
    } else {
      replaceTo = to + listWrappingOffset;
    }

    // handle the insertion of the slice
    if (((_slice$content$firstC3 = slice.content.firstChild) === null || _slice$content$firstC3 === void 0 ? void 0 : _slice$content$firstC3.type.name) === 'blockquote' && contains(slice.content.firstChild, state.schema.nodes.listItem)) {
      insertSliceInsideBlockquote({
        tr,
        slice
      });
    } else if (sliceContainsNodeThatPastesAsPlainText || nodeAfterInsertPositionIsListItem || sliceContent.childCount > 1 && ((_sliceContent$firstCh = sliceContent.firstChild) === null || _sliceContent$firstCh === void 0 ? void 0 : _sliceContent$firstCh.type.name) !== 'paragraph') {
      tr.replaceWith(from, replaceTo, sliceContent).scrollIntoView();
    } else {
      // When the selection is not at the end of a list item
      // eg. middle of list item, start of list item
      tr.replaceSelection(slice).scrollIntoView();
    }

    // Find the ordered list node after the pasted content so we can set it's order
    const mappedPositionOfNextListItem = tr.mapping.map(positionOfNextListItem);
    if (mappedPositionOfNextListItem > tr.doc.nodeSize) {
      return false;
    }
    const nodeAfterPastedContentResolvedPos = findParentNodeOfTypeClosestToPos(tr.doc.resolve(mappedPositionOfNextListItem), [orderedList]);

    // Work out the new split out lists 'order' (the number it starts from)
    const originalParentOrderedListNodeOrder = selectionParentListNode === null || selectionParentListNode === void 0 ? void 0 : selectionParentListNode.attrs.order;
    const numOfListItemsInOriginalList = (_findParentNodeOfType = findParentNodeOfTypeClosestToPos(tr.doc.resolve(from - 1), [orderedList])) === null || _findParentNodeOfType === void 0 ? void 0 : _findParentNodeOfType.node.childCount;

    // Set the new split out lists order attribute
    if (typeof originalParentOrderedListNodeOrder === 'number' && numOfListItemsInOriginalList && nodeAfterPastedContentResolvedPos) {
      tr.setNodeMarkup(nodeAfterPastedContentResolvedPos.pos, orderedList, {
        ...nodeAfterPastedContentResolvedPos.node.attrs,
        order: originalParentOrderedListNodeOrder + numOfListItemsInOriginalList
      });
    }

    // dispatch transaction
    if (tr.docChanged) {
      if (dispatch) {
        dispatch(tr);
      }
      return true;
    }
    return false;
  };
}
export const doesSelectionWhichStartsOrEndsInListContainEntireList = (selection, findRootParentListNode) => {
  const {
    $from,
    $to,
    from,
    to
  } = selection;
  const selectionParentListItemNodeResolvedPos = findRootParentListNode ? findRootParentListNode($from) || findRootParentListNode($to) : null;
  const selectionParentListNode = selectionParentListItemNodeResolvedPos === null || selectionParentListItemNodeResolvedPos === void 0 ? void 0 : selectionParentListItemNodeResolvedPos.parent;
  if (!selectionParentListItemNodeResolvedPos || !selectionParentListNode) {
    return false;
  }
  const startOfEntireList = $from.pos < $to.pos ? selectionParentListItemNodeResolvedPos.pos + $from.depth - 1 : selectionParentListItemNodeResolvedPos.pos + $to.depth - 1;
  const endOfEntireList = $from.pos < $to.pos ? selectionParentListItemNodeResolvedPos.pos + selectionParentListNode.nodeSize - $to.depth - 1 : selectionParentListItemNodeResolvedPos.pos + selectionParentListNode.nodeSize - $from.depth - 1;
  if (!startOfEntireList || !endOfEntireList) {
    return false;
  }
  if (from < to) {
    return startOfEntireList >= $from.pos && endOfEntireList <= $to.pos;
  } else if (from > to) {
    return startOfEntireList >= $to.pos && endOfEntireList <= $from.pos;
  } else {
    return false;
  }
};
export function handlePastePanelOrDecisionContentIntoList(slice, findRootParentListNode) {
  return (state, dispatch) => {
    var _blockNode$type$name;
    const {
      schema,
      tr
    } = state;
    const {
      selection
    } = tr;
    // Check this pasting action is related to copy content from panel node into a selected the list node
    const blockNode = slice.content.firstChild;
    const isSliceWholeNode = slice.openStart === 0 && slice.openEnd === 0;
    const selectionParentListItemNode = selection.$to.node(selection.$to.depth - 1);
    const sliceIsWholeNodeButShouldNotReplaceSelection = isSliceWholeNode && !doesSelectionWhichStartsOrEndsInListContainEntireList(selection, findRootParentListNode);

    // When the experiment is on, treat panel_c1 (table-in-panel variant) like a panel.
    const blockNodeIsPanelOrDecision = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? ['panel', 'decisionList'].includes(blockNode ? getBaseNodeTypeName(blockNode.type) : '') : ['panel', 'decisionList'].includes((_blockNode$type$name = blockNode === null || blockNode === void 0 ? void 0 : blockNode.type.name) !== null && _blockNode$type$name !== void 0 ? _blockNode$type$name : '');
    if (!selectionParentListItemNode || (selectionParentListItemNode === null || selectionParentListItemNode === void 0 ? void 0 : selectionParentListItemNode.type) !== schema.nodes.listItem || !blockNode || !blockNodeIsPanelOrDecision || slice.content.childCount > 1 || (blockNode === null || blockNode === void 0 ? void 0 : blockNode.content.firstChild) === undefined || sliceIsWholeNodeButShouldNotReplaceSelection) {
      return false;
    }

    // Paste the panel node contents extracted instead of pasting the entire panel node
    tr.replaceSelection(slice).scrollIntoView();
    if (dispatch) {
      dispatch(tr);
    }
    return true;
  };
}
const innerTextRangeOfTextblock = (doc, posOfBlock) => {
  const block = doc.nodeAt(posOfBlock);
  if (!block || !block.isTextblock) {
    return null;
  }

  // raw content bounds
  const contentStart = posOfBlock + 1; // +1 to move from node's start token to content start
  const contentEnd = contentStart + block.content.size;

  // clamp to doc coord space
  const start = Math.max(0, Math.min(contentStart, doc.content.size));
  const end = Math.max(0, Math.min(contentEnd, doc.content.size));
  if (end <= start) {
    return null;
  }

  // snap to nearest valid text positions
  const startSel = TextSelection.findFrom(doc.resolve(start), 1, true);
  const endSel = TextSelection.findFrom(doc.resolve(end), -1, true);
  if (!startSel || !endSel) {
    return null;
  }
  const from = startSel.$from.pos;
  const to = endSel.$to.pos;
  return to > from ? {
    from,
    to
  } : null;
};
function resolveSingleTextblockRangeIfAllSelected(state) {
  const sel = state.selection;
  if (!(sel instanceof AllSelection)) {
    return null;
  }
  let count = 0;
  let posOfBlock = -1;
  state.doc.nodesBetween(sel.from, sel.to, (node, pos) => {
    if (!node.isTextblock) {
      return true;
    }
    count++;
    if (count > 1) {
      return false;
    }
    posOfBlock = pos;
    return true;
  });
  if (count !== 1) {
    return null;
  }
  return innerTextRangeOfTextblock(state.doc, posOfBlock);
}

// If we paste a link onto some selected text, apply the link as a mark
export function handlePasteLinkOnSelectedText(slice) {
  return (state, dispatch) => {
    var _selectAllRange$from, _selectAllRange$to;
    const {
      schema,
      selection,
      selection: {
        from,
        to
      },
      tr
    } = state;
    let linkMark;

    // check if we have a link on the clipboard
    if (slice.content.childCount === 1 && isParagraph(slice.content.child(0), schema)) {
      const paragraph = slice.content.child(0);
      if (paragraph.content.childCount === 1 && isText(paragraph.content.child(0), schema)) {
        const text = paragraph.content.child(0);

        // If pasteType is plain text, then
        //  @atlaskit/editor-markdown-transformer in getMarkdownSlice decode
        //  url before setting text property of text node.
        //  However href of marks will be without decoding.
        //  So, if there is character (e.g space) in url eligible escaping then
        //  mark.attrs.href will not be equal to text.text.
        //  That's why decoding mark.attrs.href before comparing.
        // However, if pasteType is richText, that means url in text.text
        //  and href in marks, both won't be decoded.
        linkMark = text.marks.find(mark => isLinkMark(mark, schema) && (mark.attrs.href === text.text || decodeURI(mark.attrs.href) === text.text));
      }
    }

    // derive a linkable range if possible for Select‑All over a single textblock
    const selectAllRange = fg('platform_editor_link_paste_select_all') ? resolveSingleTextblockRangeIfAllSelected(state) : null;
    const rangeFrom = (_selectAllRange$from = selectAllRange === null || selectAllRange === void 0 ? void 0 : selectAllRange.from) !== null && _selectAllRange$from !== void 0 ? _selectAllRange$from : from;
    const rangeTo = (_selectAllRange$to = selectAllRange === null || selectAllRange === void 0 ? void 0 : selectAllRange.to) !== null && _selectAllRange$to !== void 0 ? _selectAllRange$to : to;

    // if we have a link, apply it to the selected text if we have any and it's allowed
    if (linkMark && (selection instanceof TextSelection || Boolean(selectAllRange)) && !selection.empty && canLinkBeCreatedInRange(rangeFrom, rangeTo)(state)) {
      tr.addMark(rangeFrom, rangeTo, linkMark);
      if (dispatch) {
        dispatch(tr);
      }
      return true;
    }
    return false;
  };
}
export function handlePasteAsPlainText(slice, _event, editorAnalyticsAPI) {
  return (state, dispatch, view) => {
    var _input;
    if (!view) {
      return false;
    }

    // prosemirror-bump-fix
    // Yes, this is wrong by default. But, we need to keep the private PAI usage to unblock the prosemirror bump
    // So, this code will make sure we are checking for both version (current and the newest prosemirror-view version
    const isShiftKeyPressed =
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    view.shiftKey || ((_input = view.input) === null || _input === void 0 ? void 0 : _input.shiftKey);
    // In case of SHIFT+CMD+V ("Paste and Match Style") we don't want to run the usual
    // fuzzy matching of content. ProseMirror already handles this scenario and will
    // provide us with slice containing paragraphs with plain text, which we decorate
    // with "stored marks".
    // @see prosemirror-view/src/clipboard.js:parseFromClipboard()).
    // @see prosemirror-view/src/input.js:doPaste().
    if (isShiftKeyPressed) {
      let tr = closeHistory(state.tr);
      const {
        selection
      } = tr;

      // <- using the same internal flag that prosemirror-view is using

      // if user has selected table we need custom logic to replace the table
      tr = replaceSelectedTable(state, slice);

      // add analytics after replacing selected table
      tr = addReplaceSelectedTableAnalytics(state, tr, editorAnalyticsAPI);

      // otherwise just replace the selection
      if (!tr.docChanged) {
        tr.replaceSelection(slice);
      }
      (state.storedMarks || []).forEach(mark => {
        tr.addMark(selection.from, selection.from + slice.size, mark);
      });
      tr.scrollIntoView();
      if (dispatch) {
        dispatch(tr);
      }
      return true;
    }
    return false;
  };
}
export function handlePastePreservingMarks(slice, queueCardsFromChangedTr) {
  return (state, dispatch) => {
    const {
      schema,
      tr: {
        selection
      }
    } = state;
    const {
      marks: {
        code: codeMark,
        annotation: annotationMark
      },
      nodes: {
        bulletList,
        emoji,
        hardBreak,
        heading,
        listItem,
        mention,
        orderedList,
        text
      }
    } = schema;
    if (!(selection instanceof TextSelection)) {
      return false;
    }
    const selectionMarks = selection.$head.marks();
    if (selectionMarks.length === 0) {
      return false;
    }

    // special case for codeMark: will preserve mark only if codeMark is currently active
    // won't preserve mark if cursor is on the edge on the mark (namely inactive)
    const hasActiveCodeMark = codeMark && codeMark.isInSet(selectionMarks) && anyMarkActive(state, codeMark);
    const hasAnnotationMark = annotationMark && annotationMark.isInSet(selectionMarks);
    const selectionIsHeading = hasParentNodeOfType([heading])(state.selection);

    // if the pasted data is one of the node types below
    // we apply current selection marks to the pasted slice
    if (hasOnlyNodesOfType(bulletList, hardBreak, heading, listItem, text, emoji, mention, orderedList)(slice) || selectionIsHeading || hasActiveCodeMark || hasAnnotationMark) {
      const transformedSlice = applyTextMarksToSlice(schema, selectionMarks)(slice);
      const tr = closeHistory(state.tr).replaceSelection(transformedSlice).setStoredMarks(selectionMarks).scrollIntoView();
      queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
      if (dispatch) {
        dispatch(tr);
      }
      return true;
    }
    return false;
  };
}
async function getSmartLinkAdf(text, type, cardOptions) {
  if (!cardOptions.provider) {
    throw Error('No card provider found');
  }
  const provider = await cardOptions.provider;
  return await provider.resolve(text, type);
}
function insertAutoMacro(slice, macro, view, from, to) {
  if (view) {
    // insert the text or linkified/md-converted clipboard data
    const selection = view.state.tr.selection;
    let tr;
    let before;
    if (typeof from === 'number' && typeof to === 'number') {
      tr = view.state.tr.replaceRange(from, to, slice);
      before = tr.mapping.map(from, -1);
    } else {
      tr = view.state.tr.replaceSelection(slice);
      before = tr.mapping.map(selection.from, -1);
    }
    view.dispatch(tr);

    // replace the text with the macro as a separate transaction
    // so the autoconversion generates 2 undo steps
    const macroTr = closeHistory(view.state.tr).replaceRangeWith(before, before + slice.size, macro).scrollIntoView();
    addLinkMetadata(view.state.selection, macroTr, {
      inputMethod: INPUT_METHOD.CLIPBOARD,
      cardAction: 'AUTO_CONVERT'
    });
    view.dispatch(macroTr);
    return true;
  }
  return false;
}
export function handleMacroAutoConvert(text, slice, queueCardsFromChangedTr, runMacroAutoConvert, cardsOptions, extensionAutoConverter) {
  return (state, dispatch, view) => {
    let macro = null;

    // try to use auto convert from extension provider first
    if (extensionAutoConverter) {
      const extension = extensionAutoConverter(text);
      if (extension) {
        macro = PMNode.fromJSON(state.schema, extension);
      }
    }

    // then try from macro provider (which will be removed some time in the future)
    if (!macro) {
      var _runMacroAutoConvert;
      macro = (_runMacroAutoConvert = runMacroAutoConvert === null || runMacroAutoConvert === void 0 ? void 0 : runMacroAutoConvert(state, text)) !== null && _runMacroAutoConvert !== void 0 ? _runMacroAutoConvert : null;
    }
    if (macro) {
      /**
       * if FF enabled, run through smart links and check for result
       */
      if (cardsOptions && cardsOptions.resolveBeforeMacros && cardsOptions.resolveBeforeMacros.length) {
        if (cardsOptions.resolveBeforeMacros.indexOf(macro.attrs.extensionKey) < 0) {
          return insertAutoMacro(slice, macro, view);
        }
        if (!view) {
          throw new Error('View is missing');
        }

        // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
        const trackingId = uuid();
        const trackingFrom = `handleMacroAutoConvert-from-${trackingId}`;
        const trackingTo = `handleMacroAutoConvert-to-${trackingId}`;
        startTrackingPastedMacroPositions({
          [trackingFrom]: state.selection.from,
          [trackingTo]: state.selection.to
        })(state, dispatch);
        getSmartLinkAdf(text, 'inline', cardsOptions).then(() => {
          // we use view.state rather than state because state becomes a stale
          // state reference after getSmartLinkAdf's async work
          const {
            pastedMacroPositions
          } = getPastePluginState(view.state);
          if (dispatch) {
            handleMarkdown(slice, queueCardsFromChangedTr, pastedMacroPositions[trackingFrom], pastedMacroPositions[trackingTo])(view.state, dispatch);
          }
        }).catch(() => {
          const {
            pastedMacroPositions
          } = getPastePluginState(view.state);
          insertAutoMacro(slice, macro, view, pastedMacroPositions[trackingFrom], pastedMacroPositions[trackingTo]);
        }).finally(() => {
          stopTrackingPastedMacroPositions([trackingFrom, trackingTo])(view.state, dispatch);
        });
        return true;
      }
      return insertAutoMacro(slice, macro, view);
    }
    return !!macro;
  };
}
export function handleCodeBlock(text) {
  return (state, dispatch) => {
    const {
      codeBlock
    } = state.schema.nodes;
    if (text && hasParentNodeOfType(codeBlock)(state.selection)) {
      const tr = closeHistory(state.tr);
      tr.scrollIntoView();
      if (dispatch) {
        dispatch(tr.insertText(text));
      }
      return true;
    }
    return false;
  };
}
function isOnlyMedia(state, slice) {
  const {
    media
  } = state.schema.nodes;
  // Ignored via go/ees005
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  return slice.content.childCount === 1 && slice.content.firstChild.type === media;
}
function isOnlyMediaSingle(state, slice) {
  const {
    mediaSingle
  } = state.schema.nodes;
  return (
    // Ignored via go/ees005
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    mediaSingle && slice.content.childCount === 1 && slice.content.firstChild.type === mediaSingle
  );
}
export function handleMediaSingle(inputMethod, insertMediaAsMediaSingle) {
  return function (slice) {
    return (state, dispatch, view) => {
      if (view) {
        if (isOnlyMedia(state, slice)) {
          var _insertMediaAsMediaSi;
          // Ignored via go/ees005
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          return (_insertMediaAsMediaSi = insertMediaAsMediaSingle === null || insertMediaAsMediaSingle === void 0 ? void 0 : insertMediaAsMediaSingle(view, slice.content.firstChild, inputMethod)) !== null && _insertMediaAsMediaSi !== void 0 ? _insertMediaAsMediaSi : false;
        }
        if (insideTable(state) && isOnlyMediaSingle(state, slice)) {
          const tr = state.tr.replaceSelection(slice);
          const nextPos = tr.doc.resolve(tr.mapping.map(state.selection.$from.pos));
          if (dispatch) {
            dispatch(tr.setSelection(new GapCursorSelection(nextPos, Side.RIGHT)));
          }
          return true;
        }
      }
      return false;
    };
  };
}
const hasTopLevelExpand = slice => {
  let hasExpand = false;
  slice.content.forEach(node => {
    if (node.type.name === 'expand' || node.type.name === 'nestedExpand') {
      hasExpand = true;
    }
  });
  return hasExpand;
};
export function handleTableContentPasteInBodiedExtension(slice) {
  return (state, dispatch) => {
    const isInsideBodyExtension = hasParentNodeOfType(state.schema.nodes.bodiedExtension)(state.selection);
    if (!insideTable(state) || !isInsideBodyExtension) {
      return false;
    }
    const {
      bodiedExtension
    } = state.schema.nodes;
    const newSlice = mapSlice(slice, maybeNode => {
      if (maybeNode.type === bodiedExtension) {
        return bodiedExtension.createChecked(maybeNode.attrs, maybeNode.content, maybeNode.marks);
      }
      return maybeNode;
    });
    if (dispatch) {
      dispatch(state.tr.replaceSelection(newSlice));
      return true;
    }
    return false;
  };
}
export function handleNestedTablePaste(slice, isNestingTablesSupported) {
  return (state, dispatch) => {
    if (!isNestingTablesSupported || !insideTable(state)) {
      return false;
    }
    const {
      schema,
      selection
    } = state;
    let sliceHasTable = false;
    slice.content.forEach(node => {
      if (node.type === state.schema.nodes.table) {
        sliceHasTable = true;
      }
    });
    if (sliceHasTable) {
      // if slice has table - if pasting to deeply nested location place paste after top table
      if (getParentOfTypeCount(schema.nodes.table)(selection.$from) > 1) {
        const positionAfterTopTable = getPositionAfterTopParentNodeOfType(schema.nodes.table)(selection.$from);
        let {
          tr
        } = state;
        tr = safeInsert(slice.content, positionAfterTopTable)(tr);
        tr.scrollIntoView();
        if (dispatch) {
          dispatch(tr);
          return true;
        }
      }
    }
    return false;
  };
}
export function handleExpandPaste(slice) {
  return (state, dispatch) => {
    const isInsideNestableExpand = !!insideExpand(state);

    // Do not handle expand if it's not being pasted into a table or expand
    // OR if it's nested within another node when being pasted into a table/expand
    if (!insideTable(state) && !isInsideNestableExpand || !hasTopLevelExpand(slice)) {
      return false;
    }
    const {
      expand,
      nestedExpand
    } = state.schema.nodes;
    let {
      tr
    } = state;
    let hasExpand = false;
    const newSlice = mapSlice(slice, maybeNode => {
      if (maybeNode.type === expand || maybeNode.type === nestedExpand) {
        hasExpand = true;
        try {
          return nestedExpand.createChecked(maybeNode.attrs, maybeNode.content, maybeNode.marks);
        } catch {
          tr = safeInsert(maybeNode, tr.selection.$to.pos)(tr);
          return Fragment.empty;
        }
      }
      return maybeNode;
    });
    if (hasExpand && dispatch) {
      // If the slice is a subset, we can let PM replace the selection
      // it will insert as text where it can't place the node.
      // Otherwise we use safeInsert to insert below instead of
      // replacing/splitting the current node.
      if (slice.openStart > 1 && slice.openEnd > 1) {
        dispatch(tr.replaceSelection(newSlice));
      } else {
        dispatch(safeInsert(newSlice.content)(tr));
      }
      return true;
    }
    return false;
  };
}
export function handleMarkdown(markdownSlice, queueCardsFromChangedTr, from, to) {
  return (state, dispatch) => {
    const tr = closeHistory(state.tr);
    const pastesFrom = typeof from === 'number' ? from : tr.selection.from;
    if (typeof from === 'number' && typeof to === 'number') {
      tr.replaceRange(from, to, markdownSlice);
    } else {
      tr.replaceSelection(markdownSlice);
    }
    const textPosition = tr.doc.resolve(Math.min(pastesFrom + markdownSlice.size, tr.doc.content.size));
    tr.setSelection(TextSelection.near(textPosition, -1));
    queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
    if (dispatch) {
      dispatch(tr.scrollIntoView());
    }
    return true;
  };
}
function removePrecedingBackTick(tr) {
  const {
    $from: {
      nodeBefore
    },
    from
  } = tr.selection;
  // Ignored via go/ees005
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  if (nodeBefore && nodeBefore.isText && nodeBefore.text.endsWith('`')) {
    tr.delete(from - 1, from);
  }
}
function hasInlineCode(state, slice) {
  return slice.content.firstChild && slice.content.firstChild.marks.some(m => m.type === state.schema.marks.code);
}
function rollupLeafListItems(list, leafListItems) {
  list.content.forEach(child => {
    if (isListNode(child) || isListItemNode(child) && isListNode(child.firstChild)) {
      rollupLeafListItems(child, leafListItems);
    } else {
      leafListItems.push(child);
    }
  });
}
function shouldFlattenList(state, slice) {
  const node = slice.content.firstChild;
  return node && insideTable(state) && isListNode(node) && slice.openStart > slice.openEnd;
}
function sliceHasTopLevelMarks(slice) {
  let hasTopLevelMarks = false;
  slice.content.descendants(node => {
    if (node.marks.length > 0) {
      hasTopLevelMarks = true;
    }
    return false;
  });
  return hasTopLevelMarks;
}
function getTopLevelMarkTypesInSlice(slice) {
  const markTypes = new Set();
  slice.content.descendants(node => {
    node.marks.map(mark => mark.type).forEach(markType => markTypes.add(markType));
    return false;
  });
  return markTypes;
}

/**
 * Peels container wrapper nodes (e.g. panel, expand) added by ProseMirror's addContext()
 * so that fontSize-marked paragraphs become top-level, preserving the mark on paste.
 */
function unwrapContainerNodesWithBlockMarks(slice, schema, fontSize) {
  let content = slice.content;
  let levelsUnwrapped = 0;
  while (content.childCount === 1 && content.firstChild && !content.firstChild.isTextblock && slice.openStart - levelsUnwrapped > 1) {
    let hasBlockMarkedParagraph = false;
    for (let i = 0; i < content.firstChild.childCount; i++) {
      const child = content.firstChild.child(i);
      if (child.type === schema.nodes.paragraph && child.marks.some(m => m.type === fontSize)) {
        hasBlockMarkedParagraph = true;
        break;
      }
    }
    if (!hasBlockMarkedParagraph) {
      break;
    }
    content = content.firstChild.content;
    levelsUnwrapped++;
  }
  if (levelsUnwrapped === 0) {
    return slice;
  }
  return new Slice(content, slice.openStart - levelsUnwrapped, Math.max(0, slice.openEnd - levelsUnwrapped));
}

/**
 * Returns the fontSize attrs to apply at the paste destination, or false if none.
 * Checks list and task destinations in priority order.
 */
function getDestinationFontSizeAttrs(destinationListNode, isInSmallTaskContext, $from, currentNode, fontSize) {
  if (destinationListNode) {
    return getFirstParagraphBlockMarkAttrs(destinationListNode, fontSize);
  }
  if (isInSmallTaskContext) {
    return getBlockMarkAttrs($from.parent, fontSize) || getFirstParagraphBlockMarkAttrs(currentNode, fontSize);
  }
  return false;
}

/**
 * Resolves which marks to apply to a paragraph node after filtering forbidden marks.
 * When a destination block mark is provided, replaces any existing fontSize mark with it.
 * When normalizing for the target context, removes the fontSize mark entirely.
 * Otherwise returns the filtered marks unchanged.
 */
function resolveParagraphMarks(marks, destinationBlockMarkAttrs, shouldNormalize, fontSize) {
  if (destinationBlockMarkAttrs) {
    return marks.filter(m => m.type !== fontSize).concat(fontSize.create(destinationBlockMarkAttrs));
  }
  if (shouldNormalize) {
    return marks.filter(m => m.type !== fontSize);
  }
  return marks;
}
export function handleParagraphBlockMarks(state, slice) {
  var _findParentNodeOfType2;
  if (slice.content.size === 0) {
    return slice;
  }
  const {
    schema,
    selection,
    selection: {
      $from
    }
  } = state;
  const {
    bulletList,
    orderedList,
    blockTaskItem,
    taskItem,
    paragraph,
    heading
  } = schema.nodes;
  const {
    fontSize
  } = schema.marks;
  const isSmallFontSizeEnabled = !!fontSize && expValEquals('platform_editor_small_font_size', 'isEnabled', true);

  // When copying from inside a container (e.g. panel, expand), ProseMirror wraps the
  // content back in the container via addContext(), increasing openStart/openEnd. Unwrap
  // so the paragraph (with its fontSize mark) becomes top-level.
  if (isSmallFontSizeEnabled) {
    slice = unwrapContainerNodesWithBlockMarks(slice, schema, fontSize);
  }
  const destinationListNode = (_findParentNodeOfType2 = findParentNodeOfType([bulletList, orderedList])(selection)) === null || _findParentNodeOfType2 === void 0 ? void 0 : _findParentNodeOfType2.node;
  const currentNode = typeof $from.node === 'function' ? $from.node() : undefined;
  const isInNormalTaskContext = (currentNode === null || currentNode === void 0 ? void 0 : currentNode.type) === taskItem || $from.parent.type === taskItem;
  const isInSmallTaskContext = !!blockTaskItem && ((currentNode === null || currentNode === void 0 ? void 0 : currentNode.type) === blockTaskItem || $from.parent.type === blockTaskItem || $from.parent.type === paragraph && $from.depth > 0 && $from.node($from.depth - 1).type === blockTaskItem);
  const destinationBlockMarkAttrs = isSmallFontSizeEnabled ? getDestinationFontSizeAttrs(destinationListNode, isInSmallTaskContext, $from, currentNode, fontSize) : false;
  const isInHeadingContext = $from.parent.type === heading;

  // If no paragraph in the slice contains marks, there's no need for special handling
  // unless we're pasting into a small-text list and need to add the destination block mark.
  // Note: this doesn't check for marks applied to lower level nodes such as text
  if (!sliceHasTopLevelMarks(slice) && !destinationBlockMarkAttrs) {
    return slice;
  }
  const shouldNormalizeFontSizeForTarget = isSmallFontSizeEnabled && (!!destinationListNode || isInNormalTaskContext || isInSmallTaskContext || isInHeadingContext);

  // If pasting a single paragraph into pre-existing content, match destination formatting.
  // For bullet/ordered lists under small-text, we still need to normalize the paragraph block mark
  // so pasted content adopts the destination list state.
  const destinationHasContent = $from.parent.textContent.length > 0;
  if (slice.content.childCount === 1 && destinationHasContent && !shouldNormalizeFontSizeForTarget) {
    return slice;
  }

  // Check the parent of (paragraph -> text) because block marks are assigned to a wrapper
  // element around the paragraph node
  const grandparent = $from.node(Math.max(0, $from.depth - 1));
  const markTypesInSlice = getTopLevelMarkTypesInSlice(slice);
  const forbiddenMarkTypes = [];
  for (const markType of markTypesInSlice) {
    if (!grandparent.type.allowsMarkType(markType)) {
      forbiddenMarkTypes.push(markType);
    }
  }
  const normalizedContent = mapSlice(slice, node => {
    if (node.type === paragraph) {
      const paragraphMarks = node.marks.filter(mark => !forbiddenMarkTypes.includes(mark.type));
      return paragraph.createChecked(undefined, node.content, resolveParagraphMarks(paragraphMarks, destinationBlockMarkAttrs, shouldNormalizeFontSizeForTarget, fontSize));
    } else if (node.type === heading) {
      // Preserve heading attributes to keep formatting
      return heading.createChecked(node.attrs, node.content, node.marks.filter(mark => !forbiddenMarkTypes.includes(mark.type)));
    }
    return node;
  });
  if (forbiddenMarkTypes.length === 0 && !shouldNormalizeFontSizeForTarget) {
    // In a slice containing one or more paragraphs at the document level (not wrapped in
    // another node), the first paragraph will only have its text content captured and pasted
    // since openStart is 1. We decrement the open depth of the slice so it retains any block
    // marks applied to it. We only care about the depth at the start of the selection so
    // there's no need to change openEnd - the rest of the slice gets pasted correctly.
    const openStart = Math.max(0, slice.openStart - 1);
    return new Slice(slice.content, openStart, slice.openEnd);
  }
  if (forbiddenMarkTypes.length === 0 && shouldNormalizeFontSizeForTarget) {
    // When pasting into a heading, keep the original openStart so ProseMirror merges inline
    // content into the heading node rather than replacing it with a paragraph.
    const openStart = isInHeadingContext ? slice.openStart : Math.max(0, slice.openStart - 1);
    return new Slice(normalizedContent.content, openStart, slice.openEnd);
  }

  // If the paragraph or heading contains marks forbidden by the parent node
  // (e.g. alignment/indentation), drop those marks from the slice. For lists under the small
  // text experiment, also normalize fontSize to the destination list state.
  return new Slice(normalizedContent.content, slice.openStart, slice.openEnd);
}

/**
 * ED-6300: When a nested list is pasted in a table cell and the slice has openStart > openEnd,
 * it splits the table. As a workaround, we flatten the list to even openStart and openEnd.
 *
 * Note: this only happens if the first child is a list
 *
 * Example: copying "one" and "two"
 * - zero
 *   - one
 * - two
 *
 * Before:
 * ul
 *   ┗━ li
 *     ┗━ ul
 *       ┗━ li
 *         ┗━ p -> "one"
 *   ┗━ li
 *     ┗━ p -> "two"
 *
 * After:
 * ul
 *   ┗━ li
 *     ┗━ p -> "one"
 *   ┗━ li
 *     ┗━p -> "two"
 */
export function flattenNestedListInSlice(slice) {
  if (!slice.content.firstChild) {
    return slice;
  }
  const listToFlatten = slice.content.firstChild;
  const leafListItems = [];
  rollupLeafListItems(listToFlatten, leafListItems);
  const contentWithFlattenedList = slice.content.replaceChild(0, listToFlatten.type.createChecked(listToFlatten.attrs, leafListItems));
  return new Slice(contentWithFlattenedList, slice.openEnd, slice.openEnd);
}
const doesSliceContainBlockquoteListNodes = (slice, listContainerNodeTypes) => {
  var _firstChildOfSlice$ty, _lastChildOfSlice$typ;
  const firstChildOfSlice = slice.content.firstChild;
  const lastChildOfSlice = slice.content.lastChild;
  const isFirstChildBlockquoteListNode = (firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$ty = firstChildOfSlice.type) === null || _firstChildOfSlice$ty === void 0 ? void 0 : _firstChildOfSlice$ty.name) === 'blockquote' && listContainerNodeTypes.some(nodeType => {
    var _firstChildOfSlice$co;
    return nodeType === (firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$co = firstChildOfSlice.content.firstChild) === null || _firstChildOfSlice$co === void 0 ? void 0 : _firstChildOfSlice$co.type);
  });
  const isLastChildBlockquoteListNode = (lastChildOfSlice === null || lastChildOfSlice === void 0 ? void 0 : (_lastChildOfSlice$typ = lastChildOfSlice.type) === null || _lastChildOfSlice$typ === void 0 ? void 0 : _lastChildOfSlice$typ.name) === 'blockquote' && listContainerNodeTypes.some(nodeType => {
    var _lastChildOfSlice$con;
    return nodeType === (lastChildOfSlice === null || lastChildOfSlice === void 0 ? void 0 : (_lastChildOfSlice$con = lastChildOfSlice.content.firstChild) === null || _lastChildOfSlice$con === void 0 ? void 0 : _lastChildOfSlice$con.type);
  });
  return isFirstChildBlockquoteListNode || isLastChildBlockquoteListNode;
};
export function handleRichText(slice, queueCardsFromChangedTr) {
  return (state, dispatch) => {
    var _slice$content, _slice$content2, _findParentNodeOfType3, _firstChildOfSlice$ty2, _lastChildOfSlice$typ2, _panelParentOverCurre;
    const {
      codeBlock,
      heading,
      paragraph,
      panel,
      panel_c1,
      bulletList,
      orderedList
    } = state.schema.nodes;
    const {
      fontSize
    } = state.schema.marks;
    const {
      selection,
      schema
    } = state;
    const firstChildOfSlice = (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.firstChild;
    const lastChildOfSlice = (_slice$content2 = slice.content) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.lastChild;
    const listContainerNodeTypes = [bulletList, orderedList];
    const isSmallFontSizeEnabled = !!fontSize && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
    const destinationListNode = isSmallFontSizeEnabled ? (_findParentNodeOfType3 = findParentNodeOfType(listContainerNodeTypes)(selection)) === null || _findParentNodeOfType3 === void 0 ? void 0 : _findParentNodeOfType3.node : undefined;
    const destinationListFontSizeAttrs = isSmallFontSizeEnabled ? getFirstParagraphBlockMarkAttrs(destinationListNode, fontSize) : false;

    // In case user is pasting inline code,
    // any backtick ` immediately preceding it should be removed.
    let tr = state.tr;
    if (hasInlineCode(state, slice)) {
      removePrecedingBackTick(tr);
    }
    if (shouldFlattenList(state, slice)) {
      slice = flattenNestedListInSlice(slice);
    }
    closeHistory(tr);
    const isFirstChildListNode = isListNode(firstChildOfSlice);
    const isLastChildListNode = isListNode(lastChildOfSlice);
    const isSliceContentListNodes = isFirstChildListNode || isLastChildListNode;
    const isFirstChildTaskListNode = (firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$ty2 = firstChildOfSlice.type) === null || _firstChildOfSlice$ty2 === void 0 ? void 0 : _firstChildOfSlice$ty2.name) === 'taskList';
    const isLastChildTaskListNode = (lastChildOfSlice === null || lastChildOfSlice === void 0 ? void 0 : (_lastChildOfSlice$typ2 = lastChildOfSlice.type) === null || _lastChildOfSlice$typ2 === void 0 ? void 0 : _lastChildOfSlice$typ2.name) === 'taskList';
    const isSliceContentTaskListNodes = isFirstChildTaskListNode || isLastChildTaskListNode;
    const sliceContentBlockquoteListNodes = doesSliceContainBlockquoteListNodes(slice, listContainerNodeTypes);

    // Compute once and reuse below to avoid traversing the slice twice.
    const sliceMarkTypes = isSmallFontSizeEnabled ? getTopLevelMarkTypesInSlice(slice) : new Set();
    const destinationIsEmpty = isSmallFontSizeEnabled && selection.$from.parent.type === paragraph && selection.$from.parent.textContent.length === 0;

    // Capture the destination paragraph's non-fontSize block marks (e.g. alignment, indentation)
    // before the paste so they can be restored if the paste replaces the paragraph entirely
    // (which happens when small text is pasted with openStart=0).
    const destinationNonFontSizeBlockMarks = isSmallFontSizeEnabled && selection.$from.parent.type === paragraph && sliceMarkTypes.has(fontSize) ? selection.$from.parent.marks.filter(m => m.type !== fontSize) : [];

    // We want to use safeInsert to insert invalid content, as it inserts at the closest non schema violating position
    // rather than spliting the selection parent node in half (which is what replaceSelection does)
    // Exception is paragraph and heading nodes, these should be split, provided their parent supports the pasted content
    const textNodes = [heading, paragraph];
    const selectionParent = selection.$to.node(selection.$to.depth - 1);
    const noNeedForSafeInsert = selection.$to.node().type.validContent(slice.content) || textNodes.includes(selection.$to.node().type) && selectionParent.type.validContent(slice.content);
    // panel_c1 is a schema variant of panel; when the experiment is on, treat both together.
    const panelParentOverCurrentSelection = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? findParentNodeOfType([panel, panel_c1].filter(Boolean))(tr.selection) : findParentNodeOfType(panel)(tr.selection);
    const isTargetPanelEmpty = panelParentOverCurrentSelection && ((_panelParentOverCurre = panelParentOverCurrentSelection.node) === null || _panelParentOverCurre === void 0 ? void 0 : _panelParentOverCurre.content.size) === 2;
    if (!isSliceContentTaskListNodes && (isSliceContentListNodes || isTargetPanelEmpty)) {
      insertSliceForLists({
        tr,
        slice,
        schema
      });
    } else if (noNeedForSafeInsert && !(expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true) && checkTaskListInList(state, slice))) {
      var _firstChildOfSlice$ty3, _firstChildOfSlice$co2, _firstChildOfSlice$co3;
      if ((firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$ty3 = firstChildOfSlice.type) === null || _firstChildOfSlice$ty3 === void 0 ? void 0 : _firstChildOfSlice$ty3.name) === 'blockquote' && firstChildOfSlice !== null && firstChildOfSlice !== void 0 && (_firstChildOfSlice$co2 = firstChildOfSlice.content.firstChild) !== null && _firstChildOfSlice$co2 !== void 0 && _firstChildOfSlice$co2.type.name && ['bulletList', 'orderedList', 'mediaSingle'].includes(firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$co3 = firstChildOfSlice.content.firstChild) === null || _firstChildOfSlice$co3 === void 0 ? void 0 : _firstChildOfSlice$co3.type.name)) {
        // checks if parent node is a blockquote and child node is either a bulletlist or orderedlist or mediaSingle
        insertSliceInsideBlockquote({
          tr,
          slice
        });
      } else {
        var _slice$content$lastCh, _slice$content$lastCh2;
        tr.replaceSelection(slice);
        // when cursor is inside a table cell, and slice.content.lastChild is a panel, expand, or decisionList
        // need to make sure the cursor position is is right after the panel, expand, or decisionList
        // still in the same table cell, see issue: https://product-fabric.atlassian.net/browse/ED-17862
        const shouldUpdateCursorPosAfterPaste = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? ['panel', 'nestedExpand', 'decisionList', 'codeBlock'].includes(slice.content.lastChild ? getBaseNodeTypeName(slice.content.lastChild.type) : '') : ['panel', 'nestedExpand', 'decisionList', 'codeBlock'].includes(((_slice$content$lastCh = slice.content.lastChild) === null || _slice$content$lastCh === void 0 ? void 0 : (_slice$content$lastCh2 = _slice$content$lastCh.type) === null || _slice$content$lastCh2 === void 0 ? void 0 : _slice$content$lastCh2.name) || '');
        const lastChild = slice.content.lastChild;
        const $nextPos = tr.doc.resolve(tr.mapping.map(selection.from));
        const nextSelection = lastChild !== null && lastChild !== void 0 && lastChild.type.isTextblock ? TextSelection.findFrom($nextPos, -1, true) : new GapCursorSelection($nextPos, Side.RIGHT);
        if (nextSelection) {
          tr.setSelection(nextSelection);
        } else if (insideTableCell(state) && shouldUpdateCursorPosAfterPaste) {
          const nextPos = tr.doc.resolve(tr.mapping.map(selection.$from.pos));
          tr.setSelection(new GapCursorSelection(nextPos, Side.RIGHT));
        }
      }
    } else {
      // need to scan the slice if there's a block node or list items inside it
      let sliceHasList = false;
      slice.content.nodesBetween(0, slice.content.size, node => {
        if (node.type === state.schema.nodes.listItem) {
          sliceHasList = true;
          return false;
        }
      });
      if (insideTableCell(state) && isInListItem(state) && canInsert(selection.$from, slice.content) && canInsert(selection.$to, slice.content) || sliceHasList) {
        tr.replaceSelection(slice);
      } else if (expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true) && !fg('platform_editor_flexible_list_kill_switch_1') && checkTaskListInList(state, slice) && !checkIfSelectionInNestedList(state)) {
        // Corrected task-into-list insertion is gated behind the flexible list indentation
        // experiment and protected by the kill switch.
        tr = insertSliceForTaskInsideList({
          tr,
          slice
        });
      } else if (checkTaskListInList(state, slice) && !checkIfSelectionInNestedList(state)) {
        // Legacy path (experiment OFF or kill switch ON): preserve the previous over-nesting
        // behaviour via the function's internal fallback.
        tr = insertSliceForTaskInsideList({
          tr,
          slice
        });
      } else {
        // need safeInsert rather than replaceSelection, so that nodes aren't split in half
        // e.g. when pasting a layout into a table, replaceSelection splits the table in half and adds the layout in the middle
        tr = safeInsert(slice.content, tr.selection.$to.pos)(tr);
        if (checkTaskListInList(state, slice)) {
          updateSelectionAfterReplace({
            tr
          });
        }
      }
    }

    // font size handling for pasting into lists or blockquotes
    if (isSmallFontSizeEnabled && (isSliceContentListNodes || sliceContentBlockquoteListNodes)) {
      const containingList = findParentNodeOfTypeClosestToPos(tr.selection.$from, listContainerNodeTypes);
      if (containingList) {
        reconcileBlockMarkForContainerAtPos(tr, containingList.pos, fontSize, isSliceContentListNodes ? destinationListFontSizeAttrs : false);
      }
    }

    // font size handling for pasting into paragraphs (normal text) - preserve source style
    if (isSmallFontSizeEnabled && destinationIsEmpty && !sliceMarkTypes.has(fontSize) && !destinationListNode && selection.$from.parent.type === paragraph && getBlockMarkAttrs(selection.$from.parent, fontSize)) {
      reconcileBlockMarkForParagraphAtPos(tr, tr.mapping.map(selection.$from.pos), fontSize, false);
    }

    // Restore destination block marks (e.g. alignment) that were lost when pasting small text
    // replaced the paragraph entirely (openStart=0 from container unwrap).
    if (isSmallFontSizeEnabled && destinationNonFontSizeBlockMarks.length > 0) {
      const pastedFrom = tr.mapping.map(selection.from, -1);
      const pastedTo = tr.mapping.map(selection.to, 1);
      for (const mark of destinationNonFontSizeBlockMarks) {
        reconcileBlockMarkInRange(tr, pastedFrom, pastedTo, mark.type, mark.attrs);
      }
    }
    tr.setStoredMarks([]);
    if (tr.selection.empty && tr.selection.$from.parent.type === codeBlock) {
      tr.setSelection(TextSelection.near(tr.selection.$from, 1));
    }
    tr.scrollIntoView();

    // queue link cards, ignoring any errors
    queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
    if (dispatch) {
      dispatch(tr);
    }
    return true;
  };
}
function isUrlString(text) {
  try {
    new URL(text);
    return true;
  } catch {
    return false;
  }
}
function isLinkOrUrlString(slice, schema) {
  if (slice.content.childCount !== 1 || !isParagraph(slice.content.child(0), schema)) {
    return false;
  }
  const paragraph = slice.content.child(0);
  if (paragraph.content.childCount !== 1 || !isText(paragraph.content.child(0), schema)) {
    return false;
  }
  const {
    marks,
    text = ''
  } = paragraph.content.child(0);
  const hasLinkMark = marks.some(mark => isLinkMark(mark, schema));
  return hasLinkMark || isUrlString(text);
}
export function handlePasteIntoCaption(slice) {
  return (state, dispatch) => {
    if (isLinkOrUrlString(slice, state.schema)) {
      return false;
    }
    const {
      caption
    } = state.schema.nodes;
    const tr = state.tr;
    if (hasParentNodeOfType(caption)(state.selection)) {
      // We let PM replace the selection and it will insert as text where it can't place the node
      // This is totally fine as caption is just a simple block that only contains inline contents
      // And it is more in line with WYSIWYG expectations
      tr.replaceSelection(slice).scrollIntoView();
      if (dispatch) {
        dispatch(tr);
      }
      return true;
    }
    return false;
  };
}
export const handleSelectedTable = editorAnalyticsAPI => slice => (state, dispatch) => {
  let tr = replaceSelectedTable(state, slice);

  // add analytics after replacing selected table
  tr = addReplaceSelectedTableAnalytics(state, tr, editorAnalyticsAPI);
  if (tr.docChanged) {
    if (dispatch) {
      dispatch(tr);
    }
    return true;
  }
  return false;
};
export function checkTaskListInList(state, slice) {
  var _slice$content$firstC4, _slice$content$firstC5;
  return Boolean(isInListItem(state) && ['taskList', 'taskItem'].includes(((_slice$content$firstC4 = slice.content.firstChild) === null || _slice$content$firstC4 === void 0 ? void 0 : (_slice$content$firstC5 = _slice$content$firstC4.type) === null || _slice$content$firstC5 === void 0 ? void 0 : _slice$content$firstC5.name) || ''));
}
export function checkIfSelectionInNestedList(state) {
  const {
    selection,
    tr
  } = state;
  const {
    orderedList,
    bulletList,
    listItem
  } = state.schema.nodes;
  const selectionParentListItemNode = findParentNodeOfType(listItem)(selection);
  const selectionParentListNodeWithPos = findParentNodeOfType([bulletList, orderedList])(selection);
  if (!selectionParentListItemNode || !selectionParentListNodeWithPos) {
    return false;
  }
  const grandParentListNode = findParentNodeOfTypeClosestToPos(tr.doc.resolve(selectionParentListNodeWithPos.pos), [bulletList, orderedList]);
  const selectionIsInNestedList = !!grandParentListNode;
  let selectedListItemHasNestedList = false;
  selectionParentListItemNode.node.content.forEach(child => {
    if (isListNode(child)) {
      selectedListItemHasNestedList = true;
    }
  });
  return selectedListItemHasNestedList || selectionIsInNestedList;
}

// Helper function to filter expand nodes from slice when not allowed
export function handlePasteExpand(slice) {
  return mapSlice(slice, node => {
    if (node.type.name === 'expand' || node.type.name === 'nestedExpand') {
      const children = [];
      if (node.attrs.title) {
        children.push(node.type.schema.nodes.paragraph.createChecked(undefined, Fragment.from(node.type.schema.text(node.attrs.title))));
      }
      node.content.forEach(inner => children.push(inner));
      return children;
    }
    return node;
  });
}

/*
 * Transform container nodes promoting them to _c1 if required as per
 * transformContainerNodes from adf-utils
 */
export function applyContainerNodeTransformToSlice(slice, schema, destinationParentType) {
  var _sliceJson$content;
  const sliceJson = slice.toJSON();
  if (!sliceJson) {
    return slice;
  }
  const wrappedAdf = {
    type: destinationParentType,
    content: (_sliceJson$content = sliceJson.content) !== null && _sliceJson$content !== void 0 ? _sliceJson$content : []
  };
  try {
    const {
      transformedAdf,
      isTransformed
    } = transformContainerNodes(wrappedAdf, schema);
    return isTransformed && transformedAdf && transformedAdf.content && transformedAdf.content.length > 0 ? Slice.fromJSON(schema, {
      ...sliceJson,
      content: transformedAdf.content
    }) : slice;
  } catch {
    return slice;
  }
}

/*
 * When platform_editor_nest_table_in_panel is OFF, move table elements out of panel divs in the
 * clipboard HTML before ProseMirror parses it. This prevents the panel schema from
 * dropping table content entirely.
 */
export function splitTablesOutOfPanelHtml(html) {
  if (!html.includes('data-panel-type')) {
    return html;
  }
  const doc = new DOMParser().parseFromString(html, 'text/html');
  const panelDivs = Array.from(doc.querySelectorAll('div[data-panel-type]'));
  let changed = false;
  for (const panelDiv of panelDivs) {
    const editorTableWrappers = Array.from(panelDiv.querySelectorAll('[data-prosemirror-node-name="table"]'));
    const rendererTableWrappers = isExperimentEnabled('platform_editor_nest_in_table_renderer_paste') ? Array.from(panelDiv.querySelectorAll('.pm-table-container')).filter(wrapper => wrapper.querySelector('table') !== null) : [];
    const tableWrappers = [...editorTableWrappers, ...rendererTableWrappers];
    if (tableWrappers.length === 0) {
      continue;
    }
    changed = true;
    let anchor = panelDiv;
    for (const tableWrapper of tableWrappers) {
      var _tableWrapper$parentN, _anchor$parentNode;
      (_tableWrapper$parentN = tableWrapper.parentNode) === null || _tableWrapper$parentN === void 0 ? void 0 : _tableWrapper$parentN.removeChild(tableWrapper);
      (_anchor$parentNode = anchor.parentNode) === null || _anchor$parentNode === void 0 ? void 0 : _anchor$parentNode.insertBefore(tableWrapper, anchor.nextSibling);
      anchor = tableWrapper;
    }
  }
  return changed ? doc.body.innerHTML : html;
}