fix: resolve TypeScript errors in frontend build

This commit is contained in:
Hiro
2026-03-30 23:16:07 +00:00
parent b733306773
commit 24925e1acb
2941 changed files with 418042 additions and 49 deletions

21
node_modules/@tiptap/extension-link/LICENSE.md generated vendored Normal file
View 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
View File

@@ -0,0 +1,18 @@
# @tiptap/extension-link
[![Version](https://img.shields.io/npm/v/@tiptap/extension-link.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-link)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-link.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-link.svg)](https://www.npmjs.com/package/@tiptap/extension-link)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](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
View 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

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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

53
node_modules/@tiptap/extension-link/package.json generated vendored Normal file
View 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/"
}
}

View 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 lets 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
},
})
}

View 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
},
},
})
}

View 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,
})
},
},
})
}

View 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
View 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
View 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
},
})