Skip to content

Commit

Permalink
Fix getMarkRange not finding marks when at the start of a mark (#5717)
Browse files Browse the repository at this point in the history
* fix getMarkRange to always find a mark, even if cursor is at start of mark

* added changesets

* added integration tests for getMarkRange

* remove console.logs

* added forward and backward boundary checks for tests
  • Loading branch information
bdbch authored Oct 11, 2024
1 parent d96f679 commit 4efd227
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-masks-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tiptap/core": patch
---

Fixed an issue with getMarkRange not returning the correct range when cursor is at the start of the specified mark
9 changes: 6 additions & 3 deletions packages/core/src/helpers/getMarkRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,20 @@ export function getMarkRange(
if (!$pos || !type) {
return
}

let start = $pos.parent.childAfter($pos.parentOffset)

if ($pos.parentOffset === start.offset && start.offset !== 0) {
// If the cursor is at the start of a text node that does not have the mark, look backward
if (!start.node || !start.node.marks.some(mark => mark.type === type)) {
start = $pos.parent.childBefore($pos.parentOffset)
}

if (!start.node) {
// If there is no text node with the mark even backward, return undefined
if (!start.node || !start.node.marks.some(mark => mark.type === type)) {
return
}

// We now know that the cursor is either at the start, middle or end of a text node with the specified mark
// so we can look it up on the targeted mark
const mark = findMarkInSet([...start.node.marks], type, attributes)

if (!mark) {
Expand Down
143 changes: 143 additions & 0 deletions tests/cypress/integration/core/getMarkRange.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/// <reference types="cypress" />

import {
getMarkRange,
getSchemaByResolvedExtensions,
} from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Link from '@tiptap/extension-link'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { Node } from '@tiptap/pm/model'

describe('getMarkRange', () => {
const document = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'This is a ' },
{ type: 'text', text: 'linked', marks: [{ type: 'link', attrs: { href: 'https://tiptap.dev' } }] },
{ type: 'text', text: ' text.' },
],
},
],
}

const schema = getSchemaByResolvedExtensions([
Document,
Paragraph,
Text,
Link.configure({ openOnClick: false }),
])

it('gets the correct range for a position inside the mark', () => {
const doc = Node.fromJSON(schema, document)
const $pos = doc.resolve(14)
const range = getMarkRange($pos, schema.marks.link)

expect(range).to.deep.eq({
from: 11,
to: 17,
})
})

it('gets the correct range for a position at the start of the mark', () => {
const doc = Node.fromJSON(schema, document)
const $pos = doc.resolve(11)
const range = getMarkRange($pos, schema.marks.link)

expect(range).to.deep.eq({
from: 11,
to: 17,
})
})

it('gets the correct range for a position at the end of the mark', () => {
const doc = Node.fromJSON(schema, document)
const $pos = doc.resolve(17)
const range = getMarkRange($pos, schema.marks.link)

expect(range).to.deep.eq({
from: 11,
to: 17,
})
})

it('gets undefined if a mark is not found', () => {
const doc = Node.fromJSON(schema, document)
const $pos = doc.resolve(6)
const range = getMarkRange($pos, schema.marks.link)

expect(range).to.eq(undefined)
})

it('doesnt cross node boundaries on backward check', () => {
const testDocument = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'This is a text with a ' },
{ type: 'text', text: 'link.', marks: [{ type: 'link', attrs: { href: 'https://tiptap.dev' } }] },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'This is a text without a link.' },
],
},
],
}

const doc = Node.fromJSON(schema, testDocument)
const $pos = doc.resolve(28)
const range = getMarkRange($pos, schema.marks.link)

expect(range).to.deep.eq({
from: 23,
to: 28,
})

const nextRange = getMarkRange(doc.resolve(30), schema.marks.link)

expect(nextRange).to.eq(undefined)
})

it('doesnt cross node boundaries on forward check', () => {
const testDocument = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'This is a text without a link.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'A link', marks: [{ type: 'link', attrs: { href: 'https://tiptap.dev' } }] },
{ type: 'text', text: ' is at the start of this paragraph.' },
],
},
],
}
const doc = Node.fromJSON(schema, testDocument)

const range = getMarkRange(doc.resolve(32), schema.marks.link)

expect(range).to.eq(undefined)

const $pos = doc.resolve(33)
const nextRange = getMarkRange($pos, schema.marks.link)

expect(nextRange).to.deep.eq({
from: 33,
to: 39,
})
})
})

0 comments on commit 4efd227

Please sign in to comment.