@tiptap/extension-link 3.30.2 → 3.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,639 +1,517 @@
1
- // src/link.ts
2
- import { Mark, markPasteRule as markPasteRule2, mergeAttributes } from "@tiptap/core";
3
- import { find as find2, registerCustomProtocol, reset } from "linkifyjs";
4
-
5
- // src/helpers/autolink.ts
6
- import {
7
- combineTransactionSteps,
8
- findChildrenInRange,
9
- getChangedRanges,
10
- getMarksBetween
11
- } from "@tiptap/core";
1
+ import { InputRule, Mark, PasteRule, combineTransactionSteps, findChildrenInRange, getAttributes, getChangedRanges, getMarksBetween, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
2
+ import { find, registerCustomProtocol, reset, tokenize } from "linkifyjs";
12
3
  import { Plugin, PluginKey } from "@tiptap/pm/state";
13
- import { tokenize } from "linkifyjs";
14
-
15
- // src/helpers/whitespace.ts
16
- var UNICODE_WHITESPACE_PATTERN = "[\0- \xA0\u1680\u180E\u2000-\u2029\u205F\u3000]";
17
- var UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN);
18
- var UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`);
19
- var UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, "g");
20
-
21
- // src/helpers/autolink.ts
4
+ //#region src/helpers/whitespace.ts
5
+ const UNICODE_WHITESPACE_PATTERN = "[\0- \xA0 ᠎ -\u2029  ]";
6
+ const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN);
7
+ const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`);
8
+ const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, "g");
9
+ //#endregion
10
+ //#region src/helpers/autolink.ts
11
+ /**
12
+ * Check if the provided tokens form a valid link structure, which can either be a single link token
13
+ * or a link token surrounded by parentheses or square brackets.
14
+ *
15
+ * This ensures that only complete and valid text is hyperlinked, preventing cases where a valid
16
+ * top-level domain (TLD) is immediately followed by an invalid character, like a number. For
17
+ * example, with the `find` method from Linkify, entering `example.com1` would result in
18
+ * `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`
19
+ * method, we can perform more comprehensive validation on the input text.
20
+ */
22
21
  function isValidLinkStructure(tokens) {
23
- if (tokens.length === 1) {
24
- return tokens[0].isLink;
25
- }
26
- if (tokens.length === 3 && tokens[1].isLink) {
27
- return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
28
- }
29
- return false;
22
+ if (tokens.length === 1) return tokens[0].isLink;
23
+ if (tokens.length === 3 && tokens[1].isLink) return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
24
+ return false;
30
25
  }
26
+ /**
27
+ * This plugin allows you to automatically add links to your editor.
28
+ * @param options The plugin options
29
+ * @returns The plugin instance
30
+ */
31
31
  function autolink(options) {
32
- return new Plugin({
33
- key: new PluginKey("autolink"),
34
- appendTransaction: (transactions, oldState, newState) => {
35
- const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc);
36
- const preventAutolink = transactions.some(
37
- (transaction) => transaction.getMeta("preventAutolink")
38
- );
39
- if (!docChanges || preventAutolink) {
40
- return;
41
- }
42
- const { tr } = newState;
43
- const transform = combineTransactionSteps(oldState.doc, [...transactions]);
44
- const changes = getChangedRanges(transform);
45
- changes.forEach(({ newRange }) => {
46
- const nodesInChangedRanges = findChildrenInRange(
47
- newState.doc,
48
- newRange,
49
- (node) => node.isTextblock
50
- );
51
- let textBlock;
52
- let textBeforeWhitespace;
53
- if (nodesInChangedRanges.length > 1) {
54
- textBlock = nodesInChangedRanges[0];
55
- textBeforeWhitespace = newState.doc.textBetween(
56
- textBlock.pos,
57
- textBlock.pos + textBlock.node.nodeSize,
58
- void 0,
59
- " "
60
- );
61
- } else if (nodesInChangedRanges.length) {
62
- const endText = newState.doc.textBetween(newRange.from, newRange.to, " ", " ");
63
- if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {
64
- return;
65
- }
66
- textBlock = nodesInChangedRanges[0];
67
- textBeforeWhitespace = newState.doc.textBetween(
68
- textBlock.pos,
69
- newRange.to,
70
- void 0,
71
- " "
72
- );
73
- }
74
- if (textBlock && textBeforeWhitespace) {
75
- const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean);
76
- if (wordsBeforeWhitespace.length <= 0) {
77
- return false;
78
- }
79
- const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
80
- const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
81
- if (!lastWordBeforeSpace) {
82
- return false;
83
- }
84
- const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(
85
- (t) => t.toObject(options.defaultProtocol)
86
- );
87
- if (!isValidLinkStructure(linksBeforeSpace)) {
88
- return false;
89
- }
90
- linksBeforeSpace.filter((link) => link.isLink).map((link) => ({
91
- ...link,
92
- from: lastWordAndBlockOffset + link.start + 1,
93
- to: lastWordAndBlockOffset + link.end + 1
94
- })).filter((link) => {
95
- if (!newState.schema.marks.code) {
96
- return true;
97
- }
98
- return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
99
- }).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => {
100
- if (getMarksBetween(link.from, link.to, newState.doc).some(
101
- (item) => item.mark.type === options.type
102
- )) {
103
- return;
104
- }
105
- tr.addMark(
106
- link.from,
107
- link.to,
108
- options.type.create({
109
- href: link.href
110
- })
111
- );
112
- });
113
- }
114
- });
115
- if (!tr.steps.length) {
116
- return;
117
- }
118
- return tr;
119
- }
120
- });
32
+ return new Plugin({
33
+ key: new PluginKey("autolink"),
34
+ appendTransaction: (transactions, oldState, newState) => {
35
+ /**
36
+ * Does the transaction change the document?
37
+ */
38
+ const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc);
39
+ /**
40
+ * Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.
41
+ */
42
+ const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink"));
43
+ /**
44
+ * Prevent autolink if the transaction is not a document change
45
+ * or if the transaction has the meta `preventAutolink`.
46
+ */
47
+ if (!docChanges || preventAutolink) return;
48
+ const { tr } = newState;
49
+ const transform = combineTransactionSteps(oldState.doc, [...transactions]);
50
+ getChangedRanges(transform).forEach(({ newRange }) => {
51
+ const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, (node) => node.isTextblock);
52
+ let textBlock;
53
+ let textBeforeWhitespace;
54
+ if (nodesInChangedRanges.length > 1) {
55
+ textBlock = nodesInChangedRanges[0];
56
+ textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, textBlock.pos + textBlock.node.nodeSize, void 0, " ");
57
+ } else if (nodesInChangedRanges.length) {
58
+ const endText = newState.doc.textBetween(newRange.from, newRange.to, " ", " ");
59
+ if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) return;
60
+ textBlock = nodesInChangedRanges[0];
61
+ textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, void 0, " ");
62
+ }
63
+ if (textBlock && textBeforeWhitespace) {
64
+ const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean);
65
+ if (wordsBeforeWhitespace.length <= 0) return false;
66
+ const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
67
+ const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
68
+ if (!lastWordBeforeSpace) return false;
69
+ const linksBeforeSpace = tokenize(lastWordBeforeSpace).map((t) => t.toObject(options.defaultProtocol));
70
+ if (!isValidLinkStructure(linksBeforeSpace)) return false;
71
+ linksBeforeSpace.filter((link) => link.isLink).map((link) => ({
72
+ ...link,
73
+ from: lastWordAndBlockOffset + link.start + 1,
74
+ to: lastWordAndBlockOffset + link.end + 1
75
+ })).filter((link) => {
76
+ if (!newState.schema.marks.code) return true;
77
+ return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
78
+ }).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => {
79
+ if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) return;
80
+ tr.addMark(link.from, link.to, options.type.create({ href: link.href }));
81
+ });
82
+ }
83
+ });
84
+ if (!tr.steps.length) return;
85
+ return tr;
86
+ }
87
+ });
121
88
  }
