Skip to content

Commit 281aebe

Browse files
authored
Merge pull request #90 from badasswp/fix/make-regex-opt-in-for-search-replace
Fix: Make regex opt in for Search & Replace
2 parents cd939e7 + 2b1fde3 commit 281aebe

File tree

6 files changed

+89
-6
lines changed

6 files changed

+89
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Feat: Add Plugin options page.
77
* Feat: Add Shortcut command (CMD + F).
88
* Feat: Add custom hooks: `afterSearchReplace`, `excludedPostTypes`, `regexPattern`.
9+
* Fix: Make default search literal & regex opt-in.
910
* Refactor: Use `replaceInput` in place of repeated instances of `text`.
1011
* Test: Add e2e tests for plugin codebase.
1112
* Tested up to WP 6.9.

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Want to add your personal touch? All of our documentation can be found [here](ht
6969
* Feat: Add Plugin options page.
7070
* Feat: Add Shortcut command (CMD + F).
7171
* Feat: Add custom hooks: `afterSearchReplace`, `excludedPostTypes`, `regexPattern`.
72+
* Fix: Make default search literal & regex opt-in.
7273
* Refactor: Use `replaceInput` in place of repeated instances of `text`.
7374
* Test: Add e2e tests for plugin codebase.
7475
* Tested up to WP 6.9.

src/core/app.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313

1414
import { Shortcut } from './shortcut';
1515
import {
16+
getPattern,
17+
escapeRegex,
1618
getAllowedBlocks,
1719
getBlockEditorIframe,
1820
ifIsCaseSensitiveBasedOnFilter,
@@ -169,13 +171,27 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
169171
}
170172

171173
// Prepare raw string pattern.
172-
const rawPattern = `(?<!<[^>]*)${ searchInput }(?<![^>]*<)`;
174+
const rawPattern = getPattern(
175+
isRegexExpression ? searchInput : escapeRegex( searchInput )
176+
);
177+
178+
// Is Search case sensitive.
179+
const isSearchCaseSensitive =
180+
ifIsCaseSensitiveBasedOnFilter() || isCaseSensitive ? 'g' : 'gi';
181+
182+
// Define pattern.
183+
let regexPattern: RegExp;
173184

174185
// Get Regex search pattern.
175-
const regexPattern: RegExp = new RegExp(
176-
rawPattern,
177-
ifIsCaseSensitiveBasedOnFilter() || isCaseSensitive ? 'g' : 'gi'
178-
);
186+
try {
187+
regexPattern = new RegExp( rawPattern, isSearchCaseSensitive );
188+
} catch ( error ) {
189+
// fallback to non-regex pattern.
190+
regexPattern = new RegExp(
191+
getPattern( escapeRegex( searchInput ) ),
192+
isSearchCaseSensitive
193+
);
194+
}
179195

180196
/**
181197
* Filter the way we set the pattern, let users

src/core/filters.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ addFilter(
7373
);
7474

7575
return {
76-
newAttr: JSON.parse( tableString ),
76+
newAttr: JSON.parse( tableString || '{}' ),
7777
isChanged: tableString !== JSON.stringify( oldAttr ),
7878
};
7979

src/core/utils.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,34 @@ export const isAllowedForPostType = (): boolean => {
397397

398398
return true;
399399
};
400+
401+
/**
402+
* Escape user input for safe
403+
* literal RegExp usage.
404+
*
405+
* @since 1.10.0
406+
*
407+
* @param {string} value Raw user input.
408+
* @return {string} Escaped input.
409+
*/
410+
export const escapeRegex = ( value: string ): string => {
411+
if ( typeof value !== 'string' ) {
412+
return '';
413+
}
414+
415+
return value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
416+
};
417+
418+
/**
419+
* Get Search Pattern.
420+
*
421+
* This function returns a string pattern
422+
* that targets only texts within valid HTML markup.
423+
*
424+
* @since 1.10.0
425+
*
426+
* @param {string} searchText Search Text.
427+
* @return {string} Search Pattern.
428+
*/
429+
export const getPattern = ( searchText: string ): string =>
430+
`(?<!<[^>]*)${ searchText }(?<![^>]*<)`;

tests/unit/js/utils.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,37 @@ describe( 'getShortcutEvent', () => {
391391
expect( shortcutEvent ).toBeInstanceOf( KeyboardEvent );
392392
} );
393393
} );
394+
395+
describe( 'escapeRegex', () => {
396+
beforeEach( () => {
397+
jest.resetModules();
398+
} );
399+
400+
it( 'escapeRegex escapes regex metacharacters', () => {
401+
const { escapeRegex } = require( '../../../src/core/utils' );
402+
403+
const escaped = escapeRegex( 'a.b+c*^$()[]{}|\\' );
404+
405+
expect( escaped ).toBe( 'a\\.b\\+c\\*\\^\\$\\(\\)\\[\\]\\{\\}\\|\\\\' );
406+
} );
407+
408+
it( 'escapeRegex leaves plain text unchanged', () => {
409+
const { escapeRegex } = require( '../../../src/core/utils' );
410+
411+
const escaped = escapeRegex( 'plain text' );
412+
413+
expect( escaped ).toBe( 'plain text' );
414+
} );
415+
416+
it( 'escapeRegex returns empty string for non-string inputs', () => {
417+
const { escapeRegex } = require( '../../../src/core/utils' );
418+
419+
expect( escapeRegex( 123 ) ).toBe( '' );
420+
} );
421+
422+
it( 'escapeRegex returns empty string for empty input', () => {
423+
const { escapeRegex } = require( '../../../src/core/utils' );
424+
425+
expect( escapeRegex( '' ) ).toBe( '' );
426+
} );
427+
} );

0 commit comments

Comments
 (0)