Skip to content

Commit

Permalink
feat(extension-link): ✨ add validate option to link extension
Browse files Browse the repository at this point in the history
  • Loading branch information
bdbch committed May 16, 2022
1 parent ccc37d5 commit 23e67ad
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 6 deletions.
Empty file.
32 changes: 32 additions & 0 deletions demos/src/Examples/AutolinkValidation/React/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import './styles.scss'

import React from 'react'

import Link from '@tiptap/extension-link'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

export default () => {
const editor = useEditor({
extensions: [
StarterKit,
Link.configure({
validate: link => /^https?:\/\//.test(link),
}),
],
content: `
<p>Hey! Try to type in url with and without a http/s protocol. - Links without a protocol should not get auto linked</p>
`,
editorProps: {
attributes: {
spellcheck: 'false',
},
},
})

return (
<div>
<EditorContent editor={editor} />
</div>
)
}
42 changes: 42 additions & 0 deletions demos/src/Examples/AutolinkValidation/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
context('/src/Examples/AutolinkValidation/React/', () => {
before(() => {
cy.visit('/src/Examples/AutolinkValidation/React/')
})

beforeEach(() => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
})

const validLinks = [
'https://tiptap.dev',
'http://tiptap.dev',
'https://www.tiptap.dev/',
'http://www.tiptap.dev/',
]

const invalidLinks = [
'tiptap.dev',
'www.tiptap.dev',
]

validLinks.forEach(link => {
it(`${link} should get autolinked`, () => {
cy.get('.ProseMirror').type(link)
cy.get('.ProseMirror').should('have.text', link)
cy.get('.ProseMirror')
.find('a')
.should('have.length', 1)
.should('have.attr', 'href', link)
})
})

invalidLinks.forEach(link => {
it(`${link} should NOT get autolinked`, () => {
cy.get('.ProseMirror').type(link)
cy.get('.ProseMirror').should('have.text', link)
cy.get('.ProseMirror')
.find('a')
.should('have.length', 0)
})
})
})
54 changes: 54 additions & 0 deletions demos/src/Examples/AutolinkValidation/React/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}

ul,
ol {
padding: 0 1rem;
}

h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}

code {
background-color: rgba(#616161, 0.1);
color: #616161;
}

pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;

code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}

img {
max-width: 100%;
height: auto;
}

hr {
margin: 1rem 0;
}

blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
}
Empty file.
7 changes: 7 additions & 0 deletions demos/src/Examples/AutolinkValidation/Vue/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
context('/src/Examples/AutolinkValidation/Vue/', () => {
before(() => {
cy.visit('/src/Examples/AutolinkValidation/Vue/')
})

// TODO: Write tests
})
101 changes: 101 additions & 0 deletions demos/src/Examples/AutolinkValidation/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<editor-content :editor="editor" />
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
Link.configure({
validate: link => /^https?:\/\//.test(link),
}),
],
content: `
<p>Hey! Try to type in url with and without a http/s protocol. - Links without a protocol should not get auto linked</p>
`,
editorProps: {
attributes: {
spellcheck: 'false',
},
},
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>

<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
hr {
margin: 1rem 0;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
}
</style>
12 changes: 12 additions & 0 deletions docs/api/marks/link.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ Link.configure({
})
```

### validate
A function that validates every autolinked link. If it exists, it will be called with the link href as argument. If it returns `false`, the link will be removed.

Can be used to set rules for example excluding or including certain domains, tlds, etc.

```js
// only autolink urls with a protocol
Link.configure({
validate: href => /^https?:\/\//.test(href),
})
```

## Commands

### setLink()
Expand Down
19 changes: 14 additions & 5 deletions packages/extension-link/src/helpers/autolink.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { find, test } from 'linkifyjs'
import { MarkType } from 'prosemirror-model'
import { Plugin, PluginKey } from 'prosemirror-state'

import {
getMarksBetween,
findChildrenInRange,
combineTransactionSteps,
findChildrenInRange,
getChangedRanges,
getMarksBetween,
} from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
import { MarkType } from 'prosemirror-model'
import { find, test } from 'linkifyjs'

type AutolinkOptions = {
type: MarkType,
validate?: (url: string) => boolean,
}

export function autolink(options: AutolinkOptions): Plugin {
Expand Down Expand Up @@ -70,6 +72,13 @@ export function autolink(options: AutolinkOptions): Plugin {

find(text)
.filter(link => link.isLink)
.filter(link => {
if (options.validate) {
return options.validate(link.value)
}

return true
})
// calculate link position
.map(link => ({
...link,
Expand Down
12 changes: 11 additions & 1 deletion packages/extension-link/src/link.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'
import { find } from 'linkifyjs'

import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'

import { autolink } from './helpers/autolink'
import { clickHandler } from './helpers/clickHandler'
import { pasteHandler } from './helpers/pasteHandler'
Expand All @@ -21,6 +23,12 @@ export interface LinkOptions {
* A list of HTML attributes to be rendered.
*/
HTMLAttributes: Record<string, any>,
/**
* 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,
}

declare module '@tiptap/core' {
Expand Down Expand Up @@ -63,6 +71,7 @@ export const Link = Mark.create<LinkOptions>({
rel: 'noopener noreferrer nofollow',
class: null,
},
validate: undefined,
}
},

Expand Down Expand Up @@ -143,6 +152,7 @@ export const Link = Mark.create<LinkOptions>({
if (this.options.autolink) {
plugins.push(autolink({
type: this.type,
validate: this.options.validate,
}))
}

Expand Down

0 comments on commit 23e67ad

Please sign in to comment.