Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@tiptap/extension-link

Package Overview
Dependencies
Maintainers
2
Versions
179
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tiptap/extension-link - npm Package Compare versions

Comparing version 2.0.0-beta.30 to 2.0.0-beta.31

dist/packages/extension-link/src/helpers/autolink.d.ts

4

dist/packages/extension-link/src/link.d.ts
import { Mark } from '@tiptap/core';
export interface LinkOptions {
/**
* If enabled, it adds links as you type.
*/
autolink: boolean;
/**
* If enabled, links will be opened on click.

@@ -5,0 +9,0 @@ */

178

dist/tiptap-extension-link.cjs.js

@@ -6,9 +6,127 @@ 'use strict';

var core = require('@tiptap/core');
var linkifyjs = require('linkifyjs');
var prosemirrorState = require('prosemirror-state');
var linkifyjs = require('linkifyjs');
function autolink(options) {
return new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('autolink'),
appendTransaction: (transactions, oldState, newState) => {
const docChanges = transactions.some(transaction => transaction.docChanged)
&& !oldState.doc.eq(newState.doc);
if (!docChanges) {
return;
}
const { tr } = newState;
const transform = core.combineTransactionSteps(oldState.doc, transactions);
const { mapping } = transform;
const changes = core.getChangedRanges(transform);
changes.forEach(({ oldRange, newRange }) => {
// at first we check if we have to remove links
core.getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
.filter(item => item.mark.type === options.type)
.forEach(oldMark => {
const newFrom = mapping.map(oldMark.from);
const newTo = mapping.map(oldMark.to);
const newMarks = core.getMarksBetween(newFrom, newTo, newState.doc)
.filter(item => item.mark.type === options.type);
if (!newMarks.length) {
return;
}
const newMark = newMarks[0];
const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to);
const newLinkText = newState.doc.textBetween(newMark.from, newMark.to);
const wasLink = linkifyjs.test(oldLinkText);
const isLink = linkifyjs.test(newLinkText);
// remove only the link, if it was a link before too
// because we don’t want to remove links that were set manually
if (wasLink && !isLink) {
tr.removeMark(newMark.from, newMark.to, options.type);
}
});
// now let’s see if we can add new links
core.findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
.forEach(textBlock => {
linkifyjs.find(textBlock.node.textContent)
.filter(link => link.isLink)
// calculate link position
.map(link => ({
...link,
from: textBlock.pos + link.start + 1,
to: textBlock.pos + link.end + 1,
}))
// check if link is within the changed range
.filter(link => {
const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
return fromIsInRange || toIsInRange;
})
// add link mark
.forEach(link => {
tr.addMark(link.from, link.to, options.type.create({
href: link.href,
}));
});
});
});
if (!tr.steps.length) {
return;
}
return tr;
},
});
}
function clickHandler(options) {
return new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
var _a;
const attrs = core.getAttributes(view.state, options.type.name);
const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
if (link && attrs.href) {
window.open(attrs.href, attrs.target);
return true;
}
return false;
},
},
});
}
function pasteHandler(options) {
return new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
const { state } = view;
const { selection } = state;
const { empty } = selection;
if (empty) {
return false;
}
let textContent = '';
slice.content.forEach(node => {
textContent += node.textContent;
});
const link = linkifyjs.find(textContent).find(item => item.isLink && item.value === textContent);
if (!textContent || !link) {
return false;
}
options.editor.commands.setMark(options.type, {
href: link.href,
});
return true;
},
},
});
}
const Link = core.Mark.create({
name: 'link',
priority: 1000,
inclusive: false,
keepOnSplit: false,
inclusive() {
return this.options.autolink;
},
addOptions() {

@@ -18,2 +136,3 @@ return {

linkOnPaste: true,
autolink: true,
HTMLAttributes: {

@@ -41,3 +160,7 @@ target: '_blank',

renderHTML({ HTMLAttributes }) {
return ['a', core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
return [
'a',
core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},

@@ -79,45 +202,16 @@ addCommands() {

const plugins = [];
if (this.options.autolink) {
plugins.push(autolink({
type: this.type,
}));
}
if (this.options.openOnClick) {
plugins.push(new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
var _a;
const attrs = this.editor.getAttributes(this.name);
const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
if (link && attrs.href) {
window.open(attrs.href, attrs.target);
return true;
}
return false;
},
},
plugins.push(clickHandler({
type: this.type,
}));
}
if (this.options.linkOnPaste) {
plugins.push(new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
const { state } = view;
const { selection } = state;
const { empty } = selection;
if (empty) {
return false;
}
let textContent = '';
slice.content.forEach(node => {
textContent += node.textContent;
});
const link = linkifyjs.find(textContent)
.find(item => item.isLink && item.value === textContent);
if (!textContent || !link) {
return false;
}
this.editor.commands.setMark(this.type, {
href: link.href,
});
return true;
},
},
plugins.push(pasteHandler({
editor: this.editor,
type: this.type,
}));

@@ -124,0 +218,0 @@ }

@@ -1,9 +0,127 @@

import { Mark, mergeAttributes, markPasteRule } from '@tiptap/core';
import { combineTransactionSteps, getChangedRanges, getMarksBetween, findChildrenInRange, getAttributes, Mark, mergeAttributes, markPasteRule } from '@tiptap/core';
import { test, find } from 'linkifyjs';
import { Plugin, PluginKey } from 'prosemirror-state';
import { find } from 'linkifyjs';
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);
if (!docChanges) {
return;
}
const { tr } = newState;
const transform = combineTransactionSteps(oldState.doc, transactions);
const { mapping } = transform;
const changes = getChangedRanges(transform);
changes.forEach(({ oldRange, newRange }) => {
// at first we check if we have to remove links
getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
.filter(item => item.mark.type === options.type)
.forEach(oldMark => {
const newFrom = mapping.map(oldMark.from);
const newTo = mapping.map(oldMark.to);
const newMarks = getMarksBetween(newFrom, newTo, newState.doc)
.filter(item => item.mark.type === options.type);
if (!newMarks.length) {
return;
}
const newMark = newMarks[0];
const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to);
const newLinkText = newState.doc.textBetween(newMark.from, newMark.to);
const wasLink = test(oldLinkText);
const isLink = test(newLinkText);
// remove only the link, if it was a link before too
// because we don’t want to remove links that were set manually
if (wasLink && !isLink) {
tr.removeMark(newMark.from, newMark.to, options.type);
}
});
// now let’s see if we can add new links
findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
.forEach(textBlock => {
find(textBlock.node.textContent)
.filter(link => link.isLink)
// calculate link position
.map(link => ({
...link,
from: textBlock.pos + link.start + 1,
to: textBlock.pos + link.end + 1,
}))
// check if link is within the changed range
.filter(link => {
const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
return fromIsInRange || toIsInRange;
})
// add link mark
.forEach(link => {
tr.addMark(link.from, link.to, options.type.create({
href: link.href,
}));
});
});
});
if (!tr.steps.length) {
return;
}
return tr;
},
});
}
function clickHandler(options) {
return new Plugin({
key: new PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
var _a;
const attrs = getAttributes(view.state, options.type.name);
const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
if (link && attrs.href) {
window.open(attrs.href, attrs.target);
return true;
}
return false;
},
},
});
}
function pasteHandler(options) {
return new Plugin({
key: new PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
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).find(item => item.isLink && item.value === textContent);
if (!textContent || !link) {
return false;
}
options.editor.commands.setMark(options.type, {
href: link.href,
});
return true;
},
},
});
}
const Link = Mark.create({
name: 'link',
priority: 1000,
inclusive: false,
keepOnSplit: false,
inclusive() {
return this.options.autolink;
},
addOptions() {

@@ -13,2 +131,3 @@ return {

linkOnPaste: true,
autolink: true,
HTMLAttributes: {

@@ -36,3 +155,7 @@ target: '_blank',

renderHTML({ HTMLAttributes }) {
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
return [
'a',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},

@@ -74,45 +197,16 @@ addCommands() {

const plugins = [];
if (this.options.autolink) {
plugins.push(autolink({
type: this.type,
}));
}
if (this.options.openOnClick) {
plugins.push(new Plugin({
key: new PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
var _a;
const attrs = this.editor.getAttributes(this.name);
const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
if (link && attrs.href) {
window.open(attrs.href, attrs.target);
return true;
}
return false;
},
},
plugins.push(clickHandler({
type: this.type,
}));
}
if (this.options.linkOnPaste) {
plugins.push(new Plugin({
key: new PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
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)
.find(item => item.isLink && item.value === textContent);
if (!textContent || !link) {
return false;
}
this.editor.commands.setMark(this.type, {
href: link.href,
});
return true;
},
},
plugins.push(pasteHandler({
editor: this.editor,
type: this.type,
}));

@@ -119,0 +213,0 @@ }

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('prosemirror-state'), require('linkifyjs')) :
typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', 'prosemirror-state', 'linkifyjs'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-link"] = {}, global.core, global.prosemirrorState, global.linkifyjs));
})(this, (function (exports, core, prosemirrorState, linkifyjs) { 'use strict';
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('linkifyjs'), require('prosemirror-state')) :
typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', 'linkifyjs', 'prosemirror-state'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-link"] = {}, global.core, global.linkifyjs, global.prosemirrorState));
})(this, (function (exports, core, linkifyjs, prosemirrorState) { 'use strict';
function autolink(options) {
return new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('autolink'),
appendTransaction: (transactions, oldState, newState) => {
const docChanges = transactions.some(transaction => transaction.docChanged)
&& !oldState.doc.eq(newState.doc);
if (!docChanges) {
return;
}
const { tr } = newState;
const transform = core.combineTransactionSteps(oldState.doc, transactions);
const { mapping } = transform;
const changes = core.getChangedRanges(transform);
changes.forEach(({ oldRange, newRange }) => {
// at first we check if we have to remove links
core.getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
.filter(item => item.mark.type === options.type)
.forEach(oldMark => {
const newFrom = mapping.map(oldMark.from);
const newTo = mapping.map(oldMark.to);
const newMarks = core.getMarksBetween(newFrom, newTo, newState.doc)
.filter(item => item.mark.type === options.type);
if (!newMarks.length) {
return;
}
const newMark = newMarks[0];
const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to);
const newLinkText = newState.doc.textBetween(newMark.from, newMark.to);
const wasLink = linkifyjs.test(oldLinkText);
const isLink = linkifyjs.test(newLinkText);
// remove only the link, if it was a link before too
// because we don’t want to remove links that were set manually
if (wasLink && !isLink) {
tr.removeMark(newMark.from, newMark.to, options.type);
}
});
// now let’s see if we can add new links
core.findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
.forEach(textBlock => {
linkifyjs.find(textBlock.node.textContent)
.filter(link => link.isLink)
// calculate link position
.map(link => ({
...link,
from: textBlock.pos + link.start + 1,
to: textBlock.pos + link.end + 1,
}))
// check if link is within the changed range
.filter(link => {
const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
return fromIsInRange || toIsInRange;
})
// add link mark
.forEach(link => {
tr.addMark(link.from, link.to, options.type.create({
href: link.href,
}));
});
});
});
if (!tr.steps.length) {
return;
}
return tr;
},
});
}
function clickHandler(options) {
return new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
var _a;
const attrs = core.getAttributes(view.state, options.type.name);
const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
if (link && attrs.href) {
window.open(attrs.href, attrs.target);
return true;
}
return false;
},
},
});
}
function pasteHandler(options) {
return new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
const { state } = view;
const { selection } = state;
const { empty } = selection;
if (empty) {
return false;
}
let textContent = '';
slice.content.forEach(node => {
textContent += node.textContent;
});
const link = linkifyjs.find(textContent).find(item => item.isLink && item.value === textContent);
if (!textContent || !link) {
return false;
}
options.editor.commands.setMark(options.type, {
href: link.href,
});
return true;
},
},
});
}
const Link = core.Mark.create({
name: 'link',
priority: 1000,
inclusive: false,
keepOnSplit: false,
inclusive() {
return this.options.autolink;
},
addOptions() {

@@ -15,2 +133,3 @@ return {

linkOnPaste: true,
autolink: true,
HTMLAttributes: {

@@ -38,3 +157,7 @@ target: '_blank',

renderHTML({ HTMLAttributes }) {
return ['a', core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
return [
'a',
core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},

@@ -76,45 +199,16 @@ addCommands() {

const plugins = [];
if (this.options.autolink) {
plugins.push(autolink({
type: this.type,
}));
}
if (this.options.openOnClick) {
plugins.push(new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
var _a;
const attrs = this.editor.getAttributes(this.name);
const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
if (link && attrs.href) {
window.open(attrs.href, attrs.target);
return true;
}
return false;
},
},
plugins.push(clickHandler({
type: this.type,
}));
}
if (this.options.linkOnPaste) {
plugins.push(new prosemirrorState.Plugin({
key: new prosemirrorState.PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
const { state } = view;
const { selection } = state;
const { empty } = selection;
if (empty) {
return false;
}
let textContent = '';
slice.content.forEach(node => {
textContent += node.textContent;
});
const link = linkifyjs.find(textContent)
.find(item => item.isLink && item.value === textContent);
if (!textContent || !link) {
return false;
}
this.editor.commands.setMark(this.type, {
href: link.href,
});
return true;
},
},
plugins.push(pasteHandler({
editor: this.editor,
type: this.type,
}));

@@ -121,0 +215,0 @@ }

{
"name": "@tiptap/extension-link",
"description": "link extension for tiptap",
"version": "2.0.0-beta.30",
"version": "2.0.0-beta.31",
"homepage": "https://tiptap.dev",

@@ -28,2 +28,3 @@ "keywords": [

"linkifyjs": "^3.0.4",
"prosemirror-model": "^1.15.0",
"prosemirror-state": "^1.3.4"

@@ -36,3 +37,3 @@ },

},
"gitHead": "6360278660d9c1e256975699d911a55dfa943d5d"
"gitHead": "4e1a50250bc5d3ef916326e5bed30c93448284b3"
}

@@ -1,11 +0,13 @@

import {
Mark,
markPasteRule,
mergeAttributes,
} from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'
import { find } from 'linkifyjs'
import autolink from './helpers/autolink'
import clickHandler from './helpers/clickHandler'
import pasteHandler from './helpers/pasteHandler'
export interface LinkOptions {
/**
* If enabled, it adds links as you type.
*/
autolink: boolean,
/**
* If enabled, links will be opened on click.

@@ -48,4 +50,8 @@ */

inclusive: false,
keepOnSplit: false,
inclusive() {
return this.options.autolink
},
addOptions() {

@@ -55,2 +61,3 @@ return {

linkOnPaste: true,
autolink: true,
HTMLAttributes: {

@@ -81,3 +88,7 @@ target: '_blank',

renderHTML({ HTMLAttributes }) {
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
return [
'a',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
]
},

@@ -90,5 +101,7 @@

},
toggleLink: attributes => ({ commands }) => {
return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
},
unsetLink: () => ({ commands }) => {

@@ -121,60 +134,19 @@ return commands.unsetMark(this.name, { extendEmptyMarkRange: true })

if (this.options.autolink) {
plugins.push(autolink({
type: this.type,
}))
}
if (this.options.openOnClick) {
plugins.push(
new Plugin({
key: new PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
const attrs = this.editor.getAttributes(this.name)
const link = (event.target as HTMLElement)?.closest('a')
if (link && attrs.href) {
window.open(attrs.href, attrs.target)
return true
}
return false
},
},
}),
)
plugins.push(clickHandler({
type: this.type,
}))
}
if (this.options.linkOnPaste) {
plugins.push(
new Plugin({
key: new PluginKey('handlePasteLink'),
props: {
handlePaste: (view, event, slice) => {
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)
.find(item => item.isLink && item.value === textContent)
if (!textContent || !link) {
return false
}
this.editor.commands.setMark(this.type, {
href: link.href,
})
return true
},
},
}),
)
plugins.push(pasteHandler({
editor: this.editor,
type: this.type,
}))
}

@@ -181,0 +153,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc