Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #3813 #3815

Merged
merged 5 commits into from
Nov 27, 2024
Merged
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: 5 additions & 0 deletions .changeset/fresh-carrots-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-find-replace': patch
---

fix: FindReplacePlugin supports matching consecutive text nodes
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ it('should be', () => {
expect(
decorateFindReplace({
...getEditorPlugin(editor, FindReplacePlugin),
entry: [{ text: '' }, [0, 0]],
entry: [{ children: [{ text: '' }], type: 'p' }, [0]],
})
).toEqual(output);
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ it('should decorate matching text', () => {
expect(
plugin.decorate?.({
...getEditorPlugin(editor, plugin),
entry: [{ text: 'test' }, [0, 0]],
entry: [{ children: [{ text: 'test' }], type: 'p' }, [0]],
})
).toEqual([
{
Expand Down Expand Up @@ -45,7 +45,7 @@ it('should decorate matching text case-insensitively', () => {
expect(
plugin.decorate?.({
...getEditorPlugin(editor, plugin),
entry: [{ text: 'test' }, [0, 0]],
entry: [{ children: [{ text: 'test' }], type: 'p' }, [0]],
})
).toEqual([
{
Expand All @@ -62,3 +62,136 @@ it('should decorate matching text case-insensitively', () => {
},
]);
});

it('should decorate matching consecutive text nodes', () => {
const editor = createSlateEditor({
plugins: [FindReplacePlugin],
});

const plugin = editor.getPlugin(FindReplacePlugin);

editor.setOption(FindReplacePlugin, 'search', 'test');

expect(
plugin.decorate?.({
...getEditorPlugin(editor, plugin),
entry: [
{ children: [{ text: 'tes' }, { bold: true, text: 't' }], type: 'p' },
[0],
],
})
).toEqual([
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 0,
path: [0, 0],
},
focus: {
offset: 3,
path: [0, 0],
},
search: 'tes',
},
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 0,
path: [0, 1],
},
focus: {
offset: 1,
path: [0, 1],
},
search: 't',
},
]);
});

it('should decorate matching multiple occurrences', () => {
const editor = createSlateEditor({
plugins: [FindReplacePlugin],
});

const plugin = editor.getPlugin(FindReplacePlugin);

editor.setOption(FindReplacePlugin, 'search', 'test');

expect(
plugin.decorate?.({
...getEditorPlugin(editor, plugin),
entry: [
{
children: [
{ text: 'tes' },
{ bold: true, text: 'ts and tests and t' },
{ text: 'ests' },
],
type: 'p',
},
[0],
],
})
).toEqual([
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 0,
path: [0, 0],
},
focus: {
offset: 3,
path: [0, 0],
},
search: 'tes',
},
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 0,
path: [0, 1],
},
focus: {
offset: 1,
path: [0, 1],
},
search: 't',
},
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 7,
path: [0, 1],
},
focus: {
offset: 11,
path: [0, 1],
},
search: 'test',
},
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 17,
path: [0, 1],
},
focus: {
offset: 18,
path: [0, 1],
},
search: 't',
},
{
[FindReplacePlugin.key]: true,
anchor: {
offset: 0,
path: [0, 2],
},
focus: {
offset: 3,
path: [0, 2],
},
search: 'est',
},
]);
});
91 changes: 74 additions & 17 deletions packages/find-replace/src/lib/decorateFindReplace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Decorate } from '@udecode/plate-common';
import type { Range } from 'slate';

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

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

Expand All @@ -12,27 +12,84 @@ export const decorateFindReplace: Decorate<FindReplaceConfig> = ({
}) => {
const { search } = getOptions();

const ranges: SearchRange[] = [];
if (!(search && isElement(node) && node.children.every(isText))) {
return [];
}

const texts = node.children.map((it) => it.text);
const str = texts.join('').toLowerCase();
const searchLower = search.toLowerCase();

if (!search || !isText(node)) {
return ranges;
let start = 0;
const matches: number[] = [];

while ((start = str.indexOf(searchLower, start)) !== -1) {
matches.push(start);
start += searchLower.length;
}

const { text } = node;
const parts = text.toLowerCase().split(search.toLowerCase());
let offset = 0;
parts.forEach((part, i) => {
if (i !== 0) {
ranges.push({
anchor: { offset: offset - search.length, path },
focus: { offset, path },
search,
[type]: true,
});
if (matches.length === 0) {
return [];
}

const ranges: SearchRange[] = [];
let cumulativePosition = 0;
let matchIndex = 0; // Index in the matches array

for (const [textIndex, text] of texts.entries()) {
const textStart = cumulativePosition;
const textEnd = textStart + text.length;

// Process matches that overlap with the current text node
while (matchIndex < matches.length && matches[matchIndex] < textEnd) {
const matchStart = matches[matchIndex];
const matchEnd = matchStart + search.length;

// If the match ends before the start of the current text, move to the next match
if (matchEnd <= textStart) {
matchIndex++;

continue;
}

// Calculate overlap between the text and the current match
const overlapStart = Math.max(matchStart, textStart);
const overlapEnd = Math.min(matchEnd, textEnd);

if (overlapStart < overlapEnd) {
const anchorOffset = overlapStart - textStart;
const focusOffset = overlapEnd - textStart;

// Corresponding offsets within the search string
const searchOverlapStart = overlapStart - matchStart;
const searchOverlapEnd = overlapEnd - matchStart;

const textNodePath = [...path, textIndex];

ranges.push({
anchor: {
offset: anchorOffset,
path: textNodePath,
},
focus: {
offset: focusOffset,
path: textNodePath,
},
search: search.slice(searchOverlapStart, searchOverlapEnd),
[type]: true,
});
}
// If the match ends within the current text, move to the next match
if (matchEnd <= textEnd) {
matchIndex++;
} else {
// The match continues in the next text node
break;
}
}

offset = offset + part.length + search.length;
});
cumulativePosition = textEnd;
}

return ranges;
};
Expand Down