Skip to content

Commit f01a424

Browse files
committed
fix: #3813
1 parent e7127e8 commit f01a424

File tree

4 files changed

+98
-17
lines changed

4 files changed

+98
-17
lines changed

.changeset/fresh-carrots-smile.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@udecode/plate-find-replace': minor
3+
---
4+
5+
fix: FindReplacePlugin supports matching consecutive text nodes

packages/find-replace/src/lib/__tests__/decorateSearchHighlight/search/empty.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ it('should be', () => {
1616
expect(
1717
decorateFindReplace({
1818
...getEditorPlugin(editor, FindReplacePlugin),
19-
entry: [{ text: '' }, [0, 0]],
19+
entry: [{ type: 'p', children: [{ text: '' }] }, [0]],
2020
})
2121
).toEqual(output);
2222
});

packages/find-replace/src/lib/__tests__/decorateSearchHighlight/search/text.spec.ts

+47-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ it('should decorate matching text', () => {
1515
expect(
1616
plugin.decorate?.({
1717
...getEditorPlugin(editor, plugin),
18-
entry: [{ text: 'test' }, [0, 0]],
18+
entry: [{ type: 'p', children: [{ text: 'test' }] }, [0]],
1919
})
2020
).toEqual([
2121
{
@@ -45,7 +45,7 @@ it('should decorate matching text case-insensitively', () => {
4545
expect(
4646
plugin.decorate?.({
4747
...getEditorPlugin(editor, plugin),
48-
entry: [{ text: 'test' }, [0, 0]],
48+
entry: [{ type: 'p', children: [{ text: 'test' }] }, [0]],
4949
})
5050
).toEqual([
5151
{
@@ -62,3 +62,48 @@ it('should decorate matching text case-insensitively', () => {
6262
},
6363
]);
6464
});
65+
66+
it('should decorate matching consecutive text nodes', () => {
67+
const editor = createSlateEditor({
68+
plugins: [FindReplacePlugin],
69+
});
70+
71+
const plugin = editor.getPlugin(FindReplacePlugin);
72+
73+
editor.setOption(FindReplacePlugin, 'search', 'test');
74+
75+
expect(
76+
plugin.decorate?.({
77+
...getEditorPlugin(editor, plugin),
78+
entry: [
79+
{ type: 'p', children: [{ text: 'tes' }, { text: 't', bold: true }] },
80+
[0],
81+
],
82+
})
83+
).toEqual([
84+
{
85+
[FindReplacePlugin.key]: true,
86+
anchor: {
87+
offset: 0,
88+
path: [0, 0],
89+
},
90+
focus: {
91+
offset: 3,
92+
path: [0, 0],
93+
},
94+
search: 'tes',
95+
},
96+
{
97+
[FindReplacePlugin.key]: true,
98+
anchor: {
99+
offset: 0,
100+
path: [0, 1],
101+
},
102+
focus: {
103+
offset: 1,
104+
path: [0, 1],
105+
},
106+
search: 't',
107+
},
108+
]);
109+
});

packages/find-replace/src/lib/decorateFindReplace.ts

+45-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Decorate } from '@udecode/plate-common';
22
import type { Range } from 'slate';
33

4-
import { isText } from '@udecode/plate-common';
4+
import { isElement, isText } from '@udecode/plate-common';
55

66
import type { FindReplaceConfig } from './FindReplacePlugin';
77

@@ -12,27 +12,58 @@ export const decorateFindReplace: Decorate<FindReplaceConfig> = ({
1212
}) => {
1313
const { search } = getOptions();
1414

15-
const ranges: SearchRange[] = [];
15+
if (!(search && isElement(node) && node.children.every(isText))) {
16+
return [];
17+
}
18+
19+
const texts = node.children.map((it) => it.text);
1620

17-
if (!search || !isText(node)) {
18-
return ranges;
21+
// Try to find a match
22+
const matchStart = texts.join('').toLowerCase().indexOf(search.toLowerCase());
23+
if (matchStart === -1) {
24+
return [];
1925
}
2026

21-
const { text } = node;
22-
const parts = text.toLowerCase().split(search.toLowerCase());
23-
let offset = 0;
24-
parts.forEach((part, i) => {
25-
if (i !== 0) {
27+
const matchEnd = matchStart + search.length;
28+
let cumulativePosition = 0;
29+
const ranges: SearchRange[] = [];
30+
31+
for (const [i, text] of texts.entries()) {
32+
const textStart = cumulativePosition;
33+
const textEnd = cumulativePosition + text.length;
34+
35+
// Corresponding offsets within the text string
36+
const overlapStart = Math.max(matchStart, textStart);
37+
const overlapEnd = Math.min(matchEnd, textEnd);
38+
39+
if (overlapStart < overlapEnd) {
40+
// Overlapping region exists
41+
const anchorOffset = overlapStart - textStart;
42+
const focusOffset = overlapEnd - textStart;
43+
44+
// Corresponding offsets within the search string
45+
const searchOverlapStart = overlapStart - matchStart;
46+
const searchOverlapEnd = overlapEnd - matchStart;
47+
48+
const textNodePath = [...path, i];
49+
2650
ranges.push({
27-
anchor: { offset: offset - search.length, path },
28-
focus: { offset, path },
29-
search,
51+
anchor: {
52+
path: textNodePath,
53+
offset: anchorOffset,
54+
},
55+
focus: {
56+
path: textNodePath,
57+
offset: focusOffset,
58+
},
59+
search: search.substring(searchOverlapStart, searchOverlapEnd),
3060
[type]: true,
3161
});
3262
}
3363

34-
offset = offset + part.length + search.length;
35-
});
64+
// Update the cumulative position for the next iteration
65+
cumulativePosition = textEnd;
66+
}
3667

3768
return ranges;
3869
};

0 commit comments

Comments
 (0)