diff --git a/.changeset/wicked-meals-shop.md b/.changeset/wicked-meals-shop.md new file mode 100644 index 000000000..45b43dd07 --- /dev/null +++ b/.changeset/wicked-meals-shop.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +getMarkRange would greedily match more content than it should have if it was the same type of mark, now it will match only the mark at the position #3872 diff --git a/packages/core/src/helpers/getMarkRange.ts b/packages/core/src/helpers/getMarkRange.ts index e152cd1f1..b7dd21688 100644 --- a/packages/core/src/helpers/getMarkRange.ts +++ b/packages/core/src/helpers/getMarkRange.ts @@ -9,7 +9,14 @@ function findMarkInSet( attributes: Record = {}, ): ProseMirrorMark | undefined { return marks.find(item => { - return item.type === type && objectIncludes(item.attrs, attributes) + return ( + item.type === type + && objectIncludes( + // Only check equality for the attributes that are provided + Object.fromEntries(Object.keys(attributes).map(k => [k, item.attrs[k]])), + attributes, + ) + ) }) } @@ -21,10 +28,23 @@ function isMarkInSet( return !!findMarkInSet(marks, type, attributes) } +/** + * Get the range of a mark at a resolved position. + */ export function getMarkRange( + /** + * The position to get the mark range for. + */ $pos: ResolvedPos, + /** + * The mark type to get the range for. + */ type: MarkType, - attributes: Record = {}, + /** + * The attributes to match against. + * If not provided, only the first mark at the position will be matched. + */ + attributes?: Record, ): Range | void { if (!$pos || !type) { return @@ -41,6 +61,9 @@ export function getMarkRange( return } + // Default to only matching against the first mark's attributes + attributes = attributes || start.node.marks[0]?.attrs + // 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) @@ -54,9 +77,10 @@ export function getMarkRange( let endIndex = startIndex + 1 let endPos = startPos + start.node.nodeSize - findMarkInSet([...start.node.marks], type, attributes) - - while (startIndex > 0 && mark.isInSet($pos.parent.child(startIndex - 1).marks)) { + while ( + startIndex > 0 + && isMarkInSet([...$pos.parent.child(startIndex - 1).marks], type, attributes) + ) { startIndex -= 1 startPos -= $pos.parent.child(startIndex).nodeSize } diff --git a/tests/cypress/integration/core/getMarkRange.spec.ts b/tests/cypress/integration/core/getMarkRange.spec.ts index ac0b9565f..3fc019800 100644 --- a/tests/cypress/integration/core/getMarkRange.spec.ts +++ b/tests/cypress/integration/core/getMarkRange.spec.ts @@ -140,4 +140,38 @@ describe('getMarkRange', () => { to: 39, }) }) + it('can distinguish mark boundaries', () => { + 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: 'text', text: 'another link', marks: [{ type: 'link', attrs: { href: 'https://tiptap.dev/invalid' } }] }, + ], + }, + { + type: 'paragraph', + content: [ + { type: 'text', text: 'This is a text without a link.' }, + ], + }, + ], + } + + const doc = Node.fromJSON(schema, testDocument) + const $pos = doc.resolve(27) + const range = getMarkRange($pos, schema.marks.link, { href: 'https://tiptap.dev' }) + + expect(range).to.deep.eq({ + from: 23, + to: 28, + }) + + const nextRange = getMarkRange(doc.resolve(28), schema.marks.link) + + expect(nextRange).to.deep.eq({ from: 28, to: 40 }) + }) })