fix(core): getMarkRange match only the current mark of a type #3872 (#5826)
Some checks failed
build / lint (20) (push) Has been cancelled
build / test (20, map[name:Demos/Examples spec:./demos/src/Examples/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Experiments spec:./demos/src/Experiments/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Extensions spec:./demos/src/Extensions/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/GuideContent spec:./demos/src/GuideContent/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/GuideGettingStarted spec:./demos/src/GuideGettingStarted/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Marks spec:./demos/src/Marks/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Nodes spec:./demos/src/Nodes/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Integration spec:./tests/cypress/integration/**/*.spec.{js,ts}]) (push) Has been cancelled
Publish / Release (20) (push) Has been cancelled
build / build (20) (push) Has been cancelled

This commit is contained in:
Nick Perez 2024-11-17 18:35:59 +01:00 committed by GitHub
parent d91f774e11
commit 2ea807d1db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 5 deletions

View File

@ -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

View File

@ -9,7 +9,14 @@ function findMarkInSet(
attributes: Record<string, any> = {}, attributes: Record<string, any> = {},
): ProseMirrorMark | undefined { ): ProseMirrorMark | undefined {
return marks.find(item => { 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) return !!findMarkInSet(marks, type, attributes)
} }
/**
* Get the range of a mark at a resolved position.
*/
export function getMarkRange( export function getMarkRange(
/**
* The position to get the mark range for.
*/
$pos: ResolvedPos, $pos: ResolvedPos,
/**
* The mark type to get the range for.
*/
type: MarkType, type: MarkType,
attributes: Record<string, any> = {}, /**
* The attributes to match against.
* If not provided, only the first mark at the position will be matched.
*/
attributes?: Record<string, any>,
): Range | void { ): Range | void {
if (!$pos || !type) { if (!$pos || !type) {
return return
@ -41,6 +61,9 @@ export function getMarkRange(
return 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 // 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 // so we can look it up on the targeted mark
const mark = findMarkInSet([...start.node.marks], type, attributes) const mark = findMarkInSet([...start.node.marks], type, attributes)
@ -54,9 +77,10 @@ export function getMarkRange(
let endIndex = startIndex + 1 let endIndex = startIndex + 1
let endPos = startPos + start.node.nodeSize let endPos = startPos + start.node.nodeSize
findMarkInSet([...start.node.marks], type, attributes) while (
startIndex > 0
while (startIndex > 0 && mark.isInSet($pos.parent.child(startIndex - 1).marks)) { && isMarkInSet([...$pos.parent.child(startIndex - 1).marks], type, attributes)
) {
startIndex -= 1 startIndex -= 1
startPos -= $pos.parent.child(startIndex).nodeSize startPos -= $pos.parent.child(startIndex).nodeSize
} }

View File

@ -140,4 +140,38 @@ describe('getMarkRange', () => {
to: 39, 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 })
})
}) })