UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

301 lines (294 loc) 13.7 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 _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"); 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; } var isSingleLine = exports.isSingleLine = function isSingleLine(text) { return !!text && text.trim().split('\n').length === 1; }; function htmlContainsSingleFile(html) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return !!html.match(/<img .*>/) && !(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'; } 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) { // 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```' */ 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'); } 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) { return Boolean(node && node.type.name === 'panel'); } function isSelectionInsidePanel(selection) { if (selection instanceof _state.NodeSelection && isPanelNode(selection.node)) { return selection.node; } var panel = selection.$from.doc.type.schema.nodes.panel; var panelPosition = (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)) { // 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; }; 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); };