122
-
123
- // src/helpers/clickHandler.ts
124
- import { getAttributes } from "@tiptap/core";
125
- import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
89
+ //#endregion
90
+ //#region src/helpers/clickHandler.ts
126
91
  function clickHandler(options) {
127
- return new Plugin2({
128
- key: new PluginKey2("handleClickLink"),
129
- props: {
130
- handleClick: (view, pos, event) => {
131
- var _a, _b;
132
- if (event.button !== 0) {
133
- return false;
134
- }
135
- if (!view.editable) {
136
- return false;
137
- }
138
- let link = null;
139
- if (event.target instanceof HTMLAnchorElement) {
140
- link = event.target;
141
- } else {
142
- const target = event.target;
143
- if (!target) {
144
- return false;
145
- }
146
- const root = options.editor.view.dom;
147
- link = target.closest("a");
148
- if (link && !root.contains(link)) {
149
- link = null;
150
- }
151
- }
152
- if (!link) {
153
- return false;
154
- }
155
- let handled = false;
156
- if (options.enableClickSelection) {
157
- const commandResult = options.editor.commands.extendMarkRange(options.type.name);
158
- handled = commandResult;
159
- }
160
- if (options.openOnClick) {
161
- const attrs = getAttributes(view.state, options.type.name);
162
- const href = (_a = link.href) != null ? _a : attrs.href;
163
- const target = (_b = link.target) != null ? _b : attrs.target;
164
- if (href) {
165
- window.open(href, target);
166
- handled = true;
167
- }
168
- }
169
- return handled;
170
- }
171
- }
172
- });
92
+ return new Plugin({
93
+ key: new PluginKey("handleClickLink"),
94
+ props: { handleClick: (view, pos, event) => {
95
+ if (event.button !== 0) return false;
96
+ if (!view.editable) return false;
97
+ let link = null;
98
+ if (event.target instanceof HTMLAnchorElement) link = event.target;
99
+ else {
100
+ const target = event.target;
101
+ if (!target) return false;
102
+ const root = options.editor.view.dom;
103
+ link = target.closest("a");
104
+ if (link && !root.contains(link)) link = null;
105
+ }
106
+ if (!link) return false;
107
+ let handled = false;
108
+ if (options.enableClickSelection) handled = options.editor.commands.extendMarkRange(options.type.name);
109
+ if (options.openOnClick) {
110
+ var _link$href, _link$target;
111
+ const attrs = getAttributes(view.state, options.type.name);
112
+ const href = (_link$href = link.href) !== null && _link$href !== void 0 ? _link$href : attrs.href;
113
+ const target = (_link$target = link.target) !== null && _link$target !== void 0 ? _link$target : attrs.target;
114
+ if (href) {
115
+ window.open(href, target);
116
+ handled = true;
117
+ }
118
+ }
119
+ return handled;
120
+ } }
121
+ });
173
122
  }
