UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

324 lines (316 loc) 15.1 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.addReplaceSelectedTableAnalytics = void 0; exports.applyTextMarksToSlice = applyTextMarksToSlice; exports.escapeBackslashAndLinksExceptCodeBlock = escapeBackslashAndLinksExceptCodeBlock; exports.escapeLinks = escapeLinks; exports.getPasteSource = getPasteSource; exports.hasOnlyNodesOfType = hasOnlyNodesOfType; exports.htmlContainsSingleFile = htmlContainsSingleFile; exports.htmlHasInvalidLinkTags = void 0; exports.isCursorSelectionAtTextStartOrEnd = isCursorSelectionAtTextStartOrEnd; exports.isEmptyNode = isEmptyNode; exports.isPanelNode = isPanelNode; exports.isPastedFromExcel = isPastedFromExcel; exports.isPastedFromWord = isPastedFromWord; exports.isSelectionInsidePanel = isSelectionInsidePanel; exports.transformUnsupportedBlockCardToInline = exports.removeDuplicateInvalidLinks = exports.isSingleLine = void 0; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _analytics = require("@atlaskit/editor-common/analytics"); var _legacyRankPlugins = require("@atlaskit/editor-common/legacy-rank-plugins"); var _utils = require("@atlaskit/editor-common/utils"); var _nodeTypeUtils = require("@atlaskit/editor-common/utils/node-type-utils"); var _model = require("@atlaskit/editor-prosemirror/model"); var _state = require("@atlaskit/editor-prosemirror/state"); var _utils2 = require("@atlaskit/editor-prosemirror/utils"); var _utils3 = require("@atlaskit/editor-tables/utils"); var _mediaClient = require("@atlaskit/media-client"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var HTML_CONTAINS_SINGLE_FILE_REGEX = /<img .*>/; // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var ESCAPE_LINKS_INNER_REGEX = /^(https?|ftp|jamfselfservice):\/\/[^\s>"]+$/; // Allows up to 3 leading spaces before ``` and optional trailing characters // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var OPENING_CODE_FENCE_REGEX = /^( {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 CLOSING_CODE_FENCE_REGEX = /^( {0,3})```[ ]*$/; // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var SPLIT_ON_ANCHOR_REGEX = /(?=<a)/; function isPastedFromWord(html) { return !!html && html.indexOf('urn:schemas-microsoft-com:office:word') >= 0; } 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; } function isPastedFromFabricRenderer(html) { return !!html && html.indexOf('data-renderer-start-pos="') >= 0 && (0, _platformFeatureFlags.fg)('platform_editor_paste_renderer_analytics'); } var isSingleLine = exports.isSingleLine = function isSingleLine(text) { return !!text && text.trim().split('\n').length === 1; }; function htmlContainsSingleFile(html) { return !!html.match(HTML_CONTAINS_SINGLE_FILE_REGEX) && !(0, _mediaClient.isMediaBlobUrl)(html); } 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'; } else if (isPastedFromFabricRenderer(html)) { return 'fabric-renderer'; } 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 */ function escapeLinks(text) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return text.replace(/(\[([^\]]+)\]\()?((https?|ftp|jamfselfservice):\/\/[^\s>"]+)/g, function (str) { return str.match(ESCAPE_LINKS_INNER_REGEX) ? "<".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```' */ function escapeBackslashAndLinksExceptCodeBlock(textInput, options) { var shouldEscapeLinks = !(options !== null && options !== void 0 && options.skipLinkEscaping); // ref: https://spec.commonmark.org/0.31.2/#fenced-code-blocks 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 && OPENING_CODE_FENCE_REGEX.test(line)) { isInsideCodeBlock = true; return line; } if (isInsideCodeBlock && CLOSING_CODE_FENCE_REGEX.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, '\\\\'); if (shouldEscapeLinks) { escaped = escapeLinks(escaped); } return escaped; } }).join('\n'); } 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; }; } 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 = _model.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((0, _toConsumableArray2.default)(node.marks && !codeMark.isInSet(marks) && node.marks.filter(function (mark) { return allowedMarksToPaste.includes(mark.type); }) || []), (0, _toConsumableArray2.default)(parent.type.allowedMarks(marks).filter(function (mark) { return mark.type !== linkMark; }))).sort((0, _legacyRankPlugins.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((0, _toConsumableArray2.default)(node.marks), (0, _toConsumableArray2.default)(parent.type.allowedMarks(marks).filter(function (mark) { return mark.type === schema.marks.annotation; }))); } return true; }); return sliceCopy; }; } 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) && _model.Mark.sameSet(emptyNode.marks, node.marks); } function isCursorSelectionAtTextStartOrEnd(selection) { return selection instanceof _state.TextSelection && selection.empty && selection.$cursor && (!selection.$cursor.nodeBefore || !selection.$cursor.nodeAfter); } function isPanelNode(node) { // panel_c1 is a schema variant of panel (used when a table is nested inside a panel). When the // experiment is on, treat it as a panel too. if ((0, _expValEquals.expValEquals)('platform_editor_nest_table_in_panel', 'isEnabled', true)) { return Boolean(node && (0, _nodeTypeUtils.getBaseNodeTypeName)(node.type) === 'panel'); } return Boolean(node && node.type.name === 'panel'); } function isSelectionInsidePanel(selection) { if (selection instanceof _state.NodeSelection && isPanelNode(selection.node)) { return selection.node; } var _selection$$from$doc$ = selection.$from.doc.type.schema.nodes, panel = _selection$$from$doc$.panel, panel_c1 = _selection$$from$doc$.panel_c1; // When the experiment is on, also match panel_c1 (table-in-panel variant). var panelPosition = (0, _expValEquals.expValEquals)('platform_editor_nest_table_in_panel', 'isEnabled', true) ? (0, _utils2.findParentNodeOfType)([panel, panel_c1].filter(Boolean))(selection) : (0, _utils2.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 var htmlHasInvalidLinkTags = exports.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>"> var removeDuplicateInvalidLinks = exports.removeDuplicateInvalidLinks = function removeDuplicateInvalidLinks(html) { if (htmlHasInvalidLinkTags(html)) { var htmlArray = html.split(SPLIT_ON_ANCHOR_REGEX); 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; }; var addReplaceSelectedTableAnalytics = exports.addReplaceSelectedTableAnalytics = function addReplaceSelectedTableAnalytics(state, tr, editorAnalyticsAPI) { if ((0, _utils3.isTableSelected)(state.selection)) { var _getSelectedTableInfo = (0, _utils3.getSelectedTableInfo)(state.selection), totalRowCount = _getSelectedTableInfo.totalRowCount, totalColumnCount = _getSelectedTableInfo.totalColumnCount; editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({ action: _analytics.TABLE_ACTION.REPLACED, actionSubject: _analytics.ACTION_SUBJECT.TABLE, attributes: { totalColumnCount: totalColumnCount, totalRowCount: totalRowCount, inputMethod: _analytics.INPUT_METHOD.CLIPBOARD }, eventType: _analytics.EVENT_TYPE.TRACK })(tr); return tr; } return state.tr; }; var transformUnsupportedBlockCardToInline = exports.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 = []; (0, _utils.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 _model.Slice(_model.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 && (0, _utils.isSupportedInParent)(state, frag); };