UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

222 lines (214 loc) • 7.84 kB
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; // File has been copied to packages/editor/editor-plugin-ai/src/provider/markdown-transformer/utils/hyperlink.ts // If changes are made to this file, please make the same update in the linked file. import { isSafeUrl, linkify, normalizeUrl as normaliseLinkHref } from '@atlaskit/adf-schema'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '../analytics/types/enums'; import { shouldAutoLinkifyMatch } from './should-auto-linkify-tld'; import { mapSlice } from './slice'; // Regular expression for a windows filepath in the format <DRIVE LETTER>:\<folder name>\ // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp export var FILEPATH_REGEXP = /([a-zA-Z]:|\\)([^\/:*?<>"|]+\\)?([^\/:*?<>"| ]+(?=\s?))?/gim; // Don't linkify if starts with $ or { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp export var DONTLINKIFY_REGEXP = /^(\$|{)/; /** * Instance of class LinkMatcher are used in autoformatting in place of Regex. * Hence it has been made similar to regex with an exec method. * Extending it directly from class Regex was introducing some issues, thus that has been avoided. */ export var LinkMatcher = /*#__PURE__*/function () { function LinkMatcher() { _classCallCheck(this, LinkMatcher); } return _createClass(LinkMatcher, null, [{ key: "create", value: function create() { var LinkMatcherRegex = /*#__PURE__*/function () { function LinkMatcherRegex() { _classCallCheck(this, LinkMatcherRegex); } return _createClass(LinkMatcherRegex, [{ key: "exec", value: function exec(str) { var stringsBySpace = str.slice(0, str.length - 1).split(' '); var lastStringBeforeSpace = stringsBySpace[stringsBySpace.length - 1]; var isLastStringValid = lastStringBeforeSpace.length > 0; if (!str.endsWith(' ') || !isLastStringValid) { return null; } if (DONTLINKIFY_REGEXP.test(lastStringBeforeSpace)) { return null; } var links = linkify.match(lastStringBeforeSpace); if (!links || links.length === 0) { return null; } var lastMatch = links[links.length - 1]; var lastLink = links[links.length - 1]; lastLink.input = str.substring(lastMatch.index); lastLink.length = lastLink.lastIndex - lastLink.index + 1; lastLink.index = str.lastIndexOf(lastStringBeforeSpace) + lastMatch.index; return lastLink; } }]); }(); return new LinkMatcherRegex(); } }]); }(); /** * Adds protocol to url if needed. */ export function normalizeUrl(url) { if (!url) { return ''; } if (isSafeUrl(url)) { return url; } return normaliseLinkHref(url); } /** * Linkify content in a slice (eg. after a rich text paste) */ export function linkifyContent(schema) { return function (slice) { return mapSlice(slice, function (node, parent) { var isAllowedInParent = !parent || parent.type !== schema.nodes.codeBlock; var link = node.type.schema.marks.link; if (link === undefined) { throw new Error('Link not in schema - unable to linkify content'); } if (isAllowedInParent && node.isText && !link.isInSet(node.marks)) { var linkified = []; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion var text = node.text; var matches = findLinkMatches(text).filter(shouldAutoLinkifyMatch); var pos = 0; var filepaths = findFilepaths(text); matches.forEach(function (match) { if (isLinkInMatches(match.index, filepaths)) { return; } if (match.index > 0) { linkified.push(node.cut(pos, match.index)); } linkified.push(node.cut(match.index, match.lastIndex).mark(link.create({ href: normalizeUrl(match.url) }).addToSet(node.marks))); pos = match.lastIndex; }); if (pos < text.length) { linkified.push(node.cut(pos)); } return linkified; } return node; }); }; } export function getLinkDomain(url) { // Remove protocol and www., if either exists // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var withoutProtocol = url.toLowerCase().replace(/^(.*):\/\//, ''); // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var withoutWWW = withoutProtocol.replace(/^(www\.)/, ''); // Remove port, fragment, path, query string // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return withoutWWW.replace(/[:\/?#](.*)$/, ''); } export function isFromCurrentDomain(url) { if (!window || !window.location) { return false; } var currentDomain = window.location.hostname; var linkDomain = getLinkDomain(url); return currentDomain === linkDomain; } /** * Fetch linkify matches from text * @param text Input text from a node * @returns Array of linkify matches. Returns empty array if text is empty or no matches found; */ function findLinkMatches(text) { if (text === '') { return []; } return linkify.match(text) || []; } export var findFilepaths = function findFilepaths(text) { var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; // Creation of a copy of the RegExp is necessary as lastIndex is stored on it when we run .exec() // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp var localRegExp = new RegExp(FILEPATH_REGEXP); var match; var matchesList = []; var maxFilepathSize = 260; // Ignored via go/ees005 // eslint-disable-next-line no-cond-assign while ((match = localRegExp.exec(text)) !== null) { var start = match.index + offset; var end = localRegExp.lastIndex + offset; if (end - start > maxFilepathSize) { end = start + maxFilepathSize; } // We don't care about big strings of text that are pretending to be filepaths!! matchesList.push({ startIndex: start, endIndex: end }); } return matchesList; }; export var isLinkInMatches = function isLinkInMatches(linkStart, matchesList) { for (var i = 0; i < matchesList.length; i++) { if (linkStart >= matchesList[i].startIndex && linkStart < matchesList[i].endIndex) { return true; } } return false; }; export function getLinkCreationAnalyticsEvent(inputMethod, url) { return { action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: ACTION_SUBJECT_ID.LINK, attributes: { inputMethod: inputMethod, fromCurrentDomain: isFromCurrentDomain(url) }, eventType: EVENT_TYPE.TRACK, nonPrivacySafeAttributes: { linkDomain: getLinkDomain(url) } }; } export var canLinkBeCreatedInRange = function canLinkBeCreatedInRange(from, to) { return function (state) { if (!state.doc.rangeHasMark(from, to, state.schema.marks.link)) { var $from = state.doc.resolve(from); var $to = state.doc.resolve(to); var link = state.schema.marks.link; if ($from.parent === $to.parent && $from.parent.isTextblock) { if ($from.parent.type.allowsMarkType(link)) { var allowed = true; state.doc.nodesBetween(from, to, function (node) { allowed = allowed && !node.marks.some(function (m) { return m.type.excludes(link); }); return allowed; }); return allowed; } } } return false; }; };