174
-
175
- // src/helpers/markdownLink.ts
176
- import { InputRule, markInputRule, markPasteRule, PasteRule } from "@tiptap/core";
177
- var MARKDOWN_LINK_INPUT_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)$/;
178
- var MARKDOWN_LINK_PASTE_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)/g;
123
+ //#endregion
124
+ //#region src/helpers/markdownLink.ts
125
+ /**
126
+ * Matches a Markdown link with an optional quoted title.
127
+ * for ex: [Tiptap](https://tiptap.dev) or [Tiptap](https://tiptap.dev "some title")
128
+ * the URL may also contain one level of balanced parentheses, as in CommonMark
129
+ * (titles accept curly quotes too, the Typography extension swaps them in while typing)
130
+ * the title delimiters must come in matching pairs
131
+ */
132
+ const MARKDOWN_LINK_INPUT_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)$/;
133
+ /**
134
+ * Same as the input regex but global, to find every Markdown link in pasted text.
135
+ */
136
+ const MARKDOWN_LINK_PASTE_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)/g;
179
137
  function isEscaped(text, index) {
180
- let backslashes = 0;
181
- for (let position = index - 1; position >= 0 && text[position] === "\\"; position -= 1) {
182
- backslashes += 1;
183
- }
184
- return backslashes % 2 === 1;
138
+ let backslashes = 0;
139
+ for (let position = index - 1; position >= 0 && text[position] === "\\"; position -= 1) backslashes += 1;
140
+ return backslashes % 2 === 1;
185
141
  }
142
+ /**
143
+ * Pairs the backtick runs before the match by length, as CommonMark does.
144
+ * A run left open means the match sits in an unfinished code span.
145
+ */
186
146
  function isInsideCodeSpan(text, matchIndex) {
187
- let openRunLength = 0;
188
- let index = 0;
189
- while (index < matchIndex) {
190
- if (text[index] !== "`") {
191
- index += 1;
192
- continue;
193
- }
194
- if (openRunLength === 0 && isEscaped(text, index)) {
195
- index += 1;
196
- continue;
197
- }
198
- let runLength = 0;
199
- while (index < matchIndex && text[index] === "`") {
200
- runLength += 1;
201
- index += 1;
202
- }
203
- if (openRunLength === 0) {
204
- openRunLength = runLength;
205
- } else if (runLength === openRunLength) {
206
- openRunLength = 0;
207
- }
208
- }
209
- return openRunLength > 0;
147
+ let openRunLength = 0;
148
+ let index = 0;
149
+ while (index < matchIndex) {
150
+ if (text[index] !== "`") {
151
+ index += 1;
152
+ continue;
153
+ }
154
+ if (openRunLength === 0 && isEscaped(text, index)) {
155
+ index += 1;
156
+ continue;
157
+ }
158
+ let runLength = 0;
159
+ while (index < matchIndex && text[index] === "`") {
160
+ runLength += 1;
161
+ index += 1;
162
+ }
163
+ if (openRunLength === 0) openRunLength = runLength;
164
+ else if (runLength === openRunLength) openRunLength = 0;
165
+ }
166
+ return openRunLength > 0;
210
167
  }
211
168
  function isConvertibleLink(text, match, isAllowedHref) {
212
- var _a, _b;
213
- const [, linkText, href] = match;
214
- const characterBefore = match.index ? text[match.index - 1] : void 0;
215
- if (characterBefore === "!" || isEscaped(text, (_a = match.index) != null ? _a : 0)) {
216
- return false;
217
- }
218
- if (isInsideCodeSpan(text, (_b = match.index) != null ? _b : 0)) {
219
- return false;
220
- }
221
- return !!linkText.trim() && isAllowedHref(href);
169
+ var _match$index, _match$index2;
170
+ const [, linkText, href] = match;
171
+ if ((match.index ? text[match.index - 1] : void 0) === "!" || isEscaped(text, (_match$index = match.index) !== null && _match$index !== void 0 ? _match$index : 0)) return false;
172
+ if (isInsideCodeSpan(text, (_match$index2 = match.index) !== null && _match$index2 !== void 0 ? _match$index2 : 0)) return false;
173
+ return !!linkText.trim() && isAllowedHref(href);
222
174
  }
223
175
  function toRuleMatch(match) {
224
- var _a, _b;
225
- const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] = match;
226
- const title = (_a = straightQuotedTitle != null ? straightQuotedTitle : curlyDoubleTitle) != null ? _a : curlySingleTitle;
227
- return {
228
- index: (_b = match.index) != null ? _b : 0,
229
- text: linkSyntax,
230
- replaceWith: linkText,
231
- data: {
232
- href,
233
- // an empty title ("") counts as no title, as in CommonMark
234
- title: title || null,
235
- markdown: true
236
- }
237
- };
176
+ var _ref, _match$index3;
177
+ const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] = match;
178
+ const title = (_ref = straightQuotedTitle !== null && straightQuotedTitle !== void 0 ? straightQuotedTitle : curlyDoubleTitle) !== null && _ref !== void 0 ? _ref : curlySingleTitle;
179
+ return {
180
+ index: (_match$index3 = match.index) !== null && _match$index3 !== void 0 ? _match$index3 : 0,
181
+ text: linkSyntax,
182
+ replaceWith: linkText,
183
+ data: {
184
+ href,
185
+ title: title || null,
186
+ markdown: true
187
+ }
188
+ };
238
189
  }
