Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions cypress/e2e/nodes/Preview.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ describe('Preview extension', { retries: 0 }, () => {
expect(editor.can().setPreview()).to.be.false
})

it('cannot run on a paragraph with other content', () => {
prepareEditor('[link text](https://example.org) hello\n')
expect(editor.can().setPreview()).to.be.false
})

it('convert a paragraph with a link', () => {
prepareEditor('[link text](https://example.org)\n')
editor.commands.setPreview()
Expand Down
75 changes: 60 additions & 15 deletions src/nodes/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { Node, getNodeType, isNodeActive } from '@tiptap/core'
import { Node, getMarkRange, getNodeType, isNodeActive } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import { domHref, isLinkToSelfWithHash, parseHref } from './../helpers/links.js'

Expand Down Expand Up @@ -78,15 +78,49 @@
setPreview:
() =>
({ state, chain }) => {
return (
previewPossible(state)
&& chain()
if (!previewPossible(state)) {
return false
}

const { $from } = state.selection

if (!hasOtherContent($from.parent)) {
// Paragraph contains only a link
return chain()
.setNode(
this.name,
previewAttributesFromSelection(state),
)
.run()
)
}

if ($from.parent.type.name !== 'paragraph') {
return false
}

const previewAttrs = previewAttributesFromSelection(state)

// Link is surrounded by other text in a paragraph
const range = getMarkRange($from, state.schema.marks.link)
if (!range) {
return false
}

const hasContentBefore = range.from > $from.start()
const hasContentAfter = range.to < $from.end()

let c = chain()

if (hasContentAfter) {
c = c.setTextSelection(range.to).splitBlock()
}
if (hasContentBefore) {
c = c.setTextSelection(range.from).splitBlock()
} else {
c = c.setTextSelection($from.before() + 1)
}

return c.setNode(this.name, previewAttrs).run()
},

/**
Expand Down Expand Up @@ -133,16 +167,29 @@
},
})

/**
*
* @param {object} state the editor state
* @returns {object|null} the link node or null

Check warning on line 173 in src/nodes/Preview.js

View workflow job for this annotation

GitHub Actions / NPM lint

Invalid JSDoc tag (preference). Replace "returns" JSDoc tag with "return"
*/
function getLinkAtCursor(state) {
const { $from } = state.selection
const range = getMarkRange($from, state.schema.marks.link)
if (!range) {
return null
}
return state.doc.resolve(range.from).nodeAfter
}

/**
* Attributes for a preview from link in the current selection
*
* @param {object} state the edior state
* @param {object} state.selection current selection
* @return {object}
*/
function previewAttributesFromSelection({ selection }) {
const { $from } = selection
const href = extractHref($from.nodeAfter)
function previewAttributesFromSelection(state) {
const linkNode = getLinkAtCursor(state)
const href = extractHref(linkNode)
return { href, title: 'preview' }
}

Expand All @@ -160,16 +207,14 @@

/**
* Is it possible to convert the currently selected node into a preview?
*
* @param {object} state current editor state
* @param {object} state.selection current selection
* @return {boolean}
*/
function previewPossible({ selection }) {
const { $from } = selection
if (hasOtherContent($from.parent)) {
return false
}
const href = extractHref($from.parent.firstChild)
function previewPossible(state) {
const linkNode = getLinkAtCursor(state)
const href = extractHref(linkNode)
if (!href || isLinkToSelfWithHash(href)) {
return false
}
Expand Down
83 changes: 83 additions & 0 deletions src/tests/nodes/Preview.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,86 @@
expect(node.attrs.href).toBe('https://example.org')
})
})

describe('setPreview Tiptap command', () => {
function findLinkRange(doc) {

Check warning on line 60 in src/tests/nodes/Preview.spec.js

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc comment
let from = null
let to = null

doc.descendants((node, pos) => {
if (
from === null
&& node.isText
&& node.marks.some((m) => m.type.name === 'link')
) {
from = pos
to = pos + node.nodeSize
}
})
return { from, to }
}

test('returns false on plain text', ({ editor }) => {
editor.commands.setContent('<p>plain text</p>')
const result = editor.commands.setPreview()
expect(result).toBe(false)
})

test('converts standalone link with cursor at start', ({ editor }) => {
editor.commands.setContent('<p><a href="https://example.org">link</a></p>')
const { from } = findLinkRange(editor.state.doc)
editor.commands.setTextSelection(from)
editor.commands.setPreview()
expect(editor.state.doc.childCount).toBe(1)
expect(editor.state.doc.firstChild.type.name).toBe('preview')
expect(editor.state.doc.firstChild.attrs.href).toBe('https://example.org')
})

test('converts standalone link with cursor at end of link text', ({
editor,
}) => {
editor.commands.setContent('<p><a href="https://example.org">link</a></p>')
const { to } = findLinkRange(editor.state.doc)
editor.commands.setTextSelection(to)
editor.commands.setPreview()
expect(editor.state.doc.childCount).toBe(1)
expect(editor.state.doc.firstChild.type.name).toBe('preview')
expect(editor.state.doc.firstChild.attrs.href).toBe('https://example.org')
})

test('splits inline link with text on both sides', ({ editor }) => {
editor.commands.setContent(
'<p>before <a href="https://example.org">link</a> after</p>',
)
const { to } = findLinkRange(editor.state.doc)
editor.commands.setTextSelection(to)
editor.commands.setPreview()
expect(editor.state.doc.childCount).toBe(3)
expect(editor.state.doc.child(1).type.name).toBe('preview')
expect(editor.state.doc.child(1).attrs.href).toBe('https://example.org')
})

test('splits inline link with text before only', ({ editor }) => {
editor.commands.setContent(
'<p>before <a href="https://example.org">link</a></p>',
)
const { to } = findLinkRange(editor.state.doc)
editor.commands.setTextSelection(to)
editor.commands.setPreview()
expect(editor.state.doc.childCount).toBe(2)
expect(editor.state.doc.child(1).type.name).toBe('preview')
expect(editor.state.doc.child(1).attrs.href).toBe('https://example.org')
})

test('splits inline link with text after only', ({ editor }) => {
editor.commands.setContent(
'<p><a href="https://example.org">link</a> after</p>',
)
const { to } = findLinkRange(editor.state.doc)
editor.commands.setTextSelection(to)
editor.commands.setPreview()
expect(editor.state.doc.childCount).toBe(2)
expect(editor.state.doc.firstChild.type.name).toBe('preview')
expect(editor.state.doc.firstChild.attrs.href).toBe('https://example.org')
})
})
Loading