UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

280 lines (274 loc) 12.5 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD, TABLE_ACTION } from '@atlaskit/editor-common/analytics'; import { sortByOrderWithTypeName } from '@atlaskit/editor-common/legacy-rank-plugins'; import { isSupportedInParent, mapChildren } from '@atlaskit/editor-common/utils'; import { Fragment, Mark, Slice } from '@atlaskit/editor-prosemirror/model'; import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { getSelectedTableInfo, isTableSelected } from '@atlaskit/editor-tables/utils'; import { isMediaBlobUrl } from '@atlaskit/media-client'; export function isPastedFromWord(html) { return !!html && html.indexOf('urn:schemas-microsoft-com:office:word') >= 0; } export function isPastedFromExcel(html) { return !!html && html.indexOf('urn:schemas-microsoft-com:office:excel') >= 0; } function isPastedFromDropboxPaper(html) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return !!html && !!html.match(/class=\"\s?author-d-.+"/gim); } function isPastedFromGoogleDocs(html) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return !!html && !!html.match(/id=\"docs-internal-guid-.+"/gim); } function isPastedFromGoogleSpreadSheets(html) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return !!html && !!html.match(/data-sheets-.+=/gim); } function isPastedFromPages(html) { return !!html && html.indexOf('content="Cocoa HTML Writer"') >= 0; } function isPastedFromFabricEditor(html) { return !!html && html.indexOf('data-pm-slice="') >= 0; } export var isSingleLine = function isSingleLine(text) { return !!text && text.trim().split('\n').length === 1; }; export function htmlContainsSingleFile(html) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return !!html.match(/<img .*>/) && !isMediaBlobUrl(html); } export function getPasteSource(event) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion var html = event.clipboardData.getData('text/html'); if (isPastedFromDropboxPaper(html)) { return 'dropbox-paper'; } else if (isPastedFromWord(html)) { return 'microsoft-word'; } else if (isPastedFromExcel(html)) { return 'microsoft-excel'; } else if (isPastedFromGoogleDocs(html)) { return 'google-docs'; } else if (isPastedFromGoogleSpreadSheets(html)) { return 'google-spreadsheets'; } else if (isPastedFromPages(html)) { return 'apple-pages'; } else if (isPastedFromFabricEditor(html)) { return 'fabric-editor'; } return 'uncategorized'; } /** * Wrap link with angle brackets if they are not already contained in markdown url syntax (e.g. [text](url)) * * This mitigate some issues in the markdown-it parser (or linkify where it would not parse the link correctly if it contains some characters. * @see https://product-fabric.atlassian.net/browse/ED-3159 * @see https://github.com/markdown-it/markdown-it/issues/38 * * This function was introduced in * https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/commits/64d0f30bbe7014#platform%2Fpackages%2Ffabric%2Feditor-core%2Fsrc%2Fplugins%2Fpaste%2Futil.ts * * If a right angle bracket or double quote is present in the url, the url will only be escaped up to the character before the right angle bracket (this is the same behaviour as in Google Docs). * * Tests in platform/packages/editor/editor-plugin-paste-tests/src/__tests__/playwright/paste.spec.ts * check behaviour of double quotes in url strings */ export function escapeLinks(text) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return text.replace(/(\[([^\]]+)\]\()?((https?|ftp|jamfselfservice):\/\/[^\s>"]+)/g, function (str) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return str.match(/^(https?|ftp|jamfselfservice):\/\/[^\s>"]+$/) ? "<".concat(str, ">") : str; }); } /** * Escapes backslashes and links outside code blocks. * * @param textInput - The input string to process, possibly containing code blocks and links. * @returns The processed string with backslashes and links escaped outside code blocks. * @example * const input = 'This is a link: https://example.com and a backslash: \\\n```\ncode block https://example.com not escaped\ncode block \\ not escaped\n```'; * const output = escapeBackslashAndLinksExceptCodeBlock(input); // 'This is a link: <https://example.com> and a backslash: \\\\\n```\ncode block https://example.com not escaped\ncode block \\ not escaped\n```' */ export function escapeBackslashAndLinksExceptCodeBlock(textInput) { // ref: https://spec.commonmark.org/0.31.2/#fenced-code-blocks // Allows up to 3 leading spaces before ``` and optional trailing characters // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var openingCodeFenceRegex = /^( {0,3})```.*$/; // Allows up to 3 leading spaces before ``` and optional trailing spaces or tabs // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var closingCodeFenceRegex = /^( {0,3})```[ \t]*$/; var isInsideCodeBlock = false; var lines = textInput.split('\n'); // In the splitted array, we traverse through every line and check if it will be parsed as a codeblock. return lines.map(function (line) { if (!isInsideCodeBlock && openingCodeFenceRegex.test(line)) { isInsideCodeBlock = true; return line; } if (isInsideCodeBlock && closingCodeFenceRegex.test(line)) { isInsideCodeBlock = false; return line; } // not code fence, don't escape anything inside code block if (isInsideCodeBlock) { return line; } else { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed) var escaped = line.replace(/\\/g, '\\\\'); escaped = escapeLinks(escaped); return escaped; } }).join('\n'); } export function hasOnlyNodesOfType() { for (var _len = arguments.length, nodeTypes = new Array(_len), _key = 0; _key < _len; _key++) { nodeTypes[_key] = arguments[_key]; } return function (slice) { var hasOnlyNodesOfType = true; slice.content.descendants(function (node) { hasOnlyNodesOfType = hasOnlyNodesOfType && nodeTypes.indexOf(node.type) > -1; return hasOnlyNodesOfType; }); return hasOnlyNodesOfType; }; } export function applyTextMarksToSlice(schema, marks) { return function (slice) { var _schema$marks = schema.marks, codeMark = _schema$marks.code, linkMark = _schema$marks.link, annotationMark = _schema$marks.annotation; if (!Array.isArray(marks) || marks.length === 0) { return slice; } var sliceCopy = Slice.fromJSON(schema, slice.toJSON() || {}); // allow links and annotations to be pasted var allowedMarksToPaste = [linkMark, annotationMark]; sliceCopy.content.descendants(function (node, _pos, parent) { if (node.isText && parent && parent.isBlock) { // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop node.marks = [].concat(_toConsumableArray(node.marks && !codeMark.isInSet(marks) && node.marks.filter(function (mark) { return allowedMarksToPaste.includes(mark.type); }) || []), _toConsumableArray(parent.type.allowedMarks(marks).filter(function (mark) { return mark.type !== linkMark; }))).sort(sortByOrderWithTypeName('marks')); return false; } if (node.isInline && ['inlineCard', 'emoji', 'status', 'date', 'mention'].includes(node.type.name) && parent && parent.isBlock) { // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop node.marks = [].concat(_toConsumableArray(node.marks), _toConsumableArray(parent.type.allowedMarks(marks).filter(function (mark) { return mark.type === schema.marks.annotation; }))); } return true; }); return sliceCopy; }; } export function isEmptyNode(node) { if (!node) { return false; } var nodeType = node.type; var emptyNode = nodeType.createAndFill(); return emptyNode && emptyNode.nodeSize === node.nodeSize && emptyNode.content.eq(node.content) && Mark.sameSet(emptyNode.marks, node.marks); } export function isCursorSelectionAtTextStartOrEnd(selection) { return selection instanceof TextSelection && selection.empty && selection.$cursor && (!selection.$cursor.nodeBefore || !selection.$cursor.nodeAfter); } export function isPanelNode(node) { return Boolean(node && node.type.name === 'panel'); } export function isSelectionInsidePanel(selection) { if (selection instanceof NodeSelection && isPanelNode(selection.node)) { return selection.node; } var panel = selection.$from.doc.type.schema.nodes.panel; var panelPosition = findParentNodeOfType(panel)(selection); if (panelPosition) { return panelPosition.node; } return null; } // https://product-fabric.atlassian.net/browse/ED-11714 // Checks for broken html that comes from links in a list item copied from Notion export var htmlHasInvalidLinkTags = function htmlHasInvalidLinkTags(html) { return !!html && (html.includes('</a></a>') || html.includes('"></a><a')); }; // https://product-fabric.atlassian.net/browse/ED-11714 // Example of broken html edge case we're solving // <li><a href="http://www.atlassian.com\"<a> href="http://www.atlassian.com\"http://www.atlassian.com</a></a></li>"> export var removeDuplicateInvalidLinks = function removeDuplicateInvalidLinks(html) { if (htmlHasInvalidLinkTags(html)) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var htmlArray = html.split(/(?=<a)/); var htmlArrayWithoutInvalidLinks = htmlArray.filter(function (item) { return !(item.includes('<a') && item.includes('"></a>')) && !(item.includes('<a') && !item.includes('</a>')); }); var fixedHtml = htmlArrayWithoutInvalidLinks.join('') // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp .replace(/<\/a><\/a>/gi, '</a>') // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp .replace(/<a>/gi, '<a'); return fixedHtml; } return html; }; export var addReplaceSelectedTableAnalytics = function addReplaceSelectedTableAnalytics(state, tr, editorAnalyticsAPI) { if (isTableSelected(state.selection)) { var _getSelectedTableInfo = getSelectedTableInfo(state.selection), totalRowCount = _getSelectedTableInfo.totalRowCount, totalColumnCount = _getSelectedTableInfo.totalColumnCount; editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({ action: TABLE_ACTION.REPLACED, actionSubject: ACTION_SUBJECT.TABLE, attributes: { totalColumnCount: totalColumnCount, totalRowCount: totalRowCount, inputMethod: INPUT_METHOD.CLIPBOARD }, eventType: EVENT_TYPE.TRACK })(tr); return tr; } return state.tr; }; export var transformUnsupportedBlockCardToInline = function transformUnsupportedBlockCardToInline(slice, state, cardOptions) { var _state$schema$nodes = state.schema.nodes, blockCard = _state$schema$nodes.blockCard, inlineCard = _state$schema$nodes.inlineCard; var children = []; mapChildren(slice.content, function (node, i, frag) { var _cardOptions$allowBlo; if (node.type === blockCard && !isBlockCardSupported(state, frag, (_cardOptions$allowBlo = cardOptions === null || cardOptions === void 0 ? void 0 : cardOptions.allowBlockCards) !== null && _cardOptions$allowBlo !== void 0 ? _cardOptions$allowBlo : false)) { children.push(inlineCard.createChecked(node.attrs, node.content, node.marks)); } else { children.push(node); } }); return new Slice(Fragment.fromArray(children), slice.openStart, slice.openEnd); }; /** * Function to determine if a block card is supported by the editor * @param state * @param frag * @param allowBlockCards * @returns */ var isBlockCardSupported = function isBlockCardSupported(state, frag, allowBlockCards) { return allowBlockCards && isSupportedInParent(state, frag); };