239
190
  function matchesOverlap(a, b) {
240
- return a.index < b.index + b.text.length && b.index < a.index + a.text.length;
191
+ return a.index < b.index + b.text.length && b.index < a.index + a.text.length;
241
192
  }
242
193
  function getMarkdownLinkAttributes(match) {
243
- var _a, _b, _c;
244
- return {
245
- href: (_a = match.data) == null ? void 0 : _a.href,
246
- title: (_c = (_b = match.data) == null ? void 0 : _b.title) != null ? _c : null
247
- };
194
+ var _match$data, _match$data$title, _match$data2;
195
+ return {
196
+ href: (_match$data = match.data) === null || _match$data === void 0 ? void 0 : _match$data.href,
197
+ title: (_match$data$title = (_match$data2 = match.data) === null || _match$data2 === void 0 ? void 0 : _match$data2.title) !== null && _match$data$title !== void 0 ? _match$data$title : null
198
+ };
248
199
  }
200
+ /**
201
+ * Turns typed Markdown link syntax into a link mark as soon as the closing `)` comes in.
202
+ * The transaction gets flagged so autolink doesn't touch the converted text again.
203
+ */
249
204
  function markdownLinkInputRule(config) {
250
- const rule = markInputRule({
251
- find: (text) => {
252
- const match = MARKDOWN_LINK_INPUT_REGEX.exec(text);
253
- if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) {
254
- return null;
255
- }
256
- return toRuleMatch(match);
257
- },
258
- type: config.type,
259
- getAttributes: getMarkdownLinkAttributes
260
- });
261
- return new InputRule({
262
- find: rule.find,
263
- handler: (props) => {
264
- const result = rule.handler(props);
265
- if (result !== null && props.state.tr.steps.length) {
266
- props.state.tr.setMeta("preventAutolink", true);
267
- }
268
- return result;
269
- }
270
- });
205
+ const rule = markInputRule({
206
+ find: (text) => {
207
+ const match = MARKDOWN_LINK_INPUT_REGEX.exec(text);
208
+ if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) return null;
209
+ return toRuleMatch(match);
210
+ },
211
+ type: config.type,
212
+ getAttributes: getMarkdownLinkAttributes
213
+ });
214
+ return new InputRule({
215
+ find: rule.find,
216
+ handler: (props) => {
217
+ const result = rule.handler(props);
218
+ if (result !== null && props.state.tr.steps.length) props.state.tr.setMeta("preventAutolink", true);
219
+ return result;
220
+ }
221
+ });
271
222
  }
223
+ /**
224
+ * Same for pasting, converts every Markdown link found in the pasted text
225
+ * and links the plain URLs from `findPlainUrls`.
226
+ */
272
227
  function markdownLinkPasteRule(config) {
273
- const rule = markPasteRule({
274
- find: (text) => {
275
- var _a, _b;
276
- const markdownMatches = [];
277
- for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) {
278
- if (isConvertibleLink(text, match, config.isAllowedHref)) {
279
- markdownMatches.push(toRuleMatch(match));
280
- }
281
- }
282
- const plainUrlMatches = ((_b = (_a = config.findPlainUrls) == null ? void 0 : _a.call(config, text)) != null ? _b : []).filter(
283
- (urlMatch) => !markdownMatches.some((markdownMatch) => matchesOverlap(markdownMatch, urlMatch))
284
- );
285
- return [...markdownMatches, ...plainUrlMatches];
286
- },
287
- type: config.type,
288
- getAttributes: getMarkdownLinkAttributes
289
- });
290
- return new PasteRule({
291
- find: rule.find,
292
- handler: (props) => {
293
- var _a;
294
- const result = rule.handler(props);
295
- if (result !== null && props.state.tr.steps.length && ((_a = props.match.data) == null ? void 0 : _a.markdown)) {
296
- props.state.tr.setMeta("preventAutolink", true);
297
- }
298
- return result;
299
- }
300
- });
228
+ const rule = markPasteRule({
229
+ find: (text) => {
230
+ var _config$findPlainUrls, _config$findPlainUrls2;
231
+ const markdownMatches = [];
232
+ for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) if (isConvertibleLink(text, match, config.isAllowedHref)) markdownMatches.push(toRuleMatch(match));
233
+ const plainUrlMatches = ((_config$findPlainUrls = (_config$findPlainUrls2 = config.findPlainUrls) === null || _config$findPlainUrls2 === void 0 ? void 0 : _config$findPlainUrls2.call(config, text)) !== null && _config$findPlainUrls !== void 0 ? _config$findPlainUrls : []).filter((urlMatch) => !markdownMatches.some((markdownMatch) => matchesOverlap(markdownMatch, urlMatch)));
234
+ return [...markdownMatches, ...plainUrlMatches];
235
+ },
236
+ type: config.type,
237
+ getAttributes: getMarkdownLinkAttributes
238
+ });
239
+ return new PasteRule({
240
+ find: rule.find,
241
+ handler: (props) => {
242
+ var _props$match$data;
243
+ const result = rule.handler(props);
244
+ if (result !== null && props.state.tr.steps.length && ((_props$match$data = props.match.data) === null || _props$match$data === void 0 ? void 0 : _props$match$data.markdown)) props.state.tr.setMeta("preventAutolink", true);
245
+ return result;
246
+ }
247
+ });
301
248
  }
