Search by

php-regex / regex-transpiler

yoeunes

Transpiles PCRE patterns to JavaScript and Python regular expressions, with the losses reported.

Package info

github.com/php-regex/regex-transpiler

Homepage

Issues

Documentation

pkg:composer/php-regex/regex-transpiler

Fund package maintenance!

yoeunes

Statistics

Installs: 11

Dependents: 5

Suggesters: 0

Stars: 0

2.x-dev 2026-11-10 12:48 UTC

This package is auto-updated.

Last update: 2026-11-10 17:42:00 UTC


README

PHPRegex Transpiler

Documentation Badge Author Badge GitHub Release Badge License Badge Packagist Downloads Badge GitHub Stars Badge Supported PHP Version Badge

PHPRegex Transpiler

Transpiles PCRE patterns to JavaScript regular expressions, HTML pattern attributes and Python regular expressions, with the losses reported.

Documentation: php-regex.com — the API reference documents transpile() in full.

Features

  • Three targets, javascript (alias js), html-pattern (alias html) and python (alias py), each producing a paste-ready literal and a constructor call
  • Dialect rewrites: (?P<name>) becomes (?<name>) in JavaScript, \k<name> becomes (?P=name) in Python, \h and \v become character classes
  • What a target reads otherwise is written out as PCRE reads it: $, which also matches before a final newline, ^ and $ under m, the dot, which leaves out \n alone, \s without u, six bytes in PHP where JavaScript's also takes the Unicode spaces, and \S, its complement, which takes a byte of U+00A0 in PHP where JavaScript's leaves that space out, and, under u, \d, \w, \s and \b, which PHP reads in Unicode
  • Flags are mapped per target: JavaScript keeps i, s, u, gains u when Unicode escapes need it, and has /m, /D, /x and /U applied while printing; Python keeps i and s, gains re.ASCII without u, has /m, /D, /x and /U applied while printing, and spells flags inline or as re constants; every target drops /S, which PHP has ignored since 7.3, with a note
  • Warnings list every rewrite to review, and the first set that takes a byte in PCRE and a character in the target when the pattern has no u; notes list the flags applied or dropped
  • A construct the target cannot express without matching otherwise — \w under u in Python, a possessive quantifier or an atomic group in JavaScript, and in Python unless pythonVersion is 3.11 or later, a backreference to a group that may not have matched in JavaScript — throws a TranspileException carrying the position in the pattern
  • Every target is checked against its own engine: the test suite runs the transpiled patterns in Node and Python on the subjects preg_match() is run on, and compares the matches and the groups

Installation

composer require php-regex/regex-transpiler

PHP 8.1 or newer. php-regex/regex-parser is pulled in automatically.

Configuration

transpile() takes an optional TranspileOptions object:

  • allowLookbehind (bool, default true) — false makes lookbehind throw for JavaScript targets.
  • pythonVersion (string or null, default null) — the oldest Python the pattern runs on, as '3.11' or '3.11.4'. Python's re takes atomic groups and possessive quantifiers from 3.11: from that version they are written, before it, or with null, they are refused.
use PHPRegex\Transpiler\Transpiler;
use PHPRegex\Transpiler\TranspileOptions;

$result = (new Transpiler())->transpile('/\d++(?>px|em)/', 'python', new TranspileOptions(pythonVersion: '3.12'));

echo $result->literal; // r'(?a)\d++(?>px|em)'

A possessive quantifier is the atomic group around the greedy repetition to PCRE, and is written so where Python's own goes wrong: before 3.11.5, where a possessive repetition inside another runs past its end (a++ is (?>a+) for '3.11'), and over a group, which Python 3.11 to 3.12.7 and 3.13.0 lose the capture of ((a|b)++ is (?>(a|b)+)).

Usage

A PCRE pattern with a named group and a backreference, for a JavaScript codebase:

use PHPRegex\Transpiler\Transpiler;

$transpiler = new Transpiler();
$result = $transpiler->transpile('/(?P<word>\w+)\k{word}/i', 'javascript');

echo $result->literal;  // /(?<word>\w+)\k<word>/i

// "$" also matches before a newline that ends the subject; JavaScript's does not.
echo $transpiler->transpile('/^\d+$/', 'javascript')->literal; // /^\d+(?=\n?$)/

Without a parser the transpiler reads with RegexParser::create(); pass the one your code reads with, as new Transpiler($parser), to read the pattern for its target and limits.

