Skip to content
Open
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
52 changes: 32 additions & 20 deletions languages/search-replace-for-block-editor.pot
Original file line number Diff line number Diff line change
@@ -1,65 +1,77 @@
# Copyright (C) badasswp
# Copyright (C) 2026 badasswp
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
"Project-Id-Version: Search and Replace for Block Editor\n"
"POT-Creation-Date: 2024-07-2 00:00+0100\n"
"PO-Revision-Date: 2024-07-2 00:00+0100\n"
"Project-Id-Version: Search and Replace for Block Editor 1.9.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/search-and-replace\n"
"POT-Creation-Date: 2026-01-29T09:23:52+00:00\n"
"PO-Revision-Date: 2026-01-29T09:23:52+00:00\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Domain: search-replace-for-block-editor\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
"X-Generator: Poedit 3.4.1\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-Flags-xgettext: --add-comments=translators:\n"
"X-Poedit-WPHeader: search-replace-for-block-editor.php\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPathExcluded-0: *.min.js\n"

#. Plugin Name
#. Plugin Name of the plugin
#: search-replace-for-block-editor.php
msgid "Search and Replace for Block Editor"
msgstr ""

#. Plugin URI
#. Plugin URI of the plugin
#: search-replace-for-block-editor.php
msgid "https://github.com/badasswp/search-and-replace"
msgstr ""

#. Description
#. Description of the plugin
#: search-replace-for-block-editor.php
msgid "Search and Replace text within the Block Editor."
msgstr ""

#. Author
#. Author of the plugin
#: search-replace-for-block-editor.php
msgid "badasswp"
msgstr ""

#. Author URI
#. Author URI of the plugin
#: search-replace-for-block-editor.php
msgid "https://github.com/badasswp"
msgstr ""

#: dist/app.js
#. translators: Plugin directory path.
#: search-replace-for-block-editor.php:32
#, php-format
msgid "Fatal Error: Composer not setup in %s"
msgstr ""

#: dist/app.js:1
msgid "Search & Replace"
msgstr ""

#. Search
msgid "Search"
#: dist/app.js:1
msgid "Invalid regular expression."
msgstr ""

#. Replace
msgid "Replace"
#: dist/app.js:1
msgid "Match Case"
msgstr ""

#. Done
msgid "Done"
#: dist/app.js:1
msgid "Use Regular Expression"
msgstr ""

#. dist/app.js
#: dist/app.js:1
msgid "item(s) replaced successfully"
msgstr ""

#. dist/app.js
msgid "Match Case | Expression"
#: dist/app.js:1
msgid "item(s) found"
msgstr ""
118 changes: 112 additions & 6 deletions src/core/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ToggleControl,
Button,
Tooltip,
Notice,
} from '@wordpress/components';

import { Shortcut } from './shortcut';
Expand All @@ -18,6 +19,7 @@ import {
isCaseSensitive,
isSelectionInModal,
isWpVersionGreaterThanOrEqualTo,
escapeRegExp,
} from './utils';

import '../styles/app.scss';
Expand All @@ -38,7 +40,9 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
const [ searchInput, setSearchInput ] = useState< string >( '' );
const [ replaceInput, setReplaceInput ] = useState< string >( '' );
const [ caseSensitive, setCaseSensitive ] = useState< boolean >( false );
const [ useRegex, setUseRegex ] = useState< boolean >( false );
const [ context, setContext ] = useState< boolean >( false );
const [ error, setError ] = useState< string >( '' );

// Reference to the first field inside the modal.
const searchFieldRef = useRef< HTMLInputElement | null >( null );
Expand All @@ -63,6 +67,7 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
setReplacements( 0 );
setSearchInput( '' );
setReplaceInput( '' );
setError( '' );
}
};

Expand Down Expand Up @@ -91,6 +96,19 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
setCaseSensitive( newValue );
};

/**
* Handle regex toggle and persist preference.
*
* @since 1.10.0
*
* @param {boolean} newValue
* @return {void}
*/
const handleUseRegex = ( newValue: boolean ): void => {
setUseRegex( newValue );
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When toggling the regex option (line 107-110), the error state is not cleared. If a user enters invalid regex syntax while in regex mode (which shows an error), then toggles regex off, the error message will persist even though the input would now be treated as literal text and is valid. Consider clearing the error state when toggling useRegex by adding setError('') in the handleUseRegex function.

Suggested change
setUseRegex( newValue );
setUseRegex( newValue );
setError( '' );

Copilot uses AI. Check for mistakes.
persistRegexPreference( newValue );
};

/**
* Listen for changes to input or case-sensitivity
* and perform Searches only.
Expand All @@ -102,7 +120,7 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
useEffect( () => {
replace();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ searchInput, caseSensitive ] );
}, [ searchInput, caseSensitive, useRegex ] );

/**
* Modal Focus.
Expand All @@ -121,6 +139,63 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
}
}, [ isModalVisible ] );

/**
* Load persisted preference on first render.
*
* @since 1.10.0
*/
useEffect( () => {
setUseRegex( getPersistedRegexPreference() );
}, [] );

/**
* Get regex preference from core/preferences.
*
* @since 1.10.0
*
* @return {boolean} Whether regex is enabled.
*/
const getPersistedRegexPreference = (): boolean => {
const scope = 'search-replace-for-block-editor';
const key = 'useRegex';

try {
const prefSelect = select( 'core/preferences' ) as any;
if ( prefSelect?.get ) {
const value = prefSelect.get( scope, key );
if ( typeof value === 'boolean' ) {
return value;
}
}
} catch ( e ) {
// Ignore preference store errors.
}

return false;
};