302
-
303
- // src/helpers/pasteHandler.ts
304
- import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@tiptap/pm/state";
305
- import { find } from "linkifyjs";
249
+ //#endregion
250
+ //#region src/helpers/pasteHandler.ts
306
251
  function pasteHandler(options) {
307
- return new Plugin3({
308
- key: new PluginKey3("handlePasteLink"),
309
- props: {
310
- handlePaste: (view, _event, slice) => {
311
- const { shouldAutoLink } = options;
312
- const { state } = view;
313
- const { selection } = state;
314
- const { empty } = selection;
315
- if (empty) {
316
- return false;
317
- }
318
- let textContent = "";
319
- slice.content.forEach((node) => {
320
- textContent += node.textContent;
321
- });
322
- const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(
323
- (item) => item.isLink && item.value === textContent
324
- );
325
- if (!textContent || !link || shouldAutoLink !== void 0 && !shouldAutoLink(link.value)) {
326
- return false;
327
- }
328
- return options.editor.commands.setMark(options.type, {
329
- href: link.href
330
- });
331
- }
332
- }
333
- });
252
+ return new Plugin({
253
+ key: new PluginKey("handlePasteLink"),
254
+ props: { handlePaste: (view, _event, slice) => {
255
+ const { shouldAutoLink } = options;
256
+ const { state } = view;
257
+ const { selection } = state;
258
+ const { empty } = selection;
259
+ if (empty) return false;
260
+ let textContent = "";
261
+ slice.content.forEach((node) => {
262
+ textContent += node.textContent;
263
+ });
264
+ const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find((item) => item.isLink && item.value === textContent);
265
+ if (!textContent || !link || shouldAutoLink !== void 0 && !shouldAutoLink(link.value)) return false;
266
+ return options.editor.commands.setMark(options.type, { href: link.href });
267
+ } }
268
+ });
334
269
  }
335
-
336
- // src/link.ts
337
- var pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
270
+ //#endregion
271
+ //#region src/link.ts
272
+ const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
338
273
  function isAllowedUri(uri, protocols) {
339
- const allowedProtocols = [
340
- "http",
341
- "https",
342
- "ftp",
343
- "ftps",
344
- "mailto",
345
- "tel",
346
- "callto",
347
- "sms",
348
- "cid",
349
- "xmpp"
350
- ];
351
- if (protocols) {
352
- protocols.forEach((protocol) => {
353
- const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
354
- if (nextProtocol) {
355
- allowedProtocols.push(nextProtocol);
356
- }
357
- });
358
- }
359
- return !uri || uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, "").match(
360
- new RegExp(
361
- `^(?:(?:${allowedProtocols.map((protocol) => protocol.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|")}):|[^a-z]|[a-z0-9+.\\-]+(?:[^a-z+.\\-:]|$))`,
362
- "i"
363
- )
364
- );
274
+ const allowedProtocols = [
275
+ "http",
276
+ "https",
277
+ "ftp",
278
+ "ftps",
279
+ "mailto",
280
+ "tel",
281
+ "callto",
282
+ "sms",
283
+ "cid",
284
+ "xmpp"
285
+ ];
286
+ if (protocols) protocols.forEach((protocol) => {
287
+ const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
288
+ if (nextProtocol) allowedProtocols.push(nextProtocol);
289
+ });
290
+ return !uri || uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, "").match(new RegExp(`^(?:(?:${allowedProtocols.map((protocol) => protocol.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|")}):|[^a-z]|[a-z0-9+.\\-]+(?:[^a-z+.\\-:]|$))`, "i"));
365
291
  }