For Python, the literal is a raw string carrying the flags inline, and the constructor is a re.compile() call. Without u PCRE reads \w, \d, \s, \b and case in ASCII, so the pattern gains re.ASCII:

$result = $transpiler->transpile('/(?P<word>\w+)\k<word>/i', 'python');

echo $result->literal;     // r'(?ai)(?P<word>\w+)(?P=word)'
echo $result->constructor; // re.compile(r'(?P<word>\w+)(?P=word)', re.ASCII | re.IGNORECASE)

Every rewrite is reported, and what a target cannot express is refused rather than approximated:

$result = $transpiler->transpile('/\p{L}+/', 'javascript');

echo $result->literal;     // /\p{L}+/u
echo $result->warnings[0]; // Added /u for Unicode property escapes.

// PCRE2 reads a bare script name as the script's extensions (from 10.40; the script alone before); JavaScript names them.
echo $transpiler->transpile('/\p{Han}+/u', 'javascript')->literal; // /\p{Script_Extensions=Han}+/u

try {
    $transpiler->transpile('/a++/', 'javascript');
} catch (\PHPRegex\Transpiler\Exception\TranspileException $e) {
    echo $e->getMessage(); // Possessive quantifiers are not supported in JavaScript.
}

For an HTML form, the pattern attribute matches the whole value under the v flag; the html-pattern target pads an unanchored side so the attribute accepts what preg_match() finds, and escapes in classes what v reserves:

$result = $transpiler->transpile('/^[\w.-]+@[\w-]+\.[a-z]{2,}$/', 'html-pattern');

echo $result->literal;     // ^[\w\.\-]+@[\w\-]+\.[a-z]{2,}$
echo $transpiler->transpile('/\d{4}/', 'html')->literal; // [\s\S]*(?:\d{4})[\s\S]*

No flag reaches the attribute. /i, (?i) and (?i:…) are spelled out: each letter, class or property is written with every character the running PCRE takes for it caselessly, so the attribute works in every browser that has the v flag, and under u the Kelvin sign and the long s come along:

echo $transpiler->transpile('/^[a-z]+-\d+$/i', 'html')->literal;  // ^[a-zA-Z]+-\d+$
echo $transpiler->transpile('/^ok$/iu', 'html')->literal;         // ^[oO][kK\u212A]$

A backreference under /i is refused: it would match its group's text in one case only. /s, /m and /D change nothing in a field value, which holds no line break. /S, which PHP has ignored since 7.3, is dropped with a note, there as for JavaScript, and /U or (?U) swaps greedy and lazy in the quantifiers it governs: /<.+>/U is /<[^\n]+?>/ for JavaScript, the dot written as PCRE reads it.

JavaScript and Python read characters, where PCRE without u reads bytes. A multibyte character written whole outside a class (/café/) stays that character. The rest has no equivalent there and is refused with the advice that works: a byte above 0x7F on its own, as an escape (\xE9, [\x80-\xFF]) or as the last byte a quantifier repeats (/é+/), asks for u; a raw byte of invalid UTF-8 asks to be written as the character, as UTF-8, before u, which refuses such a pattern; and a multibyte character in a class (/[é]/), each of whose bytes PCRE reads as a member of its own, asks for u. Before advising u, the transpiler asks the engine whether the pattern compiles with it: where something else is refused under u (/[é](?<=\C)/, a \C in a lookbehind), the advice says to rewrite that construct first. Adding u changes what the pattern matches, characters instead of bytes (/[é]/ matches à through its first byte, /[é]/u does not): confirm that matching characters is what the pattern means before taking the advice.

A set is not refused, where PCRE takes one byte for it and the targets one character: most patterns hold a dot or a negated class, and on ASCII subjects the two agree. Once the set reaches past ASCII (the dot, a negated class, \D, \W, \S, \h, \v, a property) they differ on a subject holding a character outside it, and the first such set is named in a warning, once per pattern, with the advice a refusal gives:

$result = $transpiler->transpile('/^\S+$/D', 'javascript');

echo $result->literal;     // /^[^\t-\r ]+$/
echo $result->warnings[0]; // Without the u flag "\S" at offset 1 takes one byte in PCRE and one character in this target, so the two differ on a subject holding a character outside ASCII; under u, for UTF-8 subjects, they agree. Add u.

var_dump($transpiler->transpile('/^\S+$/Du', 'javascript')->hasWarnings()); // bool(false)

Documentation

Resources

Sponsors

Sponsor

If PHPRegex saves you time, consider sponsoring its maintenance.

License

MIT. See LICENSE.