/**
* Persist regex preference per user.
*
* @since 1.10.0
*
* @param {boolean} value
* @return {void}
*/
const persistRegexPreference = ( value: boolean ): void => {
const scope = 'search-replace-for-block-editor';
const key = 'useRegex';

try {
const prefDispatch = dispatch( 'core/preferences' ) as any;
if ( prefDispatch?.set ) {
prefDispatch.set( scope, key, value );
}
} catch ( e ) {
// Ignore preference store errors.
}
};

/**
* Handle the implementation for when the user
* clicks the 'Replace' button.
Expand All @@ -134,15 +209,31 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
const replace = ( status: boolean = false ): void => {
setContext( status );
setReplacements( 0 );
setError( '' );

if ( ! searchInput ) {
return;
}

const pattern: RegExp = new RegExp(
`(?<!<[^>]*)${ searchInput }(?<![^>]*<)`,
isCaseSensitive() || caseSensitive ? 'g' : 'gi'
);
const searchValue = useRegex
? searchInput
: escapeRegExp( searchInput );
let pattern: RegExp;

try {
pattern = new RegExp(
`(?<!<[^>]*)${ searchValue }(?<![^>]*<)`,
isCaseSensitive() || caseSensitive ? 'g' : 'gi'
);
} catch ( err ) {
setError(
__(
'Invalid regular expression.',
'search-replace-for-block-editor'
)
Comment on lines +229 to +233
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an invalid regex error is shown and the user modifies the search input to correct it, the error will be cleared because the replace() function is called via the useEffect hook at line 121. However, if there's an error and the user clicks the "Replace" button (line 544), the error will persist if the regex is still invalid. This is correct behavior. However, consider providing more specific error information by including the actual regex error message from the caught exception, which would help users debug their regex patterns.

Suggested change
setError(
__(
'Invalid regular expression.',
'search-replace-for-block-editor'
)
const details =
err instanceof Error && err.message
? ' ' + err.message
: '';
setError(
__(
'Invalid regular expression.',
'search-replace-for-block-editor'
) + details

Copilot uses AI. Check for mistakes.
);
return;
}

select( 'core/block-editor' )
.getBlocks()
Expand Down Expand Up @@ -395,15 +486,30 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
<div id="search-replace-modal__toggle">
<ToggleControl
label={ __(
'Match Case | Expression',
'Match Case',
'search-replace-for-block-editor'
) }
checked={ caseSensitive }
onChange={ handleCaseSensitive }
__nextHasNoMarginBottom
/>
<ToggleControl
label={ __(
'Use Regular Expression',
'search-replace-for-block-editor'
) }
checked={ useRegex }
onChange={ handleUseRegex }
__nextHasNoMarginBottom
/>
</div>

{ error ? (
<Notice status="error" isDismissible={ false }>
{ error }
</Notice>
) : null }

{ replacements ? (
<div id="search-replace-modal__notification">
<p>
Expand Down
16 changes: 16 additions & 0 deletions src/core/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@ export const getFallbackTextBlocks = (): string[] => {
];
};

/**
* Escape user input for safe literal RegExp usage.
*
* @since 1.10.0
*
* @param {string} value Raw user input.
* @return {string} Escaped input.
*/
export const escapeRegExp = ( value: string ): string => {
if ( typeof value !== 'string' ) {
return '';
}

return value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
};

/**
* Get Shortcut Event.
*
Expand Down
22 changes: 22 additions & 0 deletions tests/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,28 @@ describe( 'getFallbackTextBlocks', () => {
} );
} );

describe( 'escapeRegExp', () => {
beforeEach( () => {
jest.resetModules();
} );

it( 'escapeRegExp escapes regex metacharacters', () => {
const { escapeRegExp } = require( '../src/core/utils' );

const escaped = escapeRegExp( 'a.b+c*' );

expect( escaped ).toBe( 'a\\.b\\+c\\*' );
} );
Comment on lines +372 to +378
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test for escapeRegExp only covers a subset of regex metacharacters (., +, *). The implementation at line 350 escapes all regex metacharacters including ^, $, {, }, (, ), |, [, ], and . Consider adding test cases that verify these additional characters are properly escaped. For example, test inputs like "test^$" should become "test\^\$" and "(test)" should become "\(test\)".

Copilot uses AI. Check for mistakes.

it( 'escapeRegExp leaves plain text unchanged', () => {
const { escapeRegExp } = require( '../src/core/utils' );

const escaped = escapeRegExp( 'plain text' );

expect( escaped ).toBe( 'plain text' );
} );
} );
Comment on lines +367 to +387
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage for escapeRegExp is missing an important edge case test that aligns with the guard clause in the implementation (line 346-348 in utils.tsx). Based on the pattern used in other tests in this file (e.g., getNumberToBase10 at lines 333-340), there should be a test case to verify that passing a non-string parameter returns an empty string.

Copilot uses AI. Check for mistakes.
Comment on lines +367 to +387
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case for escapeRegExp with an empty string input to verify it returns an empty string, consistent with the behavior when a non-string is passed. This would complete the edge case coverage alongside the planned test for non-string inputs.

Copilot uses AI. Check for mistakes.

describe( 'getShortcutEvent', () => {
beforeEach( () => {
jest.resetModules();
Expand Down