Skip to content
100% Client-SidePHP (PCRE — preg_*)Dialect-Aware Translation

PHP Regex Tester — PCRE Patterns for preg_match & preg_replace

Paste the pattern exactly as it appears between preg_match() delimiters — /pattern/i, #pattern#u, any delimiter you use — and the translator peels the delimiters, applies the modifiers, and rewrites PCRE constructs to their JavaScript equivalents, with a notice for every transformation and a precise rejection for the ones that cannot travel.

  • Zero upload, ever

    Patterns and test data are evaluated in your browser — connection strings and column values never touch a server.

  • Live translation

    The dialect translator and the match engine recompute on every keystroke, with notices explaining each rewrite.

  • Honest where it matters

    Constructs that cannot be translated faithfully are rejected with a precise reason — never silently mis-matched.

Test string

Matches highlight as you type

Match results

Sandboxed Web Worker

Type a pattern to see live matches.

PHP regular expressions, in depth

Engine: PCRE2, as used by preg_match and preg_replace_callback. The tester translates your pattern for live evaluation — these sections document how PHP's engine really behaves and where it differs from the evaluation engine.

Delimiters and modifiers — the PHP wrapper, handled

PCRE patterns in PHP always wear a wrapper: a delimiter character (/, #, ~, %, {}, or any non-alphanumeric) around the pattern, optionally followed by modifier letters. preg_match('/#([0-9a-f]{6})/i', $css) carries the # delimiter and the i modifier. The tester accepts the wrapper whole — it detects a matching delimiter pair, strips them, maps i, m, s, and u to JavaScript flags, and rejects modifiers it cannot translate honestly (x verbose, U greedy-swap) rather than guessing. Bare, unwrapped patterns work too, for the times you are reasoning about engine-level syntax only.

The four translatable modifiers map cleanly: i is case-insensitivity, m makes ^ and $ anchor per line, s is PCRE's dotall — the dot crossing newlines — and u switches PCRE to UTF-8 mode, which JavaScript's u flag approximates. What has no JavaScript twin is x (free-spacing), U (ungreedy swap, which inverts every quantifier's greediness), and e (long-removed eval-on-replace), and the tester names the offending modifier when it refuses — the error message tells you exactly which letter to resolve by hand.

The PCRE extensions that translate — and the ones that cannot

PCRE's everyday extensions mostly have JavaScript equivalents once rewritten: named groups in any of their three spellings — (?P<name>...), (?<name>...), (?'name'...) — normalize to (?<name>...), and named backreferences (?P=name) become \k<name>. The horizontal and vertical whitespace shorthands \h, \H, \v, and \V expand to explicit character classes covering exactly the code points PCRE defines; \R — any line break, with CRLF kept atomic — becomes an alternation that preserves the CRLF-pairing behavior a naive [\r\n] would break. \Q...\E literal quoting, the PHP idiom for embedding verbatim text, expands to an explicitly escaped literal.

The hard boundary is the PCRE feature set with no JavaScript counterpart: \K resets where the reported match starts, recursion constructs (?R) and (?1) match nested structures recursively, subroutines (?&name) reuse group definitions, callouts (?C) invoke code mid-pattern, and atomic groups (?>...) and possessive quantifiers a++ suppress backtracking. Each is rejected with a message naming the construct — because every one of them would change what matches, not just how it is spelled, and a translator that approximated them would hand you matches your PHP code will not produce.

preg_match, preg_match_all, and what the flags bar means

The tester's global mode is preg_match_all() — every match, in order, with named groups broken out per match just as PREG_SET_ORDER arranges them. Turning global off is preg_match(): first match only. The named-group panel mirrors the $matches array's string keys; the numeric capture groups mirror its integer keys. If your production call uses PREG_OFFSET_CAPTURE, the match table's [start, end) offsets are those same byte-ish positions — UTF-8 code units in JavaScript versus bytes in PHP, a difference that matters only for multi-byte text, where offsets should be treated as logical rather than byte positions.

preg_replace_callback() and preg_replace() map to the replace flows you would expect, including $1 and \1 and named ${name} substitution — but the pattern side is where this tool earns its keep: a replaced capture group boundary bug is visible instantly in the highlight view, where alternating match colors and per-group underlines show exactly what each group consumed. For the classic 'match HTML tags' family of patterns, the ReDoS guard also catches the ones that will hang: nested quantifier shapes are terminated at two seconds with the shape called out by name.

UTF-8 mode, bytes, and code points

Without the u modifier, PCRE operates on bytes — the dot matches one byte, not one character, and multi-byte UTF-8 text produces offset and match behavior that surprises everyone once. With u, PCRE validates the subject as UTF-8 and switches to code-point semantics. JavaScript's engine is natively code-unit based and becomes code-point based with its own u flag, which is why the tester maps PHP's u modifier to JavaScript's u and attaches a notice: close parity for matching behavior, with the caveat that JavaScript never rejects invalid UTF-8 the way PCRE's u mode does — browsers normalize malformed sequences before the engine ever sees them.

The practical guidance is boring and valuable: if your production pattern carries the u modifier, test here with u on; if it does not, test against single-byte text or accept that byte-versus-code-unit offsets diverge on multi-byte input. And when a pattern needs Unicode properties — \p{L}, \p{N}, \p{Han} — PCRE requires u and JavaScript does too; those constructs pass through with the flag auto-added and a note confirming the property-name vocabulary you rely on is shared between the two engines.

PHP versus the evaluation engine

What translates automatically, what carries a caveat, and what is rejected — construct by construct.

Syntax differences between PHP regex and JavaScript RegExp
ConstructPHPThis tester
Pattern wrapperDelimiters + trailing modifiers (/…/imsxu)Bare pattern + separate flags — wrapper parsed automatically
Named groups(?P<name>...), (?<name>...), (?'name'...)(?<name>...) — all spellings normalized
\h \v \RHorizontal / vertical / any-linebreak classesExpanded to explicit classes automatically
\KMatch start reset — report from here onwardNo equivalent — rejected (use a capture group instead)
Recursion (?R) (?1)Matches nested balanced structuresNo equivalent — rejected with guidance
Possessive / atomica*+ and (?>...) suppress backtrackingNo equivalent — rejected with guidance
UTF-8 modeu modifier validates UTF-8 and switches to code pointsu flag approximates; invalid sequences are normalized upstream
OffsetsPREG_OFFSET_CAPTURE returns byte offsetsOffsets are UTF-16 code units — treat as logical positions

PHP regex cheat sheet

The PHP-specific tokens, with the constructs that cannot be translated marked explicitly.

PHP regex token cheat sheet
TokenMeaningExample
/pattern/iDelimiter + case-insensitive modifier (auto-parsed)/api/i → matches API
\h \HHorizontal whitespace / its negation\h+ → spaces and tabs
\v \VVertical whitespace / its negation\v → \n, \r, VT, FF
\RAny line-break sequence, CRLF atomica\Rb → a\r\nb
\Q…\EQuote literal text until \E\Q1+1=2\E → literal
(?P<name>…)Named group (PCRE spelling)(?P<id>\d+) → groups.id
(?&name)Subroutine call — not translatableblocked with a reason
(?R)Full-pattern recursion — not translatableblocked with a reason
\KReset match start — not translatableblocked with a reason
a*+ / (?>a)Possessive / atomic — not translatableblocked with a reason

Frequently asked questions

Do I need to strip the /.../ delimiters before pasting?

No — paste the whole thing, delimiters and modifiers included. The tester detects a matched delimiter pair, removes them, and maps the trailing modifiers to JavaScript flags: i, m, s, and u translate directly; x, U, and other PCRE-only modifiers are refused with a message naming the exact letter. If you paste a bare pattern instead, it is evaluated as-is with the flags you choose in the flag bar.

Why is my \K pattern rejected?

\K tells PCRE to report the match starting from that point — foo\Kbar matches bar but returns it as if the match began there. JavaScript has no way to shift a match's reported start, so any translation would return different offsets and different replace() output than your PHP code. The rejection message suggests the standard restructure: use a capture group around the part you care about (foo(bar)) and read group 1 — same result, portable pattern.

How do I test recursion patterns for nested parentheses?

The classic \((?:[^()]|(?R))*\) pattern is rejected, because JavaScript's engine cannot call itself recursively. The workaround family works in both languages: iterate a counter-based grammar in code, use a parsing library for nested structures, or — for bounded nesting — unroll the recursion into a fixed number of levels like (?:\((?:[^()]|\([^()]*\))*\)). The tester is honest about this boundary rather than pretending the recursion ran.

What do the notices under the pattern input mean?

Every notice documents one concrete translation the tool performed — POSIX or PCRE class expansions, named-group rewrites, anchor mappings, delimiters stripped, flags added — or a semantic caveat you should know about (case sensitivity, Unicode width, trailing-newline behavior). The rewritten JavaScript pattern is shown alongside so you can see exactly what the engine evaluated. Nothing is rewritten silently; if the tool cannot preserve meaning, it errors instead.

Does preg_replace's ${1} substitution work the same in JavaScript?

Yes — PHP's ${name} and $1 substitution in preg_replace map to JavaScript's $<name> and $1 in String.replace(). Capture-group numbering and naming behave identically after the named-group syntax is normalized, so a pattern validated here with named groups drops into preg_replace_callback with the same $matches['name'] keys the group panel shows. Offsets differ only on multi-byte subjects, where PHP reports bytes and JavaScript code units.

Why does my pattern fail with 'modifier cannot be translated'?

Because a trailing modifier has no honest JavaScript equivalent. The x modifier makes whitespace and # comments insignificant — a semantic change a translator must not guess at. U inverts every quantifier's greediness, which changes results, not spelling. Those patterns are refused with the offending modifier named; resolve it by hand (remove whitespace/comments for x, flip each quantifier's laziness for U) and the rest of the pattern will translate fine.