@atlaskit/editor-plugin-paste
Version:
Paste plugin for @atlaskit/editor-core
324 lines (316 loc) • 15.1 kB
JavaScript
;
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);
};