@atlaskit/editor-plugin-paste
Version:
Paste plugin for @atlaskit/editor-core
615 lines (581 loc) • 35.4 kB
JavaScript
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid';
import { ACTION, INPUT_METHOD, PasteTypes } from '@atlaskit/editor-common/analytics';
import { addLinkMetadata } from '@atlaskit/editor-common/card';
import { insideTable } from '@atlaskit/editor-common/core-utils';
import { getExtensionAutoConvertersFromProvider } from '@atlaskit/editor-common/extensions';
import { isNestedTablesSupported } from '@atlaskit/editor-common/nesting';
import { isPastedFile as isPastedFileFromEvent, md } from '@atlaskit/editor-common/paste';
import { measureRender } from '@atlaskit/editor-common/performance/measure-render';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { SyncBlockRendererDataAttributeName } from '@atlaskit/editor-common/sync-block';
import { removeBreakoutFromRendererSyncBlockHTML, transformSingleColumnLayout, transformSingleLineCodeBlockToCodeMark, transformSliceEnsureListItemParagraphFirst, transformSliceNestedExpandToExpand, transformSliceToDecisionList, transformSliceToJoinAdjacentCodeBlocks, transformSliceToRemoveLegacyContentMacro, transformSliceToRemoveMacroId } from '@atlaskit/editor-common/transforms';
import { containsAnyAnnotations, extractSliceFromStep, linkifyContent, mapChildren } from '@atlaskit/editor-common/utils';
import { MarkdownTransformer } from '@atlaskit/editor-markdown-transformer';
import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
import { contains, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { handlePaste as handlePasteTable } from '@atlaskit/editor-tables/utils';
import { insm } from '@atlaskit/insm';
import { extractClientIdsFromHtml } from '@atlaskit/media-common';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { PastePluginActionTypes } from '../editor-actions/actions';
import { splitParagraphs, upgradeTextToLists } from '../editor-commands/commands';
import { transformSliceForMedia, transformSliceToCorrectMediaWrapper, transformSliceToMediaSingleWithNewExperience, unwrapNestedMediaElements } from '../pm-plugins/media';
import { createPasteMeasurePayload, getContentNodeTypes, handleCodeBlockWithAnalytics, handleExpandWithAnalytics, handleMarkdownWithAnalytics, handleMediaSingleWithAnalytics, handleNestedTablePasteWithAnalytics, handlePasteAsPlainTextWithAnalytics, handlePasteIntoCaptionWithAnalytics, handlePasteIntoTaskAndDecisionWithAnalytics, handlePasteLinkOnSelectedTextWithAnalytics, handlePasteNonNestableBlockNodesIntoListWithAnalytics, handlePastePanelOrDecisionIntoListWithAnalytics, handlePastePreservingMarksWithAnalytics, handleRichTextWithAnalytics, handleSelectedTableWithAnalytics, sendPasteAnalyticsEvent } from './analytics';
import { createClipboardTextSerializer } from './create-clipboard-text-serializer';
import { createPluginState, pluginKey as stateKey } from './plugin-factory';
import { escapeBackslashAndLinksExceptCodeBlock, getPasteSource, htmlContainsSingleFile, htmlHasInvalidLinkTags, isPastedFromExcel, isPastedFromWord, removeDuplicateInvalidLinks, transformUnsupportedBlockCardToInline } from './util';
import { handleVSCodeBlock } from './util/edge-cases/handleVSCodeBlock';
import { handleMacroAutoConvert, handleMention, handleParagraphBlockMarks, handlePasteExpand, handleTableContentPasteInBodiedExtension } from './util/handlers';
import { handleSyncBlocksPaste } from './util/sync-block';
import { htmlHasIncompleteTable, isPastedFromTinyMCEConfluence, tryRebuildCompleteTableHtml } from './util/tinyMCE';
export const isInsideBlockQuote = state => {
const {
blockquote
} = state.schema.nodes;
return hasParentNodeOfType(blockquote)(state.selection);
};
const enableNewDomainCheckToImproveSmartLinkResolveRate = hostname => {
return expValEquals('improve_3p_smart_link_resolve_rate', 'isEnabled', true) && (
// OneDrive Shortlinks
hostname.endsWith('1drv.ms') ||
// MS Teams links
hostname.endsWith('teams.live.com') || hostname.endsWith('teams.cloud.microsoft') || hostname.endsWith('teams.microsoft.com'));
};
const PASTE = 'Editor Paste Plugin Paste Duration';
export function isSharePointUrl(url) {
if (!url) {
return false;
}
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname.toLowerCase();
const protocol = urlObj.protocol.toLowerCase();
// Only accept HTTPS URLs for security
if (protocol !== 'https:') {
return false;
}
// Check if hostname ends with the trusted domains (not just contains them)
return hostname.endsWith('sharepoint.com') || hostname.endsWith('onedrive.com') || hostname.endsWith('onedrive.live.com') || enableNewDomainCheckToImproveSmartLinkResolveRate(hostname);
} catch {
// If URL parsing fails, return false for safety
return false;
}
}
export function createPlugin(schema, dispatchAnalyticsEvent, dispatch, featureFlags, pluginInjectionApi, getIntl, cardOptions, sanitizePrivateContent, providerFactory, pasteWarningOptions) {
var _pluginInjectionApi$a;
const editorAnalyticsAPI = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions;
const atlassianMarkDownParser = new MarkdownTransformer(schema, md);
function getMarkdownSlice(text, openStart, openEnd) {
const escapedTextInput = escapeBackslashAndLinksExceptCodeBlock(text);
const doc = atlassianMarkDownParser.parse(escapedTextInput);
if (doc && doc.content) {
return new Slice(doc.content, openStart, openEnd);
}
return;
}
let extensionAutoConverter;
async function setExtensionAutoConverter(name, extensionProviderPromise) {
if (name !== 'extensionProvider' || !extensionProviderPromise) {
return;
}
try {
extensionAutoConverter = await getExtensionAutoConvertersFromProvider(extensionProviderPromise);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
if (providerFactory) {
providerFactory.subscribe('extensionProvider', setExtensionAutoConverter);
}
let mostRecentPasteEvent;
let pastedFromBitBucket = false;
return new SafePlugin({
key: stateKey,
state: createPluginState(dispatch, {
activeFlag: null,
pastedMacroPositions: {},
lastContentPasted: null
}),
props: {
// For serialising to plain text
clipboardTextSerializer: createClipboardTextSerializer(getIntl()),
handleDOMEvents: {
// note
paste: (view, event) => {
mostRecentPasteEvent = event;
if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true) && event.clipboardData) {
insm.startHeavyTask('paste');
}
return false;
}
},
// note
handlePaste(view, rawEvent, slice) {
var _text, _schema$nodes, _schema$nodes2, _schema$nodes3, _pluginInjectionApi$m, _schema$nodes$table;
const event = rawEvent;
if (!event.clipboardData) {
return false;
}
let text = event.clipboardData.getData('text/plain');
const html = event.clipboardData.getData('text/html');
const uriList = event.clipboardData.getData('text/uri-list');
// Extract clientId values from pasted HTML for media cross-product copy/paste
// This must be done before ProseMirror parses the HTML, as clientId is not stored in ADF
if (fg('platform_media_cross_client_copy_with_auth')) {
extractClientIdsFromHtml(html);
}
// Links copied from iOS Safari share button only have the text/uri-list data type
// ProseMirror don't do anything with this type so we want to make our own open slice
// with url as text content so link is pasted inline
if (uriList && !text && !html) {
text = uriList;
slice = new Slice(Fragment.from(schema.text(text)), 1, 1);
}
if ((_text = text) !== null && _text !== void 0 && _text.includes('\r')) {
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
text = text.replace(/\r/g, '');
}
// Strip Legacy Content Macro (LCM) extensions on paste
if (!fg('platform_editor_legacy_content_macro_insert')) {
slice = transformSliceToRemoveLegacyContentMacro(slice, schema);
}
const isPastedFile = isPastedFileFromEvent(event);
const isPlainText = text && !html;
const isRichText = !!html;
// Bail if copied content has files
if (isPastedFile) {
if (!html) {
if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) {
insm.endHeavyTask('paste');
}
/**
* Microsoft Office, Number, Pages, etc. adds an image to clipboard
* with other mime-types so we don't let the event reach media.
* The detection ration here is that if the payload has both `html` and
* `files`, then it could be one of above or an image copied from web.
* Here, we don't have html, so we return true to allow default event behaviour
*/
return true;
}
/**
* We want to return false for external copied image to allow
* it to be uploaded by the client.
*
* Scenario where we are pasting an external image inside a block quote
* is skipped and handled in handleRichText
*/
if (htmlContainsSingleFile(html) && !isInsideBlockQuote(view.state)) {
if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) {
insm.endHeavyTask('paste');
}
return true;
}
/**
* https://product-fabric.atlassian.net/browse/ED-21993
* stopImmediatePropagation will run the first event attached to the same element
* Which chould have race condition issue
*/
event.stopPropagation();
}
const {
state
} = view;
const content = getContentNodeTypes(slice.content);
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
const pasteId = uuid();
const measureName = `${PASTE}_${pasteId}`;
measureRender(measureName, ({
duration,
distortedDuration
}) => {
const payload = createPasteMeasurePayload({
view,
duration,
content,
distortedDuration
});
if (payload) {
dispatchAnalyticsEvent(payload);
}
if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) {
insm.endHeavyTask('paste');
}
});
const getLastPastedSlice = tr => {
let slice;
for (const step of tr.steps) {
const stepSlice = extractSliceFromStep(step);
if (stepSlice) {
slice = stepSlice;
}
}
return slice;
};
// creating a custom dispatch because we want to add a meta whenever we do a paste.
const dispatch = tr => {
var _state$doc$resolve$no;
// https://product-fabric.atlassian.net/browse/ED-12633
// don't add closeHistory call if we're pasting a text inside placeholder text as we want the whole action
// to be atomic
const {
placeholder
} = state.schema.nodes;
const isPastingTextInsidePlaceholderText = ((_state$doc$resolve$no = state.doc.resolve(state.selection.$anchor.pos).nodeAfter) === null || _state$doc$resolve$no === void 0 ? void 0 : _state$doc$resolve$no.type) === placeholder;
// Don't add closeHistory if we're pasting over layout columns, as we will appendTransaction
// to cleanup the layout's structure and we want to keep the paste and re-structuring as
// one event.
const isPastingOverLayoutColumns = hasParentNodeOfType(state.schema.nodes.layoutColumn)(state.selection);
// don't add closeHistory call if we're pasting a table, as some tables may involve additional
// appendedTransactions to repair them (if they're partial or incomplete) and we don't want
// to split those repairing transactions in prosemirror-history when they're being added to the
// "done" stack
const isPastingTable = tr.steps.some(step => {
var _slice$content;
const slice = extractSliceFromStep(step);
let tableExists = false;
slice === null || slice === void 0 ? void 0 : (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.forEach(node => {
if (node.type === state.schema.nodes.table) {
tableExists = true;
}
});
return tableExists;
});
// Don't flag as a paste event (add closeHistory) when the paste affects a list and
// the list plugin's appendTransaction will normalise the structure, and we want the
// paste + normalisation to be a single undo step.
// Pasting into an existing list — selection is inside a list node.
let isPastingIntoList = false;
// Pasting list content from an external source — slice top-level contains a list node.
let isPastingListContent = false;
if (expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true)) {
const listNodeTypes = [state.schema.nodes.bulletList, state.schema.nodes.orderedList, state.schema.nodes.taskList].filter(n => Boolean(n));
isPastingIntoList = hasParentNodeOfType(listNodeTypes)(state.selection);
for (let i = 0; i < slice.content.childCount; i++) {
if (listNodeTypes.includes(slice.content.child(i).type)) {
isPastingListContent = true;
break;
}
}
}
if (!isPastingTextInsidePlaceholderText && !isPastingTable && !isPastingOverLayoutColumns && !isPastingIntoList && !isPastingListContent && pluginInjectionApi !== null && pluginInjectionApi !== void 0 && pluginInjectionApi.betterTypeHistory) {
var _pluginInjectionApi$b;
tr = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$b = pluginInjectionApi.betterTypeHistory) === null || _pluginInjectionApi$b === void 0 ? void 0 : _pluginInjectionApi$b.actions.flagPasteEvent(tr);
}
const isDocChanged = tr.docChanged;
addLinkMetadata(view.state.selection, tr, {
action: isPlainText ? ACTION.PASTED_AS_PLAIN : ACTION.PASTED,
inputMethod: INPUT_METHOD.CLIPBOARD
});
// handleMacroAutoConvert dispatches twice
// we make sure to call paste options toolbar
// only for a valid paste action
if (isDocChanged) {
const pastedSlice = getLastPastedSlice(tr);
if (pastedSlice) {
var _input;
const pasteStartPos = state.selection.from;
const pasteEndPos = tr.selection.to;
const contentPasted = {
pasteStartPos,
pasteEndPos,
text,
isShiftPressed: Boolean(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view.shiftKey || ((_input = view.input) === null || _input === void 0 ? void 0 : _input.shiftKey)),
isPlainText: Boolean(isPlainText),
pastedSlice,
pastedAt: Date.now(),
pasteSource: getPasteSource(event)
};
tr.setMeta(stateKey, {
type: PastePluginActionTypes.ON_PASTE,
contentPasted
});
}
}
// the handlePaste definition overrides the generic prosemirror behaviour which would previously
// include a uiEvent meta of paste. To align with the docs (https://prosemirror.net/docs/ref/#state.Transaction)
// This will re-add the uiEvent meta.
view.dispatch(tr.setMeta('uiEvent', 'paste'));
};
slice = handleParagraphBlockMarks(state, slice);
slice = handleVSCodeBlock({
state,
slice,
event,
text
});
if (editorExperiment('platform_synced_block', true)) {
slice = handleSyncBlocksPaste(slice, schema, getPasteSource(event), html, pasteWarningOptions, pluginInjectionApi);
}
const plainTextPasteSlice = linkifyContent(state.schema)(slice);
if (handlePasteAsPlainTextWithAnalytics(editorAnalyticsAPI)(view, event, plainTextPasteSlice)(state, dispatch, view)) {
return true;
}
if (handlePasteIntoCaptionWithAnalytics(editorAnalyticsAPI)(view, event, slice, PasteTypes.richText)(state, dispatch)) {
// Create a custom handler to avoid handling with handleRichText method
// As SafeInsert is used inside handleRichText which caused some bad UX like this:
// https://product-fabric.atlassian.net/browse/MEX-1520
// Converting caption to plain text needs to be handled before transformSliceForMedia
// as createChecked will fail when trying to create a mediaSingle node with a caption
// that is not plain text.
return true;
}
// transform slices based on destination
slice = transformSliceForMedia(slice, schema, pluginInjectionApi)(state.selection);
let markdownSlice;
if (isPlainText) {
var _markdownSlice;
markdownSlice = getMarkdownSlice(text, slice.openStart, slice.openEnd);
// https://product-fabric.atlassian.net/browse/ED-15134
// Lists are not allowed within Blockquotes at this time. Attempting to
// paste a markdown list ie. ">- foo" will yeild a markdownSlice of size 0.
// Rather then blocking the paste action with no UI feedback, this will instead
// force a "paste as plain text" action by clearing the markdownSlice.
markdownSlice = !((_markdownSlice = markdownSlice) !== null && _markdownSlice !== void 0 && _markdownSlice.size) ? undefined : markdownSlice;
if (markdownSlice) {
var _pluginInjectionApi$c, _pluginInjectionApi$c2, _pluginInjectionApi$e, _pluginInjectionApi$e2;
// linkify text prior to converting to macro
if (handlePasteLinkOnSelectedTextWithAnalytics(editorAnalyticsAPI)(view, event, markdownSlice, PasteTypes.markdown)(state, dispatch)) {
return true;
}
// run macro autoconvert prior to other conversions
if (handleMacroAutoConvert(text, markdownSlice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.card) === null || _pluginInjectionApi$c === void 0 ? void 0 : (_pluginInjectionApi$c2 = _pluginInjectionApi$c.actions) === null || _pluginInjectionApi$c2 === void 0 ? void 0 : _pluginInjectionApi$c2.queueCardsFromChangedTr, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e = pluginInjectionApi.extension) === null || _pluginInjectionApi$e === void 0 ? void 0 : (_pluginInjectionApi$e2 = _pluginInjectionApi$e.actions) === null || _pluginInjectionApi$e2 === void 0 ? void 0 : _pluginInjectionApi$e2.runMacroAutoConvert, cardOptions, extensionAutoConverter)(state, dispatch, view)) {
// TODO: ED-26959 - handleMacroAutoConvert dispatch twice, so we can't use the helper
sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, markdownSlice, {
type: PasteTypes.markdown
});
return true;
}
}
}
slice = transformUnsupportedBlockCardToInline(slice, state, cardOptions);
// Handles edge case so that when copying text from the top level of the document
// it can be pasted into nodes like panels/actions/decisions without removing them.
// Overriding openStart to be 1 when only pasting a paragraph makes the preferred
// depth favour the text, rather than the paragraph node.
// https://github.com/ProseMirror/prosemirror-transform/blob/master/src/replace.js#:~:text=Transform.prototype.-,replaceRange,-%3D%20function(from%2C%20to
const selectionDepth = state.selection.$head.depth;
const selectionParentNode = state.selection.$head.node(selectionDepth - 1);
const selectionParentType = selectionParentNode === null || selectionParentNode === void 0 ? void 0 : selectionParentNode.type;
const edgeCaseNodeTypes = [(_schema$nodes = schema.nodes) === null || _schema$nodes === void 0 ? void 0 : _schema$nodes.panel, (_schema$nodes2 = schema.nodes) === null || _schema$nodes2 === void 0 ? void 0 : _schema$nodes2.taskList, (_schema$nodes3 = schema.nodes) === null || _schema$nodes3 === void 0 ? void 0 : _schema$nodes3.decisionList];
if (slice.openStart === 0 && slice.openEnd !== 1 && selectionParentNode && edgeCaseNodeTypes.includes(selectionParentType)) {
// @ts-ignore - [unblock prosemirror bump] assigning to readonly prop
slice.openStart = 1;
}
// If we're in a code block, append the text contents of clipboard inside it
if (handleCodeBlockWithAnalytics(editorAnalyticsAPI)(view, event, slice, text)(state, dispatch)) {
return true;
}
if (handleMediaSingleWithAnalytics(editorAnalyticsAPI)(view, event, slice, isPastedFile ? PasteTypes.binary : PasteTypes.richText, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$m = pluginInjectionApi.media) === null || _pluginInjectionApi$m === void 0 ? void 0 : _pluginInjectionApi$m.actions.insertMediaAsMediaSingle)(state, dispatch, view)) {
return true;
}
if (handleSelectedTableWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) {
return true;
}
let isNestedMarkdownTable = false;
// if paste a markdown table inside a table cell, we should treat it as a table slice
const isParentNodeTdOrTh = selectionParentType === schema.nodes.tableCell || selectionParentType === schema.nodes.tableHeader;
isNestedMarkdownTable = !!(markdownSlice && isPlainText && isParentNodeTdOrTh && getContentNodeTypes(markdownSlice.content).includes((_schema$nodes$table = schema.nodes.table) === null || _schema$nodes$table === void 0 ? void 0 : _schema$nodes$table.name));
slice = isNestedMarkdownTable ? markdownSlice : slice;
// get editor-tables to handle pasting tables if it can
// otherwise, just the replace the selection with the content
if (handlePasteTable(view, event, slice, {
pasteSource: getPasteSource(event)
})) {
sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, slice, {
type: PasteTypes.richText
});
return true;
}
// handle paste of nested tables to ensure nesting limits are respected
if (handleNestedTablePasteWithAnalytics(editorAnalyticsAPI, isNestedTablesSupported(state.schema))(view, event, slice)(state, dispatch)) {
return true;
}
if (handlePasteIntoTaskAndDecisionWithAnalytics(view, event, slice, isPlainText ? PasteTypes.plain : PasteTypes.richText, pluginInjectionApi)(state, dispatch)) {
return true;
}
// If the clipboard only contains plain text, attempt to parse it as Markdown
if (isPlainText && markdownSlice && !isNestedMarkdownTable) {
if (handlePastePreservingMarksWithAnalytics(view, event, markdownSlice, PasteTypes.markdown, pluginInjectionApi)(state, dispatch)) {
return true;
}
return handleMarkdownWithAnalytics(view, event, markdownSlice, pluginInjectionApi)(state, dispatch);
}
if (isRichText && isInsideBlockQuote(state)) {
//If pasting inside blockquote
//Skip the blockquote node and keep remaining nodes as they are
//prevent doing this if there is list inside blockquote as the list is pasted incorrectly inside blockquote due to wrong openStart and openEnd
const {
blockquote
} = schema.nodes;
const children = [];
mapChildren(slice.content, node => {
if (node.type === blockquote && !contains(node, state.schema.nodes.listItem)) {
for (let i = 0; i < node.childCount; i++) {
children.push(node.child(i));
}
} else {
children.push(node);
}
});
slice = new Slice(Fragment.fromArray(children), slice.openStart, slice.openEnd);
}
// finally, handle rich-text copy-paste
if (isRichText || isNestedMarkdownTable) {
var _pluginInjectionApi$c3, _pluginInjectionApi$c4, _pluginInjectionApi$e3, _pluginInjectionApi$e4, _pluginInjectionApi$l;
// linkify the text where possible
slice = linkifyContent(state.schema)(slice);
if (handlePasteLinkOnSelectedTextWithAnalytics(editorAnalyticsAPI)(view, event, slice, PasteTypes.richText)(state, dispatch)) {
return true;
}
// run macro autoconvert prior to other conversions
if (handleMacroAutoConvert(text, slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c3 = pluginInjectionApi.card) === null || _pluginInjectionApi$c3 === void 0 ? void 0 : (_pluginInjectionApi$c4 = _pluginInjectionApi$c3.actions) === null || _pluginInjectionApi$c4 === void 0 ? void 0 : _pluginInjectionApi$c4.queueCardsFromChangedTr, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e3 = pluginInjectionApi.extension) === null || _pluginInjectionApi$e3 === void 0 ? void 0 : (_pluginInjectionApi$e4 = _pluginInjectionApi$e3.actions) === null || _pluginInjectionApi$e4 === void 0 ? void 0 : _pluginInjectionApi$e4.runMacroAutoConvert, cardOptions, extensionAutoConverter)(state, dispatch, view)) {
// TODO: ED-26959 - handleMacroAutoConvert dispatch twice, so we can't use the helper
sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, slice, {
type: PasteTypes.richText
});
return true;
}
// Special handling for SharePoint URLs generated from Share button
// eslint-disable-next-line @atlaskit/platform/no-preconditioning
if (isSharePointUrl(text) && (fg('platform_editor_sharepoint_url_smart_card_fallback') || fg('platform_editor_sharepoint_url_smart_card_jira'))) {
// Create an inline card directly for SharePoint URLs to show the "Connect" button
const inlineCardNode = schema.nodes.inlineCard.create({
url: text
});
const cardSlice = new Slice(Fragment.from(inlineCardNode), 0, 0);
if (dispatch) {
dispatch(state.tr.replaceSelection(cardSlice));
}
return true;
}
// handle the case when copy content from a table cell inside bodied extension
if (handleTableContentPasteInBodiedExtension(slice)(state, dispatch)) {
return true;
}
// remove annotation marks from the pasted data if they are not present in the document
// for the cases when they are pasted from external pages
if (slice.content.size && containsAnyAnnotations(slice, state)) {
var _pluginInjectionApi$a2;
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a2 = pluginInjectionApi.annotation) === null || _pluginInjectionApi$a2 === void 0 ? void 0 : _pluginInjectionApi$a2.actions.stripNonExistingAnnotations(slice, state);
}
if (handlePastePreservingMarksWithAnalytics(view, event, slice, PasteTypes.richText, pluginInjectionApi)(state, dispatch)) {
return true;
}
// Check that we are pasting in a location that does not accept
// breakout marks, if so we strip the mark and paste. Note that
// breakout marks are only valid in the root document.
if (selectionParentType !== state.schema.nodes.doc) {
const sliceCopy = Slice.fromJSON(state.schema, slice.toJSON() || {});
sliceCopy.content.descendants(node => {
// @ts-ignore - [unblock prosemirror bump] assigning to readonly prop
node.marks = node.marks.filter(mark => mark.type.name !== 'breakout');
// as breakout marks should only be on top level nodes,
// we don't traverse the entire document
return false;
});
slice = sliceCopy;
}
if (handleExpandWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) {
return true;
}
if (!insideTable(state)) {
slice = transformSliceNestedExpandToExpand(slice, state.schema);
}
if (handlePastePanelOrDecisionIntoListWithAnalytics(editorAnalyticsAPI)(view, event, slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$l = pluginInjectionApi.list) === null || _pluginInjectionApi$l === void 0 ? void 0 : _pluginInjectionApi$l.actions.findRootParentListNode)(state, dispatch)) {
return true;
}
if (handlePasteNonNestableBlockNodesIntoListWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) {
return true;
}
return handleRichTextWithAnalytics(view, event, slice, pluginInjectionApi)(state, dispatch);
}
return false;
},
transformPasted(slice) {
var _pluginInjectionApi$e5, _pluginInjectionApi$e6, _pluginInjectionApi$e7;
if (sanitizePrivateContent) {
slice = handleMention(slice, schema);
}
/* Bitbucket copies diffs as multiple adjacent code blocks
* so we merge ALL adjacent code blocks to support paste here */
if (pastedFromBitBucket) {
slice = transformSliceToJoinAdjacentCodeBlocks(slice);
}
// Filter out expand nodes if allowExpand is false
if (!(pluginInjectionApi !== null && pluginInjectionApi !== void 0 && (_pluginInjectionApi$e5 = pluginInjectionApi.expand) !== null && _pluginInjectionApi$e5 !== void 0 && (_pluginInjectionApi$e6 = _pluginInjectionApi$e5.sharedState) !== null && _pluginInjectionApi$e6 !== void 0 && (_pluginInjectionApi$e7 = _pluginInjectionApi$e6.currentState()) !== null && _pluginInjectionApi$e7 !== void 0 && _pluginInjectionApi$e7.allowInsertion) && expValEquals('platform_editor_expand_paste_in_comment_editor', 'isEnabled', true)) {
slice = handlePasteExpand(slice);
}
slice = transformSingleLineCodeBlockToCodeMark(slice, schema);
slice = transformSliceToCorrectMediaWrapper(slice, schema);
slice = transformSliceToMediaSingleWithNewExperience(slice, schema, pluginInjectionApi);
slice = transformSliceToDecisionList(slice, schema);
// splitting linebreaks into paragraphs must happen before upgrading text to lists
slice = splitParagraphs(slice, schema);
slice = upgradeTextToLists(slice, schema);
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (slice.content.childCount && slice.content.lastChild.type === schema.nodes.codeBlock) {
slice = new Slice(slice.content, 0, 0);
}
if (!editorExperiment('single_column_layouts', true)) {
slice = transformSingleColumnLayout(slice, schema);
}
slice = transformSliceToRemoveMacroId(slice, schema);
if (expValEquals('platform_editor_flexible_list_schema', 'isEnabled', true) && !expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
// Prevent pasted externally-authored flexible list HTML from producing flexible list structures
// Only when schema support is enabled but indentation behaviour is not, meaning editor gracefully
// handles the structure, but ideally does not produce it
slice = transformSliceEnsureListItemParagraphFirst(slice, schema);
}
return slice;
},
transformPastedHTML(html) {
// Fix for issue ED-4438
// text from google docs should not be pasted as inline code
if (html.indexOf('id="docs-internal-guid-') >= 0) {
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
html = html.replace(/white-space:pre/g, '');
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
html = html.replace(/white-space:pre-wrap/g, '');
}
// Partial fix for ED-7331: During a copy/paste from the legacy tinyMCE
// confluence editor, if we encounter an incomplete table (e.g. table elements
// not wrapped in <table>), we try to rebuild a complete, valid table if possible.
if (mostRecentPasteEvent && isPastedFromTinyMCEConfluence(mostRecentPasteEvent, html) && htmlHasIncompleteTable(html)) {
const completeTableHtml = tryRebuildCompleteTableHtml(html);
if (completeTableHtml) {
html = completeTableHtml;
}
}
if (!isPastedFromWord(html) && !isPastedFromExcel(html) && html.indexOf('<img ') >= 0) {
html = unwrapNestedMediaElements(html);
}
// https://product-fabric.atlassian.net/browse/ED-11714
// Checking for edge case when copying a list item containing links from Notion
// The html from this case is invalid with duplicate nested links
if (htmlHasInvalidLinkTags(html)) {
html = removeDuplicateInvalidLinks(html);
}
// Fix for ED-13568: Code blocks being copied/pasted when next to each other get merged
pastedFromBitBucket = html.indexOf('data-qa="code-line"') >= 0;
// Remove breakout marks HTML around sync block renderer nodes
// so the breakout mark doesn't get applied to the wrong nodes
if (html.indexOf(SyncBlockRendererDataAttributeName) >= 0 && editorExperiment('platform_synced_block', true)) {
html = removeBreakoutFromRendererSyncBlockHTML(html);
}
mostRecentPasteEvent = null;
return html;
}
}
});
}