366
- var Link = Mark.create({
367
- name: "link",
368
- priority: 1e3,
369
- keepOnSplit: false,
370
- exitable: true,
371
- onCreate() {
372
- if (this.options.validate && !this.options.shouldAutoLink) {
373
- this.options.shouldAutoLink = this.options.validate;
374
- console.warn(
375
- "The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead."
376
- );
377
- }
378
- this.options.protocols.forEach((protocol) => {
379
- if (typeof protocol === "string") {
380
- registerCustomProtocol(protocol);
381
- return;
382
- }
383
- registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
384
- });
385
- },
386
- onDestroy() {
387
- reset();
388
- },
389
- inclusive() {
390
- return this.options.autolink;
391
- },
392
- addOptions() {
393
- return {
394
- openOnClick: true,
395
- enableClickSelection: false,
396
- linkOnPaste: true,
397
- markdownLinks: false,
398
- // TODO (major) - default to true on next major version
399
- autolink: true,
400
- protocols: [],
401
- defaultProtocol: "http",
402
- HTMLAttributes: {
403
- target: "_blank",
404
- rel: "noopener noreferrer nofollow",
405
- class: null
406
- },
407
- isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
408
- validate: (url) => !!url,
409
- shouldAutoLink: (url) => {
410
- const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
411
- const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url);
412
- if (hasProtocol || hasMaybeProtocol && !url.includes("@")) {
413
- return true;
414
- }
415
- const urlWithoutUserinfo = url.includes("@") ? url.split("@").pop() : url;
416
- const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0];
417
- if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
418
- return false;
419
- }
420
- if (!/\./.test(hostname)) {
421
- return false;
422
- }
423
- return true;
424
- }
425
- };
426
- },
427
- addAttributes() {
428
- var _a, _b, _c;
429
- return {
430
- href: {
431
- default: null,
432
- parseHTML(element) {
433
- return element.getAttribute("href");
434
- }
435
- },
436
- target: {
437
- // Coerce `undefined` to `null` because `undefined` is an invalid attribute value
438
- default: (_a = this.options.HTMLAttributes.target) != null ? _a : null
439
- },
440
- rel: {
441
- // Coerce `undefined` to `null` because `undefined` is an invalid attribute value
442
- default: (_b = this.options.HTMLAttributes.rel) != null ? _b : null
443
- },
444
- class: {
445
- // Coerce `undefined` to `null` because `undefined` is an invalid attribute value
446
- default: (_c = this.options.HTMLAttributes.class) != null ? _c : null
447
- },
448
- title: {
449
- default: null
450
- }
451
- };
452
- },
453
- parseHTML() {
454
- return [
455
- {
456
- tag: "a[href]",
457
- getAttrs: (dom) => {
458
- const href = dom.getAttribute("href");
459
- if (!href || !this.options.isAllowedUri(href, {
460
- defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
461
- protocols: this.options.protocols,
462
- defaultProtocol: this.options.defaultProtocol
463
- })) {
464
- return false;
465
- }
466
- return null;
467
- }
468
- }
469
- ];
470
- },
471
- renderHTML({ HTMLAttributes }) {
472
- if (!this.options.isAllowedUri(HTMLAttributes.href, {
473
- defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols),
474
- protocols: this.options.protocols,
475
- defaultProtocol: this.options.defaultProtocol
476
- })) {
477
- return ["a", mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
478
- }
479
- return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
480
- },
481
- markdownTokenName: "link",
482
- parseMarkdown: (token, helpers) => {
483
- return helpers.applyMark("link", helpers.parseInline(token.tokens || []), {
484
- href: token.href,
485
- title: token.title || null
486
- });
487
- },
488
- renderMarkdown: (node, h) => {
489
- var _a, _b, _c, _d;
490
- const href = (_b = (_a = node.attrs) == null ? void 0 : _a.href) != null ? _b : "";
491
- const title = (_d = (_c = node.attrs) == null ? void 0 : _c.title) != null ? _d : "";
492
- const text = h.renderChildren(node);
493
- return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`;
494
- },
495
- addCommands() {
496
- return {
497
- setLink: (attributes) => ({ chain }) => {
498
- const { href } = attributes;
499
- if (!this.options.isAllowedUri(href, {
500
- defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
501
- protocols: this.options.protocols,
502
- defaultProtocol: this.options.defaultProtocol
503
- })) {
504
- return false;
505
- }
506
- return chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run();
507
- },
508
- toggleLink: (attributes) => ({ chain }) => {
509
- const { href } = attributes || {};
510
- if (href && !this.options.isAllowedUri(href, {
511
- defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
512
- protocols: this.options.protocols,
513
- defaultProtocol: this.options.defaultProtocol
514
- })) {
515
- return false;
516
- }
517
- return chain().toggleMark(this.name, attributes, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
518
- },
519
- unsetLink: () => ({ chain }) => {
520
- return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
521
- }
522
- };
523
- },
524
- addInputRules() {
525
- if (!this.options.markdownLinks) {
526
- return [];
527
- }
528
- return [
529
- markdownLinkInputRule({
530
- type: this.type,
531
- isAllowedHref: (href) => this.options.isAllowedUri(href, {
532
- defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
533
- protocols: this.options.protocols,
534
- defaultProtocol: this.options.defaultProtocol
535
- })
536
- })
537
- ];
538
- },
539
- addPasteRules() {
540
- const findPlainUrls = (text) => {
541
- const foundLinks = [];
542
- if (text) {
543
- const { protocols, defaultProtocol } = this.options;
544
- const links = find2(text).filter(
545
- (item) => item.isLink && this.options.isAllowedUri(item.value, {
546
- defaultValidate: (href) => !!isAllowedUri(href, protocols),
547
- protocols,
548
- defaultProtocol
549
- })
550
- );
551
- links.forEach((link) => {
552
- if (!this.options.shouldAutoLink(link.value)) {
553
- return;
554
- }
555
- foundLinks.push({
556
- text: link.value,
557
- data: {
558
- href: link.href
559
- },
560
- index: link.start
561
- });
562
- });
563
- }
564
- return foundLinks;
565
- };
566
- if (this.options.markdownLinks) {
567
- return [
568
- markdownLinkPasteRule({
569
- type: this.type,
570
- isAllowedHref: (href) => this.options.isAllowedUri(href, {
571
- defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
572
- protocols: this.options.protocols,
573
- defaultProtocol: this.options.defaultProtocol
574
- }),
575
- findPlainUrls
576
- })
577
- ];
578
- }
579
- return [
580
- markPasteRule2({
581
- find: findPlainUrls,
582
- type: this.type,
583
- getAttributes: (match) => {
584
- var _a;
585
- return {
586
- href: (_a = match.data) == null ? void 0 : _a.href
587
- };
588
- }
589
- })
590
- ];
591
- },
592
- addProseMirrorPlugins() {
593
- const plugins = [];
594
- const { protocols, defaultProtocol } = this.options;
595
- if (this.options.autolink) {
596
- plugins.push(
597
- autolink({
598
- type: this.type,
599
- defaultProtocol: this.options.defaultProtocol,
600
- validate: (url) => this.options.isAllowedUri(url, {
601
- defaultValidate: (href) => !!isAllowedUri(href, protocols),
602
- protocols,
603
- defaultProtocol
604
- }),
605
- shouldAutoLink: this.options.shouldAutoLink
606
- })
607
- );
608
- }
609
- plugins.push(
610
- clickHandler({
611
- type: this.type,
612
- editor: this.editor,
613
- openOnClick: this.options.openOnClick === "whenNotEditable" ? true : this.options.openOnClick,
614
- enableClickSelection: this.options.enableClickSelection
615
- })
616
- );
617
- if (this.options.linkOnPaste) {
618
- plugins.push(
619
- pasteHandler({
620
- editor: this.editor,
621
- defaultProtocol: this.options.defaultProtocol,
622
- type: this.type,
623
- shouldAutoLink: this.options.shouldAutoLink
624
- })
625
- );
626
- }
627
- return plugins;
628
- }
292
+ /**
293
+ * This extension allows you to create links.
294
+ * @see https://www.tiptap.dev/api/marks/link
295
+ */
296
+ const Link = Mark.create({
297
+ name: "link",
298
+ priority: 1e3,
299
+ keepOnSplit: false,
300
+ exitable: true,
301
+ onCreate() {
302
+ if (this.options.validate && !this.options.shouldAutoLink) {
303
+ this.options.shouldAutoLink = this.options.validate;
304
+ console.warn("The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.");
305
+ }
306
+ this.options.protocols.forEach((protocol) => {
307
+ if (typeof protocol === "string") {
308
+ registerCustomProtocol(protocol);
309
+ return;
310
+ }
311
+ registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
312
+ });
313
+ },
314
+ onDestroy() {
315
+ reset();
316
+ },
317
+ inclusive() {
318
+ return this.options.autolink;
319
+ },
320
+ addOptions() {
321
+ return {
322
+ openOnClick: true,
323
+ enableClickSelection: false,
324
+ linkOnPaste: true,
325
+ markdownLinks: false,
326
+ autolink: true,
327
+ protocols: [],
328
+ defaultProtocol: "http",
329
+ HTMLAttributes: {
330
+ target: "_blank",
331
+ rel: "noopener noreferrer nofollow",
332
+ class: null
333
+ },
334
+ isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
335
+ validate: (url) => !!url,
336
+ shouldAutoLink: (url) => {
337
+ const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
338
+ const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url);
339
+ if (hasProtocol || hasMaybeProtocol && !url.includes("@")) return true;
340
+ const hostname = (url.includes("@") ? url.split("@").pop() : url).split(/[/?#:]/)[0];
341
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return false;
342
+ if (!/\./.test(hostname)) return false;
343
+ return true;
344
+ }
345
+ };
346
+ },
347
+ addAttributes() {
348
+ var _this$options$HTMLAtt, _this$options$HTMLAtt2, _this$options$HTMLAtt3;
349
+ return {
350
+ href: {
351
+ default: null,
352
+ parseHTML(element) {
353
+ return element.getAttribute("href");
354
+ }
355
+ },
356
+ target: { default: (_this$options$HTMLAtt = this.options.HTMLAttributes.target) !== null && _this$options$HTMLAtt !== void 0 ? _this$options$HTMLAtt : null },
357
+ rel: { default: (_this$options$HTMLAtt2 = this.options.HTMLAttributes.rel) !== null && _this$options$HTMLAtt2 !== void 0 ? _this$options$HTMLAtt2 : null },
358
+ class: { default: (_this$options$HTMLAtt3 = this.options.HTMLAttributes.class) !== null && _this$options$HTMLAtt3 !== void 0 ? _this$options$HTMLAtt3 : null },
359
+ title: { default: null }
360
+ };
361
+ },
362
+ parseHTML() {
363
+ return [{
364
+ tag: "a[href]",
365
+ getAttrs: (dom) => {
366
+ const href = dom.getAttribute("href");
367
+ if (!href || !this.options.isAllowedUri(href, {
368
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
369
+ protocols: this.options.protocols,
370
+ defaultProtocol: this.options.defaultProtocol
371
+ })) return false;
372
+ return null;
373
+ }
374
+ }];
375
+ },
376
+ renderHTML({ HTMLAttributes }) {
377
+ if (!this.options.isAllowedUri(HTMLAttributes.href, {
378
+ defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols),
379
+ protocols: this.options.protocols,
380
+ defaultProtocol: this.options.defaultProtocol
381
+ })) return [
382
+ "a",
383
+ mergeAttributes(this.options.HTMLAttributes, {
384
+ ...HTMLAttributes,
385
+ href: ""
386
+ }),
387
+ 0
388
+ ];
389
+ return [
390
+ "a",
391
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
392
+ 0
393
+ ];
394
+ },
395
+ markdownTokenName: "link",
396
+ parseMarkdown: (token, helpers) => {
397
+ return helpers.applyMark("link", helpers.parseInline(token.tokens || []), {
398
+ href: token.href,
399
+ title: token.title || null
400
+ });
401
+ },
402
+ renderMarkdown: (node, h) => {
403
+ var _node$attrs$href, _node$attrs, _node$attrs$title, _node$attrs2;
404
+ const href = (_node$attrs$href = (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.href) !== null && _node$attrs$href !== void 0 ? _node$attrs$href : "";
405
+ const title = (_node$attrs$title = (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.title) !== null && _node$attrs$title !== void 0 ? _node$attrs$title : "";
406
+ const text = h.renderChildren(node);
407
+ return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`;
408
+ },
409
+ addCommands() {
410
+ return {
411
+ setLink: (attributes) => ({ chain }) => {
412
+ const { href } = attributes;
413
+ if (!this.options.isAllowedUri(href, {
414
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
415
+ protocols: this.options.protocols,
416
+ defaultProtocol: this.options.defaultProtocol
417
+ })) return false;
418
+ return chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run();
419
+ },
420
+ toggleLink: (attributes) => ({ chain }) => {
421
+ const { href } = attributes || {};
422
+ if (href && !this.options.isAllowedUri(href, {
423
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
424
+ protocols: this.options.protocols,
425
+ defaultProtocol: this.options.defaultProtocol
426
+ })) return false;
427
+ return chain().toggleMark(this.name, attributes, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
428
+ },
429
+ unsetLink: () => ({ chain }) => {
430
+ return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
431
+ }
432
+ };
433
+ },
434
+ addInputRules() {
435
+ if (!this.options.markdownLinks) return [];
436
+ return [markdownLinkInputRule({
437
+ type: this.type,
438
+ isAllowedHref: (href) => this.options.isAllowedUri(href, {
439
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
440
+ protocols: this.options.protocols,
441
+ defaultProtocol: this.options.defaultProtocol
442
+ })
443
+ })];
444
+ },
445
+ addPasteRules() {
446
+ const findPlainUrls = (text) => {
447
+ const foundLinks = [];
448
+ if (text) {
449
+ const { protocols, defaultProtocol } = this.options;
450
+ find(text).filter((item) => item.isLink && this.options.isAllowedUri(item.value, {
451
+ defaultValidate: (href) => !!isAllowedUri(href, protocols),
452
+ protocols,
453
+ defaultProtocol
454
+ })).forEach((link) => {
455
+ if (!this.options.shouldAutoLink(link.value)) return;
456
+ foundLinks.push({
457
+ text: link.value,
458
+ data: { href: link.href },
459
+ index: link.start
460
+ });
461
+ });
462
+ }
463
+ return foundLinks;
464
+ };
465
+ if (this.options.markdownLinks) return [markdownLinkPasteRule({
466
+ type: this.type,
467
+ isAllowedHref: (href) => this.options.isAllowedUri(href, {
468
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
469
+ protocols: this.options.protocols,
470
+ defaultProtocol: this.options.defaultProtocol
471
+ }),
472
+ findPlainUrls
473
+ })];
474
+ return [markPasteRule({
475
+ find: findPlainUrls,
476
+ type: this.type,
477
+ getAttributes: (match) => {
478
+ var _match$data;
479
+ return { href: (_match$data = match.data) === null || _match$data === void 0 ? void 0 : _match$data.href };
480
+ }
481
+ })];
482
+ },
483
+ addProseMirrorPlugins() {
484
+ const plugins = [];
485
+ const { protocols, defaultProtocol } = this.options;
486
+ if (this.options.autolink) plugins.push(autolink({
487
+ type: this.type,
488
+ defaultProtocol: this.options.defaultProtocol,
489
+ validate: (url) => this.options.isAllowedUri(url, {
490
+ defaultValidate: (href) => !!isAllowedUri(href, protocols),
491
+ protocols,
492
+ defaultProtocol
493
+ }),
494
+ shouldAutoLink: this.options.shouldAutoLink
495
+ }));
496
+ plugins.push(clickHandler({
497
+ type: this.type,
498
+ editor: this.editor,
499
+ openOnClick: this.options.openOnClick === "whenNotEditable" ? true : this.options.openOnClick,
500
+ enableClickSelection: this.options.enableClickSelection
501
+ }));
502
+ if (this.options.linkOnPaste) plugins.push(pasteHandler({
503
+ editor: this.editor,
504
+ defaultProtocol: this.options.defaultProtocol,
505
+ type: this.type,
506
+ shouldAutoLink: this.options.shouldAutoLink
507
+ }));
508
+ return plugins;
509
+ }
629
510
  });
511
+ //#endregion
512
+ //#region src/index.ts
513
+ var src_default = Link;
514
+ //#endregion
515
+ export { Link, src_default as default, isAllowedUri, pasteRegex };
630
516
 
631
- // src/index.ts
632
- var index_default = Link;
633
- export {
634
- Link,
635
- index_default as default,
636
- isAllowedUri,
637
- pasteRegex
638
- };
639
517
  //# sourceMappingURL=index.js.map