fix: resolve TypeScript errors in frontend build
This commit is contained in:
21
node_modules/@tiptap/extension-link/LICENSE.md
generated
vendored
Normal file
21
node_modules/@tiptap/extension-link/LICENSE.md
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025, Tiptap GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
node_modules/@tiptap/extension-link/README.md
generated
vendored
Normal file
18
node_modules/@tiptap/extension-link/README.md
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# @tiptap/extension-link
|
||||
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-link)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-link)
|
||||
[](https://github.com/sponsors/ueberdosis)
|
||||
|
||||
## Introduction
|
||||
|
||||
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation can be found on the [Tiptap website](https://tiptap.dev).
|
||||
|
||||
## License
|
||||
|
||||
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
|
||||
475
node_modules/@tiptap/extension-link/dist/index.cjs
generated
vendored
Normal file
475
node_modules/@tiptap/extension-link/dist/index.cjs
generated
vendored
Normal file
@@ -0,0 +1,475 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
Link: () => Link,
|
||||
default: () => index_default,
|
||||
isAllowedUri: () => isAllowedUri,
|
||||
pasteRegex: () => pasteRegex
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/link.ts
|
||||
var import_core3 = require("@tiptap/core");
|
||||
var import_linkifyjs3 = require("linkifyjs");
|
||||
|
||||
// src/helpers/autolink.ts
|
||||
var import_core = require("@tiptap/core");
|
||||
var import_state = require("@tiptap/pm/state");
|
||||
var import_linkifyjs = require("linkifyjs");
|
||||
|
||||
// src/helpers/whitespace.ts
|
||||
var UNICODE_WHITESPACE_PATTERN = "[\0- \xA0\u1680\u180E\u2000-\u2029\u205F\u3000]";
|
||||
var UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN);
|
||||
var UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`);
|
||||
var UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, "g");
|
||||
|
||||
// src/helpers/autolink.ts
|
||||
function isValidLinkStructure(tokens) {
|
||||
if (tokens.length === 1) {
|
||||
return tokens[0].isLink;
|
||||
}
|
||||
if (tokens.length === 3 && tokens[1].isLink) {
|
||||
return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function autolink(options) {
|
||||
return new import_state.Plugin({
|
||||
key: new import_state.PluginKey("autolink"),
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc);
|
||||
const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink"));
|
||||
if (!docChanges || preventAutolink) {
|
||||
return;
|
||||
}
|
||||
const { tr } = newState;
|
||||
const transform = (0, import_core.combineTransactionSteps)(oldState.doc, [...transactions]);
|
||||
const changes = (0, import_core.getChangedRanges)(transform);
|
||||
changes.forEach(({ newRange }) => {
|
||||
const nodesInChangedRanges = (0, import_core.findChildrenInRange)(newState.doc, newRange, (node) => node.isTextblock);
|
||||
let textBlock;
|
||||
let textBeforeWhitespace;
|
||||
if (nodesInChangedRanges.length > 1) {
|
||||
textBlock = nodesInChangedRanges[0];
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
textBlock.pos + textBlock.node.nodeSize,
|
||||
void 0,
|
||||
" "
|
||||
);
|
||||
} else if (nodesInChangedRanges.length) {
|
||||
const endText = newState.doc.textBetween(newRange.from, newRange.to, " ", " ");
|
||||
if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {
|
||||
return;
|
||||
}
|
||||
textBlock = nodesInChangedRanges[0];
|
||||
textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, void 0, " ");
|
||||
}
|
||||
if (textBlock && textBeforeWhitespace) {
|
||||
const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean);
|
||||
if (wordsBeforeWhitespace.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
|
||||
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
|
||||
if (!lastWordBeforeSpace) {
|
||||
return false;
|
||||
}
|
||||
const linksBeforeSpace = (0, import_linkifyjs.tokenize)(lastWordBeforeSpace).map((t) => t.toObject(options.defaultProtocol));
|
||||
if (!isValidLinkStructure(linksBeforeSpace)) {
|
||||
return false;
|
||||
}
|
||||
linksBeforeSpace.filter((link) => link.isLink).map((link) => ({
|
||||
...link,
|
||||
from: lastWordAndBlockOffset + link.start + 1,
|
||||
to: lastWordAndBlockOffset + link.end + 1
|
||||
})).filter((link) => {
|
||||
if (!newState.schema.marks.code) {
|
||||
return true;
|
||||
}
|
||||
return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
|
||||
}).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => {
|
||||
if ((0, import_core.getMarksBetween)(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
|
||||
return;
|
||||
}
|
||||
tr.addMark(
|
||||
link.from,
|
||||
link.to,
|
||||
options.type.create({
|
||||
href: link.href
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!tr.steps.length) {
|
||||
return;
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// src/helpers/clickHandler.ts
|
||||
var import_core2 = require("@tiptap/core");
|
||||
var import_state2 = require("@tiptap/pm/state");
|
||||
function clickHandler(options) {
|
||||
return new import_state2.Plugin({
|
||||
key: new import_state2.PluginKey("handleClickLink"),
|
||||
props: {
|
||||
handleClick: (view, pos, event) => {
|
||||
var _a, _b;
|
||||
if (event.button !== 0) {
|
||||
return false;
|
||||
}
|
||||
if (!view.editable) {
|
||||
return false;
|
||||
}
|
||||
let link = null;
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
link = event.target;
|
||||
} else {
|
||||
const target = event.target;
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const root = options.editor.view.dom;
|
||||
link = target.closest("a");
|
||||
if (link && !root.contains(link)) {
|
||||
link = null;
|
||||
}
|
||||
}
|
||||
if (!link) {
|
||||
return false;
|
||||
}
|
||||
let handled = false;
|
||||
if (options.enableClickSelection) {
|
||||
const commandResult = options.editor.commands.extendMarkRange(options.type.name);
|
||||
handled = commandResult;
|
||||
}
|
||||
if (options.openOnClick) {
|
||||
const attrs = (0, import_core2.getAttributes)(view.state, options.type.name);
|
||||
const href = (_a = link.href) != null ? _a : attrs.href;
|
||||
const target = (_b = link.target) != null ? _b : attrs.target;
|
||||
if (href) {
|
||||
window.open(href, target);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// src/helpers/pasteHandler.ts
|
||||
var import_state3 = require("@tiptap/pm/state");
|
||||
var import_linkifyjs2 = require("linkifyjs");
|
||||
function pasteHandler(options) {
|
||||
return new import_state3.Plugin({
|
||||
key: new import_state3.PluginKey("handlePasteLink"),
|
||||
props: {
|
||||
handlePaste: (view, _event, slice) => {
|
||||
const { shouldAutoLink } = options;
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
const { empty } = selection;
|
||||
if (empty) {
|
||||
return false;
|
||||
}
|
||||
let textContent = "";
|
||||
slice.content.forEach((node) => {
|
||||
textContent += node.textContent;
|
||||
});
|
||||
const link = (0, import_linkifyjs2.find)(textContent, { defaultProtocol: options.defaultProtocol }).find(
|
||||
(item) => item.isLink && item.value === textContent
|
||||
);
|
||||
if (!textContent || !link || shouldAutoLink !== void 0 && !shouldAutoLink(link.value)) {
|
||||
return false;
|
||||
}
|
||||
return options.editor.commands.setMark(options.type, {
|
||||
href: link.href
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// src/link.ts
|
||||
var pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
|
||||
function isAllowedUri(uri, protocols) {
|
||||
const allowedProtocols = ["http", "https", "ftp", "ftps", "mailto", "tel", "callto", "sms", "cid", "xmpp"];
|
||||
if (protocols) {
|
||||
protocols.forEach((protocol) => {
|
||||
const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
|
||||
if (nextProtocol) {
|
||||
allowedProtocols.push(nextProtocol);
|
||||
}
|
||||
});
|
||||
}
|
||||
return !uri || uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, "").match(
|
||||
new RegExp(
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
`^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
|
||||
"i"
|
||||
)
|
||||
);
|
||||
}
|
||||
var Link = import_core3.Mark.create({
|
||||
name: "link",
|
||||
priority: 1e3,
|
||||
keepOnSplit: false,
|
||||
exitable: true,
|
||||
onCreate() {
|
||||
if (this.options.validate && !this.options.shouldAutoLink) {
|
||||
this.options.shouldAutoLink = this.options.validate;
|
||||
console.warn("The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.");
|
||||
}
|
||||
this.options.protocols.forEach((protocol) => {
|
||||
if (typeof protocol === "string") {
|
||||
(0, import_linkifyjs3.registerCustomProtocol)(protocol);
|
||||
return;
|
||||
}
|
||||
(0, import_linkifyjs3.registerCustomProtocol)(protocol.scheme, protocol.optionalSlashes);
|
||||
});
|
||||
},
|
||||
onDestroy() {
|
||||
(0, import_linkifyjs3.reset)();
|
||||
},
|
||||
inclusive() {
|
||||
return this.options.autolink;
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
openOnClick: true,
|
||||
enableClickSelection: false,
|
||||
linkOnPaste: true,
|
||||
autolink: true,
|
||||
protocols: [],
|
||||
defaultProtocol: "http",
|
||||
HTMLAttributes: {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer nofollow",
|
||||
class: null
|
||||
},
|
||||
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||
validate: (url) => !!url,
|
||||
shouldAutoLink: (url) => {
|
||||
const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
|
||||
const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url);
|
||||
if (hasProtocol || hasMaybeProtocol && !url.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
const urlWithoutUserinfo = url.includes("@") ? url.split("@").pop() : url;
|
||||
const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0];
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
|
||||
return false;
|
||||
}
|
||||
if (!/\./.test(hostname)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
href: {
|
||||
default: null,
|
||||
parseHTML(element) {
|
||||
return element.getAttribute("href");
|
||||
}
|
||||
},
|
||||
target: {
|
||||
default: this.options.HTMLAttributes.target
|
||||
},
|
||||
rel: {
|
||||
default: this.options.HTMLAttributes.rel
|
||||
},
|
||||
class: {
|
||||
default: this.options.HTMLAttributes.class
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "a[href]",
|
||||
getAttrs: (dom) => {
|
||||
const href = dom.getAttribute("href");
|
||||
if (!href || !this.options.isAllowedUri(href, {
|
||||
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
if (!this.options.isAllowedUri(HTMLAttributes.href, {
|
||||
defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return ["a", (0, import_core3.mergeAttributes)(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
|
||||
}
|
||||
return ["a", (0, import_core3.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
markdownTokenName: "link",
|
||||
parseMarkdown: (token, helpers) => {
|
||||
return helpers.applyMark("link", helpers.parseInline(token.tokens || []), {
|
||||
href: token.href,
|
||||
title: token.title || null
|
||||
});
|
||||
},
|
||||
renderMarkdown: (node, h) => {
|
||||
var _a, _b, _c, _d;
|
||||
const href = (_b = (_a = node.attrs) == null ? void 0 : _a.href) != null ? _b : "";
|
||||
const title = (_d = (_c = node.attrs) == null ? void 0 : _c.title) != null ? _d : "";
|
||||
const text = h.renderChildren(node);
|
||||
return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`;
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setLink: (attributes) => ({ chain }) => {
|
||||
const { href } = attributes;
|
||||
if (!this.options.isAllowedUri(href, {
|
||||
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run();
|
||||
},
|
||||
toggleLink: (attributes) => ({ chain }) => {
|
||||
const { href } = attributes || {};
|
||||
if (href && !this.options.isAllowedUri(href, {
|
||||
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return chain().toggleMark(this.name, attributes, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
|
||||
},
|
||||
unsetLink: () => ({ chain }) => {
|
||||
return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
|
||||
}
|
||||
};
|
||||
},
|
||||
addPasteRules() {
|
||||
return [
|
||||
(0, import_core3.markPasteRule)({
|
||||
find: (text) => {
|
||||
const foundLinks = [];
|
||||
if (text) {
|
||||
const { protocols, defaultProtocol } = this.options;
|
||||
const links = (0, import_linkifyjs3.find)(text).filter(
|
||||
(item) => item.isLink && this.options.isAllowedUri(item.value, {
|
||||
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol
|
||||
})
|
||||
);
|
||||
if (links.length) {
|
||||
links.forEach((link) => {
|
||||
if (!this.options.shouldAutoLink(link.value)) {
|
||||
return;
|
||||
}
|
||||
foundLinks.push({
|
||||
text: link.value,
|
||||
data: {
|
||||
href: link.href
|
||||
},
|
||||
index: link.start
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return foundLinks;
|
||||
},
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
var _a;
|
||||
return {
|
||||
href: (_a = match.data) == null ? void 0 : _a.href
|
||||
};
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
const plugins = [];
|
||||
const { protocols, defaultProtocol } = this.options;
|
||||
if (this.options.autolink) {
|
||||
plugins.push(
|
||||
autolink({
|
||||
type: this.type,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
validate: (url) => this.options.isAllowedUri(url, {
|
||||
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol
|
||||
}),
|
||||
shouldAutoLink: this.options.shouldAutoLink
|
||||
})
|
||||
);
|
||||
}
|
||||
plugins.push(
|
||||
clickHandler({
|
||||
type: this.type,
|
||||
editor: this.editor,
|
||||
openOnClick: this.options.openOnClick === "whenNotEditable" ? true : this.options.openOnClick,
|
||||
enableClickSelection: this.options.enableClickSelection
|
||||
})
|
||||
);
|
||||
if (this.options.linkOnPaste) {
|
||||
plugins.push(
|
||||
pasteHandler({
|
||||
editor: this.editor,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
type: this.type,
|
||||
shouldAutoLink: this.options.shouldAutoLink
|
||||
})
|
||||
);
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
});
|
||||
|
||||
// src/index.ts
|
||||
var index_default = Link;
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Link,
|
||||
isAllowedUri,
|
||||
pasteRegex
|
||||
});
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
1
node_modules/@tiptap/extension-link/dist/index.cjs.map
generated
vendored
Normal file
1
node_modules/@tiptap/extension-link/dist/index.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
151
node_modules/@tiptap/extension-link/dist/index.d.cts
generated
vendored
Normal file
151
node_modules/@tiptap/extension-link/dist/index.d.cts
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Mark } from '@tiptap/core';
|
||||
|
||||
interface LinkProtocolOptions {
|
||||
/**
|
||||
* The protocol scheme to be registered.
|
||||
* @default '''
|
||||
* @example 'ftp'
|
||||
* @example 'git'
|
||||
*/
|
||||
scheme: string;
|
||||
/**
|
||||
* If enabled, it allows optional slashes after the protocol.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
optionalSlashes?: boolean;
|
||||
}
|
||||
declare const pasteRegex: RegExp;
|
||||
/**
|
||||
* @deprecated The default behavior is now to open links when the editor is not editable.
|
||||
*/
|
||||
type DeprecatedOpenWhenNotEditable = 'whenNotEditable';
|
||||
interface LinkOptions {
|
||||
/**
|
||||
* If enabled, the extension will automatically add links as you type.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
autolink: boolean;
|
||||
/**
|
||||
* An array of custom protocols to be registered with linkifyjs.
|
||||
* @default []
|
||||
* @example ['ftp', 'git']
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
/**
|
||||
* Default protocol to use when no protocol is specified.
|
||||
* @default 'http'
|
||||
*/
|
||||
defaultProtocol: string;
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable;
|
||||
/**
|
||||
* If enabled, the link will be selected when clicked.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
enableClickSelection: boolean;
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
linkOnPaste: boolean;
|
||||
/**
|
||||
* HTML attributes to add to the link element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>;
|
||||
/**
|
||||
* @deprecated Use the `shouldAutoLink` option instead.
|
||||
* A validation function that modifies link verification for the auto linker.
|
||||
* @param url - The url to be validated.
|
||||
* @returns - True if the url is valid, false otherwise.
|
||||
*/
|
||||
validate: (url: string) => boolean;
|
||||
/**
|
||||
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
||||
* Only modify this if you know what you're doing.
|
||||
*
|
||||
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
||||
*
|
||||
* @example
|
||||
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
||||
* return url.startsWith('./') || defaultValidate(url)
|
||||
* }
|
||||
*/
|
||||
isAllowedUri: (
|
||||
/**
|
||||
* The URL to be validated.
|
||||
*/
|
||||
url: string, ctx: {
|
||||
/**
|
||||
* The default validation function.
|
||||
*/
|
||||
defaultValidate: (url: string) => boolean;
|
||||
/**
|
||||
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
/**
|
||||
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
||||
*/
|
||||
defaultProtocol: string;
|
||||
}) => boolean;
|
||||
/**
|
||||
* Determines whether a valid link should be automatically linked in the content.
|
||||
*
|
||||
* @param {string} url - The URL that has already been validated.
|
||||
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
||||
*/
|
||||
shouldAutoLink: (url: string) => boolean;
|
||||
}
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
link: {
|
||||
/**
|
||||
* Set a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
setLink: (attributes: {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
title?: string | null;
|
||||
}) => ReturnType;
|
||||
/**
|
||||
* Toggle a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
toggleLink: (attributes?: {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
title?: string | null;
|
||||
}) => ReturnType;
|
||||
/**
|
||||
* Unset a link mark
|
||||
* @example editor.commands.unsetLink()
|
||||
*/
|
||||
unsetLink: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
declare function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']): true | RegExpMatchArray | null;
|
||||
/**
|
||||
* This extension allows you to create links.
|
||||
* @see https://www.tiptap.dev/api/marks/link
|
||||
*/
|
||||
declare const Link: Mark<LinkOptions, any>;
|
||||
|
||||
export { Link, type LinkOptions, type LinkProtocolOptions, Link as default, isAllowedUri, pasteRegex };
|
||||
151
node_modules/@tiptap/extension-link/dist/index.d.ts
generated
vendored
Normal file
151
node_modules/@tiptap/extension-link/dist/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Mark } from '@tiptap/core';
|
||||
|
||||
interface LinkProtocolOptions {
|
||||
/**
|
||||
* The protocol scheme to be registered.
|
||||
* @default '''
|
||||
* @example 'ftp'
|
||||
* @example 'git'
|
||||
*/
|
||||
scheme: string;
|
||||
/**
|
||||
* If enabled, it allows optional slashes after the protocol.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
optionalSlashes?: boolean;
|
||||
}
|
||||
declare const pasteRegex: RegExp;
|
||||
/**
|
||||
* @deprecated The default behavior is now to open links when the editor is not editable.
|
||||
*/
|
||||
type DeprecatedOpenWhenNotEditable = 'whenNotEditable';
|
||||
interface LinkOptions {
|
||||
/**
|
||||
* If enabled, the extension will automatically add links as you type.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
autolink: boolean;
|
||||
/**
|
||||
* An array of custom protocols to be registered with linkifyjs.
|
||||
* @default []
|
||||
* @example ['ftp', 'git']
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
/**
|
||||
* Default protocol to use when no protocol is specified.
|
||||
* @default 'http'
|
||||
*/
|
||||
defaultProtocol: string;
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable;
|
||||
/**
|
||||
* If enabled, the link will be selected when clicked.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
enableClickSelection: boolean;
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
linkOnPaste: boolean;
|
||||
/**
|
||||
* HTML attributes to add to the link element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>;
|
||||
/**
|
||||
* @deprecated Use the `shouldAutoLink` option instead.
|
||||
* A validation function that modifies link verification for the auto linker.
|
||||
* @param url - The url to be validated.
|
||||
* @returns - True if the url is valid, false otherwise.
|
||||
*/
|
||||
validate: (url: string) => boolean;
|
||||
/**
|
||||
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
||||
* Only modify this if you know what you're doing.
|
||||
*
|
||||
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
||||
*
|
||||
* @example
|
||||
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
||||
* return url.startsWith('./') || defaultValidate(url)
|
||||
* }
|
||||
*/
|
||||
isAllowedUri: (
|
||||
/**
|
||||
* The URL to be validated.
|
||||
*/
|
||||
url: string, ctx: {
|
||||
/**
|
||||
* The default validation function.
|
||||
*/
|
||||
defaultValidate: (url: string) => boolean;
|
||||
/**
|
||||
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
/**
|
||||
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
||||
*/
|
||||
defaultProtocol: string;
|
||||
}) => boolean;
|
||||
/**
|
||||
* Determines whether a valid link should be automatically linked in the content.
|
||||
*
|
||||
* @param {string} url - The URL that has already been validated.
|
||||
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
||||
*/
|
||||
shouldAutoLink: (url: string) => boolean;
|
||||
}
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
link: {
|
||||
/**
|
||||
* Set a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
setLink: (attributes: {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
title?: string | null;
|
||||
}) => ReturnType;
|
||||
/**
|
||||
* Toggle a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
toggleLink: (attributes?: {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
title?: string | null;
|
||||
}) => ReturnType;
|
||||
/**
|
||||
* Unset a link mark
|
||||
* @example editor.commands.unsetLink()
|
||||
*/
|
||||
unsetLink: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
declare function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']): true | RegExpMatchArray | null;
|
||||
/**
|
||||
* This extension allows you to create links.
|
||||
* @see https://www.tiptap.dev/api/marks/link
|
||||
*/
|
||||
declare const Link: Mark<LinkOptions, any>;
|
||||
|
||||
export { Link, type LinkOptions, type LinkProtocolOptions, Link as default, isAllowedUri, pasteRegex };
|
||||
446
node_modules/@tiptap/extension-link/dist/index.js
generated
vendored
Normal file
446
node_modules/@tiptap/extension-link/dist/index.js
generated
vendored
Normal file
@@ -0,0 +1,446 @@
|
||||
// src/link.ts
|
||||
import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
|
||||
import { find as find2, registerCustomProtocol, reset } from "linkifyjs";
|
||||
|
||||
// src/helpers/autolink.ts
|
||||
import { combineTransactionSteps, findChildrenInRange, getChangedRanges, getMarksBetween } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { tokenize } from "linkifyjs";
|
||||
|
||||
// src/helpers/whitespace.ts
|
||||
var UNICODE_WHITESPACE_PATTERN = "[\0- \xA0\u1680\u180E\u2000-\u2029\u205F\u3000]";
|
||||
var UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN);
|
||||
var UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`);
|
||||
var UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, "g");
|
||||
|
||||
// src/helpers/autolink.ts
|
||||
function isValidLinkStructure(tokens) {
|
||||
if (tokens.length === 1) {
|
||||
return tokens[0].isLink;
|
||||
}
|
||||
if (tokens.length === 3 && tokens[1].isLink) {
|
||||
return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function autolink(options) {
|
||||
return new Plugin({
|
||||
key: new PluginKey("autolink"),
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc);
|
||||
const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink"));
|
||||
if (!docChanges || preventAutolink) {
|
||||
return;
|
||||
}
|
||||
const { tr } = newState;
|
||||
const transform = combineTransactionSteps(oldState.doc, [...transactions]);
|
||||
const changes = getChangedRanges(transform);
|
||||
changes.forEach(({ newRange }) => {
|
||||
const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, (node) => node.isTextblock);
|
||||
let textBlock;
|
||||
let textBeforeWhitespace;
|
||||
if (nodesInChangedRanges.length > 1) {
|
||||
textBlock = nodesInChangedRanges[0];
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
textBlock.pos + textBlock.node.nodeSize,
|
||||
void 0,
|
||||
" "
|
||||
);
|
||||
} else if (nodesInChangedRanges.length) {
|
||||
const endText = newState.doc.textBetween(newRange.from, newRange.to, " ", " ");
|
||||
if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {
|
||||
return;
|
||||
}
|
||||
textBlock = nodesInChangedRanges[0];
|
||||
textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, void 0, " ");
|
||||
}
|
||||
if (textBlock && textBeforeWhitespace) {
|
||||
const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean);
|
||||
if (wordsBeforeWhitespace.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
|
||||
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
|
||||
if (!lastWordBeforeSpace) {
|
||||
return false;
|
||||
}
|
||||
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map((t) => t.toObject(options.defaultProtocol));
|
||||
if (!isValidLinkStructure(linksBeforeSpace)) {
|
||||
return false;
|
||||
}
|
||||
linksBeforeSpace.filter((link) => link.isLink).map((link) => ({
|
||||
...link,
|
||||
from: lastWordAndBlockOffset + link.start + 1,
|
||||
to: lastWordAndBlockOffset + link.end + 1
|
||||
})).filter((link) => {
|
||||
if (!newState.schema.marks.code) {
|
||||
return true;
|
||||
}
|
||||
return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
|
||||
}).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => {
|
||||
if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
|
||||
return;
|
||||
}
|
||||
tr.addMark(
|
||||
link.from,
|
||||
link.to,
|
||||
options.type.create({
|
||||
href: link.href
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!tr.steps.length) {
|
||||
return;
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// src/helpers/clickHandler.ts
|
||||
import { getAttributes } from "@tiptap/core";
|
||||
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
|
||||
function clickHandler(options) {
|
||||
return new Plugin2({
|
||||
key: new PluginKey2("handleClickLink"),
|
||||
props: {
|
||||
handleClick: (view, pos, event) => {
|
||||
var _a, _b;
|
||||
if (event.button !== 0) {
|
||||
return false;
|
||||
}
|
||||
if (!view.editable) {
|
||||
return false;
|
||||
}
|
||||
let link = null;
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
link = event.target;
|
||||
} else {
|
||||
const target = event.target;
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const root = options.editor.view.dom;
|
||||
link = target.closest("a");
|
||||
if (link && !root.contains(link)) {
|
||||
link = null;
|
||||
}
|
||||
}
|
||||
if (!link) {
|
||||
return false;
|
||||
}
|
||||
let handled = false;
|
||||
if (options.enableClickSelection) {
|
||||
const commandResult = options.editor.commands.extendMarkRange(options.type.name);
|
||||
handled = commandResult;
|
||||
}
|
||||
if (options.openOnClick) {
|
||||
const attrs = getAttributes(view.state, options.type.name);
|
||||
const href = (_a = link.href) != null ? _a : attrs.href;
|
||||
const target = (_b = link.target) != null ? _b : attrs.target;
|
||||
if (href) {
|
||||
window.open(href, target);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// src/helpers/pasteHandler.ts
|
||||
import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@tiptap/pm/state";
|
||||
import { find } from "linkifyjs";
|
||||
function pasteHandler(options) {
|
||||
return new Plugin3({
|
||||
key: new PluginKey3("handlePasteLink"),
|
||||
props: {
|
||||
handlePaste: (view, _event, slice) => {
|
||||
const { shouldAutoLink } = options;
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
const { empty } = selection;
|
||||
if (empty) {
|
||||
return false;
|
||||
}
|
||||
let textContent = "";
|
||||
slice.content.forEach((node) => {
|
||||
textContent += node.textContent;
|
||||
});
|
||||
const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(
|
||||
(item) => item.isLink && item.value === textContent
|
||||
);
|
||||
if (!textContent || !link || shouldAutoLink !== void 0 && !shouldAutoLink(link.value)) {
|
||||
return false;
|
||||
}
|
||||
return options.editor.commands.setMark(options.type, {
|
||||
href: link.href
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// src/link.ts
|
||||
var pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
|
||||
function isAllowedUri(uri, protocols) {
|
||||
const allowedProtocols = ["http", "https", "ftp", "ftps", "mailto", "tel", "callto", "sms", "cid", "xmpp"];
|
||||
if (protocols) {
|
||||
protocols.forEach((protocol) => {
|
||||
const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
|
||||
if (nextProtocol) {
|
||||
allowedProtocols.push(nextProtocol);
|
||||
}
|
||||
});
|
||||
}
|
||||
return !uri || uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, "").match(
|
||||
new RegExp(
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
`^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
|
||||
"i"
|
||||
)
|
||||
);
|
||||
}
|
||||
var Link = Mark.create({
|
||||
name: "link",
|
||||
priority: 1e3,
|
||||
keepOnSplit: false,
|
||||
exitable: true,
|
||||
onCreate() {
|
||||
if (this.options.validate && !this.options.shouldAutoLink) {
|
||||
this.options.shouldAutoLink = this.options.validate;
|
||||
console.warn("The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.");
|
||||
}
|
||||
this.options.protocols.forEach((protocol) => {
|
||||
if (typeof protocol === "string") {
|
||||
registerCustomProtocol(protocol);
|
||||
return;
|
||||
}
|
||||
registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
|
||||
});
|
||||
},
|
||||
onDestroy() {
|
||||
reset();
|
||||
},
|
||||
inclusive() {
|
||||
return this.options.autolink;
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
openOnClick: true,
|
||||
enableClickSelection: false,
|
||||
linkOnPaste: true,
|
||||
autolink: true,
|
||||
protocols: [],
|
||||
defaultProtocol: "http",
|
||||
HTMLAttributes: {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer nofollow",
|
||||
class: null
|
||||
},
|
||||
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||
validate: (url) => !!url,
|
||||
shouldAutoLink: (url) => {
|
||||
const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
|
||||
const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url);
|
||||
if (hasProtocol || hasMaybeProtocol && !url.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
const urlWithoutUserinfo = url.includes("@") ? url.split("@").pop() : url;
|
||||
const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0];
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
|
||||
return false;
|
||||
}
|
||||
if (!/\./.test(hostname)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
href: {
|
||||
default: null,
|
||||
parseHTML(element) {
|
||||
return element.getAttribute("href");
|
||||
}
|
||||
},
|
||||
target: {
|
||||
default: this.options.HTMLAttributes.target
|
||||
},
|
||||
rel: {
|
||||
default: this.options.HTMLAttributes.rel
|
||||
},
|
||||
class: {
|
||||
default: this.options.HTMLAttributes.class
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "a[href]",
|
||||
getAttrs: (dom) => {
|
||||
const href = dom.getAttribute("href");
|
||||
if (!href || !this.options.isAllowedUri(href, {
|
||||
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
if (!this.options.isAllowedUri(HTMLAttributes.href, {
|
||||
defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return ["a", mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
|
||||
}
|
||||
return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
markdownTokenName: "link",
|
||||
parseMarkdown: (token, helpers) => {
|
||||
return helpers.applyMark("link", helpers.parseInline(token.tokens || []), {
|
||||
href: token.href,
|
||||
title: token.title || null
|
||||
});
|
||||
},
|
||||
renderMarkdown: (node, h) => {
|
||||
var _a, _b, _c, _d;
|
||||
const href = (_b = (_a = node.attrs) == null ? void 0 : _a.href) != null ? _b : "";
|
||||
const title = (_d = (_c = node.attrs) == null ? void 0 : _c.title) != null ? _d : "";
|
||||
const text = h.renderChildren(node);
|
||||
return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`;
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setLink: (attributes) => ({ chain }) => {
|
||||
const { href } = attributes;
|
||||
if (!this.options.isAllowedUri(href, {
|
||||
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run();
|
||||
},
|
||||
toggleLink: (attributes) => ({ chain }) => {
|
||||
const { href } = attributes || {};
|
||||
if (href && !this.options.isAllowedUri(href, {
|
||||
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return chain().toggleMark(this.name, attributes, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
|
||||
},
|
||||
unsetLink: () => ({ chain }) => {
|
||||
return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
|
||||
}
|
||||
};
|
||||
},
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule({
|
||||
find: (text) => {
|
||||
const foundLinks = [];
|
||||
if (text) {
|
||||
const { protocols, defaultProtocol } = this.options;
|
||||
const links = find2(text).filter(
|
||||
(item) => item.isLink && this.options.isAllowedUri(item.value, {
|
||||
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol
|
||||
})
|
||||
);
|
||||
if (links.length) {
|
||||
links.forEach((link) => {
|
||||
if (!this.options.shouldAutoLink(link.value)) {
|
||||
return;
|
||||
}
|
||||
foundLinks.push({
|
||||
text: link.value,
|
||||
data: {
|
||||
href: link.href
|
||||
},
|
||||
index: link.start
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return foundLinks;
|
||||
},
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
var _a;
|
||||
return {
|
||||
href: (_a = match.data) == null ? void 0 : _a.href
|
||||
};
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
const plugins = [];
|
||||
const { protocols, defaultProtocol } = this.options;
|
||||
if (this.options.autolink) {
|
||||
plugins.push(
|
||||
autolink({
|
||||
type: this.type,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
validate: (url) => this.options.isAllowedUri(url, {
|
||||
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol
|
||||
}),
|
||||
shouldAutoLink: this.options.shouldAutoLink
|
||||
})
|
||||
);
|
||||
}
|
||||
plugins.push(
|
||||
clickHandler({
|
||||
type: this.type,
|
||||
editor: this.editor,
|
||||
openOnClick: this.options.openOnClick === "whenNotEditable" ? true : this.options.openOnClick,
|
||||
enableClickSelection: this.options.enableClickSelection
|
||||
})
|
||||
);
|
||||
if (this.options.linkOnPaste) {
|
||||
plugins.push(
|
||||
pasteHandler({
|
||||
editor: this.editor,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
type: this.type,
|
||||
shouldAutoLink: this.options.shouldAutoLink
|
||||
})
|
||||
);
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
});
|
||||
|
||||
// src/index.ts
|
||||
var index_default = Link;
|
||||
export {
|
||||
Link,
|
||||
index_default as default,
|
||||
isAllowedUri,
|
||||
pasteRegex
|
||||
};
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
node_modules/@tiptap/extension-link/dist/index.js.map
generated
vendored
Normal file
1
node_modules/@tiptap/extension-link/dist/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
53
node_modules/@tiptap/extension-link/package.json
generated
vendored
Normal file
53
node_modules/@tiptap/extension-link/package.json
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@tiptap/extension-link",
|
||||
"description": "link extension for tiptap",
|
||||
"version": "3.21.0",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
"tiptap extension"
|
||||
],
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
"import": "./dist/index.d.ts",
|
||||
"require": "./dist/index.d.cts"
|
||||
},
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tiptap/core": "^3.21.0",
|
||||
"@tiptap/pm": "^3.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.21.0",
|
||||
"@tiptap/pm": "^3.21.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
"directory": "packages/extension-link"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
|
||||
}
|
||||
}
|
||||
159
node_modules/@tiptap/extension-link/src/helpers/autolink.ts
generated
vendored
Normal file
159
node_modules/@tiptap/extension-link/src/helpers/autolink.ts
generated
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { NodeWithPos } from '@tiptap/core'
|
||||
import { combineTransactionSteps, findChildrenInRange, getChangedRanges, getMarksBetween } from '@tiptap/core'
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import type { MultiToken } from 'linkifyjs'
|
||||
import { tokenize } from 'linkifyjs'
|
||||
|
||||
import { UNICODE_WHITESPACE_REGEX, UNICODE_WHITESPACE_REGEX_END } from './whitespace.js'
|
||||
|
||||
/**
|
||||
* Check if the provided tokens form a valid link structure, which can either be a single link token
|
||||
* or a link token surrounded by parentheses or square brackets.
|
||||
*
|
||||
* This ensures that only complete and valid text is hyperlinked, preventing cases where a valid
|
||||
* top-level domain (TLD) is immediately followed by an invalid character, like a number. For
|
||||
* example, with the `find` method from Linkify, entering `example.com1` would result in
|
||||
* `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`
|
||||
* method, we can perform more comprehensive validation on the input text.
|
||||
*/
|
||||
function isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>) {
|
||||
if (tokens.length === 1) {
|
||||
return tokens[0].isLink
|
||||
}
|
||||
|
||||
if (tokens.length === 3 && tokens[1].isLink) {
|
||||
return ['()', '[]'].includes(tokens[0].value + tokens[2].value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type AutolinkOptions = {
|
||||
type: MarkType
|
||||
defaultProtocol: string
|
||||
validate: (url: string) => boolean
|
||||
shouldAutoLink: (url: string) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin allows you to automatically add links to your editor.
|
||||
* @param options The plugin options
|
||||
* @returns The plugin instance
|
||||
*/
|
||||
export function autolink(options: AutolinkOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('autolink'),
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
/**
|
||||
* Does the transaction change the document?
|
||||
*/
|
||||
const docChanges = transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)
|
||||
|
||||
/**
|
||||
* Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.
|
||||
*/
|
||||
const preventAutolink = transactions.some(transaction => transaction.getMeta('preventAutolink'))
|
||||
|
||||
/**
|
||||
* Prevent autolink if the transaction is not a document change
|
||||
* or if the transaction has the meta `preventAutolink`.
|
||||
*/
|
||||
if (!docChanges || preventAutolink) {
|
||||
return
|
||||
}
|
||||
|
||||
const { tr } = newState
|
||||
const transform = combineTransactionSteps(oldState.doc, [...transactions])
|
||||
const changes = getChangedRanges(transform)
|
||||
|
||||
changes.forEach(({ newRange }) => {
|
||||
// Now let’s see if we can add new links.
|
||||
const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
|
||||
|
||||
let textBlock: NodeWithPos | undefined
|
||||
let textBeforeWhitespace: string | undefined
|
||||
|
||||
if (nodesInChangedRanges.length > 1) {
|
||||
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
||||
textBlock = nodesInChangedRanges[0]
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
textBlock.pos + textBlock.node.nodeSize,
|
||||
undefined,
|
||||
' ',
|
||||
)
|
||||
} else if (nodesInChangedRanges.length) {
|
||||
const endText = newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ')
|
||||
if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {
|
||||
return
|
||||
}
|
||||
textBlock = nodesInChangedRanges[0]
|
||||
textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, undefined, ' ')
|
||||
}
|
||||
|
||||
if (textBlock && textBeforeWhitespace) {
|
||||
const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean)
|
||||
|
||||
if (wordsBeforeWhitespace.length <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]
|
||||
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)
|
||||
|
||||
if (!lastWordBeforeSpace) {
|
||||
return false
|
||||
}
|
||||
|
||||
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))
|
||||
|
||||
if (!isValidLinkStructure(linksBeforeSpace)) {
|
||||
return false
|
||||
}
|
||||
|
||||
linksBeforeSpace
|
||||
.filter(link => link.isLink)
|
||||
// Calculate link position.
|
||||
.map(link => ({
|
||||
...link,
|
||||
from: lastWordAndBlockOffset + link.start + 1,
|
||||
to: lastWordAndBlockOffset + link.end + 1,
|
||||
}))
|
||||
// ignore link inside code mark
|
||||
.filter(link => {
|
||||
if (!newState.schema.marks.code) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code)
|
||||
})
|
||||
// validate link
|
||||
.filter(link => options.validate(link.value))
|
||||
// check whether should autolink
|
||||
.filter(link => options.shouldAutoLink(link.value))
|
||||
// Add link mark.
|
||||
.forEach(link => {
|
||||
if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {
|
||||
return
|
||||
}
|
||||
|
||||
tr.addMark(
|
||||
link.from,
|
||||
link.to,
|
||||
options.type.create({
|
||||
href: link.href,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!tr.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
})
|
||||
}
|
||||
73
node_modules/@tiptap/extension-link/src/helpers/clickHandler.ts
generated
vendored
Normal file
73
node_modules/@tiptap/extension-link/src/helpers/clickHandler.ts
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { getAttributes } from '@tiptap/core'
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
type ClickHandlerOptions = {
|
||||
type: MarkType
|
||||
editor: Editor
|
||||
openOnClick?: boolean
|
||||
enableClickSelection?: boolean
|
||||
}
|
||||
|
||||
export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('handleClickLink'),
|
||||
props: {
|
||||
handleClick: (view, pos, event) => {
|
||||
if (event.button !== 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!view.editable) {
|
||||
return false
|
||||
}
|
||||
|
||||
let link: HTMLAnchorElement | null = null
|
||||
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
link = event.target
|
||||
} else {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (!target) {
|
||||
return false
|
||||
}
|
||||
|
||||
const root = options.editor.view.dom
|
||||
|
||||
// Tntentionally limit the lookup to the editor root.
|
||||
// Using tag names like DIV as boundaries breaks with custom NodeViews,
|
||||
link = target.closest<HTMLAnchorElement>('a')
|
||||
|
||||
if (link && !root.contains(link)) {
|
||||
link = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
return false
|
||||
}
|
||||
|
||||
let handled = false
|
||||
|
||||
if (options.enableClickSelection) {
|
||||
const commandResult = options.editor.commands.extendMarkRange(options.type.name)
|
||||
handled = commandResult
|
||||
}
|
||||
|
||||
if (options.openOnClick) {
|
||||
const attrs = getAttributes(view.state, options.type.name)
|
||||
const href = link.href ?? attrs.href
|
||||
const target = link.target ?? attrs.target
|
||||
|
||||
if (href) {
|
||||
window.open(href, target)
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
|
||||
return handled
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
49
node_modules/@tiptap/extension-link/src/helpers/pasteHandler.ts
generated
vendored
Normal file
49
node_modules/@tiptap/extension-link/src/helpers/pasteHandler.ts
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { find } from 'linkifyjs'
|
||||
|
||||
import type { LinkOptions } from '../link.js'
|
||||
|
||||
type PasteHandlerOptions = {
|
||||
editor: Editor
|
||||
defaultProtocol: string
|
||||
type: MarkType
|
||||
shouldAutoLink?: LinkOptions['shouldAutoLink']
|
||||
}
|
||||
|
||||
export function pasteHandler(options: PasteHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('handlePasteLink'),
|
||||
props: {
|
||||
handlePaste: (view, _event, slice) => {
|
||||
const { shouldAutoLink } = options
|
||||
const { state } = view
|
||||
const { selection } = state
|
||||
const { empty } = selection
|
||||
|
||||
if (empty) {
|
||||
return false
|
||||
}
|
||||
|
||||
let textContent = ''
|
||||
|
||||
slice.content.forEach(node => {
|
||||
textContent += node.textContent
|
||||
})
|
||||
|
||||
const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(
|
||||
item => item.isLink && item.value === textContent,
|
||||
)
|
||||
|
||||
if (!textContent || !link || (shouldAutoLink !== undefined && !shouldAutoLink(link.value))) {
|
||||
return false
|
||||
}
|
||||
|
||||
return options.editor.commands.setMark(options.type, {
|
||||
href: link.href,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
7
node_modules/@tiptap/extension-link/src/helpers/whitespace.ts
generated
vendored
Normal file
7
node_modules/@tiptap/extension-link/src/helpers/whitespace.ts
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// From DOMPurify
|
||||
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts
|
||||
export const UNICODE_WHITESPACE_PATTERN = '[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]'
|
||||
|
||||
export const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN)
|
||||
export const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`)
|
||||
export const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, 'g')
|
||||
5
node_modules/@tiptap/extension-link/src/index.ts
generated
vendored
Normal file
5
node_modules/@tiptap/extension-link/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Link } from './link.js'
|
||||
|
||||
export * from './link.js'
|
||||
|
||||
export default Link
|
||||
489
node_modules/@tiptap/extension-link/src/link.ts
generated
vendored
Normal file
489
node_modules/@tiptap/extension-link/src/link.ts
generated
vendored
Normal file
@@ -0,0 +1,489 @@
|
||||
import type { PasteRuleMatch } from '@tiptap/core'
|
||||
import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'
|
||||
import type { Plugin } from '@tiptap/pm/state'
|
||||
import { find, registerCustomProtocol, reset } from 'linkifyjs'
|
||||
|
||||
import { autolink } from './helpers/autolink.js'
|
||||
import { clickHandler } from './helpers/clickHandler.js'
|
||||
import { pasteHandler } from './helpers/pasteHandler.js'
|
||||
import { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'
|
||||
|
||||
export interface LinkProtocolOptions {
|
||||
/**
|
||||
* The protocol scheme to be registered.
|
||||
* @default '''
|
||||
* @example 'ftp'
|
||||
* @example 'git'
|
||||
*/
|
||||
scheme: string
|
||||
|
||||
/**
|
||||
* If enabled, it allows optional slashes after the protocol.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
optionalSlashes?: boolean
|
||||
}
|
||||
|
||||
export const pasteRegex =
|
||||
/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi
|
||||
|
||||
/**
|
||||
* @deprecated The default behavior is now to open links when the editor is not editable.
|
||||
*/
|
||||
type DeprecatedOpenWhenNotEditable = 'whenNotEditable'
|
||||
|
||||
export interface LinkOptions {
|
||||
/**
|
||||
* If enabled, the extension will automatically add links as you type.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
autolink: boolean
|
||||
|
||||
/**
|
||||
* An array of custom protocols to be registered with linkifyjs.
|
||||
* @default []
|
||||
* @example ['ftp', 'git']
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>
|
||||
|
||||
/**
|
||||
* Default protocol to use when no protocol is specified.
|
||||
* @default 'http'
|
||||
*/
|
||||
defaultProtocol: string
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable
|
||||
/**
|
||||
* If enabled, the link will be selected when clicked.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
enableClickSelection: boolean
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
linkOnPaste: boolean
|
||||
|
||||
/**
|
||||
* HTML attributes to add to the link element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>
|
||||
|
||||
/**
|
||||
* @deprecated Use the `shouldAutoLink` option instead.
|
||||
* A validation function that modifies link verification for the auto linker.
|
||||
* @param url - The url to be validated.
|
||||
* @returns - True if the url is valid, false otherwise.
|
||||
*/
|
||||
validate: (url: string) => boolean
|
||||
|
||||
/**
|
||||
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
||||
* Only modify this if you know what you're doing.
|
||||
*
|
||||
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
||||
*
|
||||
* @example
|
||||
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
||||
* return url.startsWith('./') || defaultValidate(url)
|
||||
* }
|
||||
*/
|
||||
isAllowedUri: (
|
||||
/**
|
||||
* The URL to be validated.
|
||||
*/
|
||||
url: string,
|
||||
ctx: {
|
||||
/**
|
||||
* The default validation function.
|
||||
*/
|
||||
defaultValidate: (url: string) => boolean
|
||||
/**
|
||||
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>
|
||||
/**
|
||||
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
||||
*/
|
||||
defaultProtocol: string
|
||||
},
|
||||
) => boolean
|
||||
|
||||
/**
|
||||
* Determines whether a valid link should be automatically linked in the content.
|
||||
*
|
||||
* @param {string} url - The URL that has already been validated.
|
||||
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
||||
*/
|
||||
shouldAutoLink: (url: string) => boolean
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
link: {
|
||||
/**
|
||||
* Set a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
setLink: (attributes: {
|
||||
href: string
|
||||
target?: string | null
|
||||
rel?: string | null
|
||||
class?: string | null
|
||||
title?: string | null
|
||||
}) => ReturnType
|
||||
/**
|
||||
* Toggle a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
toggleLink: (attributes?: {
|
||||
href: string
|
||||
target?: string | null
|
||||
rel?: string | null
|
||||
class?: string | null
|
||||
title?: string | null
|
||||
}) => ReturnType
|
||||
/**
|
||||
* Unset a link mark
|
||||
* @example editor.commands.unsetLink()
|
||||
*/
|
||||
unsetLink: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
|
||||
const allowedProtocols: string[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp']
|
||||
|
||||
if (protocols) {
|
||||
protocols.forEach(protocol => {
|
||||
const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
|
||||
|
||||
if (nextProtocol) {
|
||||
allowedProtocols.push(nextProtocol)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
!uri ||
|
||||
uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, '').match(
|
||||
new RegExp(
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))`,
|
||||
'i',
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to create links.
|
||||
* @see https://www.tiptap.dev/api/marks/link
|
||||
*/
|
||||
export const Link = Mark.create<LinkOptions>({
|
||||
name: 'link',
|
||||
|
||||
priority: 1000,
|
||||
|
||||
keepOnSplit: false,
|
||||
|
||||
exitable: true,
|
||||
|
||||
onCreate() {
|
||||
// TODO: v4 - remove validate option
|
||||
if (this.options.validate && !this.options.shouldAutoLink) {
|
||||
// Copy the validate function to the shouldAutoLink option
|
||||
this.options.shouldAutoLink = this.options.validate
|
||||
console.warn('The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.')
|
||||
}
|
||||
this.options.protocols.forEach(protocol => {
|
||||
if (typeof protocol === 'string') {
|
||||
registerCustomProtocol(protocol)
|
||||
return
|
||||
}
|
||||
registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)
|
||||
})
|
||||
},
|
||||
|
||||
onDestroy() {
|
||||
reset()
|
||||
},
|
||||
|
||||
inclusive() {
|
||||
return this.options.autolink
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
openOnClick: true,
|
||||
enableClickSelection: false,
|
||||
linkOnPaste: true,
|
||||
autolink: true,
|
||||
protocols: [],
|
||||
defaultProtocol: 'http',
|
||||
HTMLAttributes: {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
class: null,
|
||||
},
|
||||
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||
validate: url => !!url,
|
||||
shouldAutoLink: url => {
|
||||
// URLs with explicit protocols (e.g., https://) should be auto-linked
|
||||
// But not if @ appears before :// (that would be userinfo like user:pass@host)
|
||||
const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url)
|
||||
const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url)
|
||||
|
||||
if (hasProtocol || (hasMaybeProtocol && !url.includes('@'))) {
|
||||
return true
|
||||
}
|
||||
// Strip userinfo (user:pass@) if present, then extract hostname
|
||||
const urlWithoutUserinfo = url.includes('@') ? url.split('@').pop()! : url
|
||||
const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]
|
||||
|
||||
// Don't auto-link IP addresses without protocol
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
|
||||
return false
|
||||
}
|
||||
// Don't auto-link single-word hostnames without TLD (e.g., "localhost")
|
||||
if (!/\./.test(hostname)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
href: {
|
||||
default: null,
|
||||
parseHTML(element) {
|
||||
return element.getAttribute('href')
|
||||
},
|
||||
},
|
||||
target: {
|
||||
default: this.options.HTMLAttributes.target,
|
||||
},
|
||||
rel: {
|
||||
default: this.options.HTMLAttributes.rel,
|
||||
},
|
||||
class: {
|
||||
default: this.options.HTMLAttributes.class,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => {
|
||||
const href = (dom as HTMLElement).getAttribute('href')
|
||||
|
||||
// prevent XSS attacks
|
||||
if (
|
||||
!href ||
|
||||
!this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
// prevent XSS attacks
|
||||
if (
|
||||
!this.options.isAllowedUri(HTMLAttributes.href, {
|
||||
defaultValidate: href => !!isAllowedUri(href, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
// strip out the href
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
|
||||
}
|
||||
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
markdownTokenName: 'link',
|
||||
|
||||
parseMarkdown: (token, helpers) => {
|
||||
return helpers.applyMark('link', helpers.parseInline(token.tokens || []), {
|
||||
href: token.href,
|
||||
title: token.title || null,
|
||||
})
|
||||
},
|
||||
|
||||
renderMarkdown: (node, h) => {
|
||||
const href = node.attrs?.href ?? ''
|
||||
const title = node.attrs?.title ?? ''
|
||||
const text = h.renderChildren(node)
|
||||
|
||||
return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setLink:
|
||||
attributes =>
|
||||
({ chain }) => {
|
||||
const { href } = attributes
|
||||
|
||||
if (
|
||||
!this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()
|
||||
},
|
||||
|
||||
toggleLink:
|
||||
attributes =>
|
||||
({ chain }) => {
|
||||
const { href } = attributes || {}
|
||||
|
||||
if (
|
||||
href &&
|
||||
!this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return chain()
|
||||
.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
|
||||
.setMeta('preventAutolink', true)
|
||||
.run()
|
||||
},
|
||||
|
||||
unsetLink:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta('preventAutolink', true).run()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule({
|
||||
find: text => {
|
||||
const foundLinks: PasteRuleMatch[] = []
|
||||
|
||||
if (text) {
|
||||
const { protocols, defaultProtocol } = this.options
|
||||
const links = find(text).filter(
|
||||
item =>
|
||||
item.isLink &&
|
||||
this.options.isAllowedUri(item.value, {
|
||||
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol,
|
||||
}),
|
||||
)
|
||||
|
||||
if (links.length) {
|
||||
links.forEach(link => {
|
||||
if (!this.options.shouldAutoLink(link.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
foundLinks.push({
|
||||
text: link.value,
|
||||
data: {
|
||||
href: link.href,
|
||||
},
|
||||
index: link.start,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return foundLinks
|
||||
},
|
||||
type: this.type,
|
||||
getAttributes: match => {
|
||||
return {
|
||||
href: match.data?.href,
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const plugins: Plugin[] = []
|
||||
const { protocols, defaultProtocol } = this.options
|
||||
|
||||
if (this.options.autolink) {
|
||||
plugins.push(
|
||||
autolink({
|
||||
type: this.type,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
validate: url =>
|
||||
this.options.isAllowedUri(url, {
|
||||
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol,
|
||||
}),
|
||||
shouldAutoLink: this.options.shouldAutoLink,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
plugins.push(
|
||||
clickHandler({
|
||||
type: this.type,
|
||||
editor: this.editor,
|
||||
openOnClick: this.options.openOnClick === 'whenNotEditable' ? true : this.options.openOnClick,
|
||||
enableClickSelection: this.options.enableClickSelection,
|
||||
}),
|
||||
)
|
||||
|
||||
if (this.options.linkOnPaste) {
|
||||
plugins.push(
|
||||
pasteHandler({
|
||||
editor: this.editor,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
type: this.type,
|
||||
shouldAutoLink: this.options.shouldAutoLink,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return plugins
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user