diff --git a/languages/search-replace-for-block-editor.pot b/languages/search-replace-for-block-editor.pot index 5d8ea1a..9cd6b91 100644 --- a/languages/search-replace-for-block-editor.pot +++ b/languages/search-replace-for-block-editor.pot @@ -1,10 +1,11 @@ -# 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" @@ -12,7 +13,7 @@ msgstr "" "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" @@ -20,46 +21,57 @@ msgstr "" "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 "" diff --git a/src/core/app.tsx b/src/core/app.tsx index 2e18803..b853213 100644 --- a/src/core/app.tsx +++ b/src/core/app.tsx @@ -9,6 +9,7 @@ import { ToggleControl, Button, Tooltip, + Notice, } from '@wordpress/components'; import { Shortcut } from './shortcut'; @@ -18,6 +19,7 @@ import { isCaseSensitive, isSelectionInModal, isWpVersionGreaterThanOrEqualTo, + escapeRegExp, } from './utils'; import '../styles/app.scss'; @@ -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 ); @@ -63,6 +67,7 @@ const SearchReplaceForBlockEditor = (): JSX.Element => { setReplacements( 0 ); setSearchInput( '' ); setReplaceInput( '' ); + setError( '' ); } }; @@ -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 ); + persistRegexPreference( newValue ); + }; + /** * Listen for changes to input or case-sensitivity * and perform Searches only. @@ -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. @@ -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. @@ -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' + ) + ); + return; + } select( 'core/block-editor' ) .getBlocks() @@ -395,15 +486,30 @@ const SearchReplaceForBlockEditor = (): JSX.Element => {
diff --git a/src/core/utils.tsx b/src/core/utils.tsx index 7cc1018..813b2a2 100644 --- a/src/core/utils.tsx +++ b/src/core/utils.tsx @@ -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. * diff --git a/tests/utils.test.tsx b/tests/utils.test.tsx index d83512e..2d73366 100644 --- a/tests/utils.test.tsx +++ b/tests/utils.test.tsx @@ -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\\*' ); + } ); + + it( 'escapeRegExp leaves plain text unchanged', () => { + const { escapeRegExp } = require( '../src/core/utils' ); + + const escaped = escapeRegExp( 'plain text' ); + + expect( escaped ).toBe( 'plain text' ); + } ); +} ); + describe( 'getShortcutEvent', () => { beforeEach( () => { jest.resetModules();