-
Notifications
You must be signed in to change notification settings - Fork 7
fix: make regex opt‑in for search/replace #75
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 "" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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' | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
Comment on lines
+229
to
+233
|
||||||||||||||||||||||||||||||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
|
|
||
| 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
|
||
|
|
||
| describe( 'getShortcutEvent', () => { | ||
| beforeEach( () => { | ||
| jest.resetModules(); | ||
|
|
||
There was a problem hiding this comment.
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 thehandleUseRegexfunction.