ecma-regex 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henry Andrews
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.5
2
+ Name: ecma-regex
3
+ Version: 0.0.1
4
+ Summary: ECMA-262 regular expressions for Python: parse, translate to re, and search with JavaScript semantics
5
+ Project-URL: Homepage, https://github.com/handrews/py-json-schema-engine/tree/main/packages/ecma-regex
6
+ Project-URL: Repository, https://github.com/handrews/py-json-schema-engine
7
+ Project-URL: Issues, https://github.com/handrews/py-json-schema-engine/issues
8
+ Author: Henry Andrews
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ecma-262,ecmascript,javascript,regex,regexp,unicode
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Text Processing
21
+ Requires-Python: >=3.12
22
+ Provides-Extra: regex
23
+ Requires-Dist: regex; extra == 'regex'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # ecma-regex
27
+
28
+ ECMA-262 regular expressions for Python: parse them, translate them to `re`
29
+ (or `regex`), and match with JavaScript semantics.
30
+
31
+ Several widely used data formats specify their regular expressions as
32
+ ECMA-262 patterns — JSON Schema above all. A Python program that hands
33
+ those patterns straight to `re` does not get an error; it gets *different
34
+ answers*, quietly, for `^`, `$`, `.`, `\d`, `\w`, `\s` and `\b`, and a hard
35
+ failure for `\p{...}`. This package closes that gap.
36
+
37
+ It has **no dependencies** and imports nothing from any consumer. It
38
+ happens to power [json-schema-engine][engine], but it does not depend on
39
+ it, and it is useful anywhere an ECMA-262 pattern has to run under Python.
40
+
41
+ [engine]: https://github.com/handrews/py-json-schema-engine
42
+
43
+ Produced by Henry Andrews via Claude Code.
44
+
45
+ **Status: 0.0.x.** The implementation is functional and tested against every
46
+ regular expression in the official JSON Schema test suite, but the
47
+ documentation is AI-written and not yet audited against actual usage or
48
+ human readability standards. `0.1.x` ships once the documentation is deemed
49
+ suitable for general consumption.
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ pip install ecma-regex
55
+ # optional: the `regex` backend, which adds variable-width lookbehind
56
+ # and native \p{Script=...} support
57
+ pip install 'ecma-regex[regex]'
58
+ ```
59
+
60
+ Python 3.12+.
61
+
62
+ ## Usage
63
+
64
+ ```python
65
+ import ecma_regex
66
+
67
+ pattern = ecma_regex.compile(r"^\p{Letter}+$")
68
+ pattern.search("olé") # True
69
+ pattern.search("olé1") # False
70
+ pattern.translated # the emitted `re` pattern
71
+ pattern.compiled # the underlying re.Pattern
72
+
73
+ ecma_regex.compile("^a+$").search("aaa\n") # False (re says True)
74
+ ecma_regex.compile("f.o").search("f\ro") # False (re says True)
75
+ ecma_regex.compile(r"\d").search("٣") # False (re says True)
76
+ ecma_regex.compile("^b$", flags="m").search("a\u2028b") # True
77
+ ```
78
+
79
+ `search` has `RegExp.prototype.test` semantics: unanchored, boolean.
80
+
81
+ Lower-level entry points, if you want the pieces:
82
+
83
+ ```python
84
+ tree = ecma_regex.parse(r"(?<year>\d{4})-\d{2}") # an AST
85
+ source = ecma_regex.translate(tree) # a backend pattern string
86
+ flags = ecma_regex.translate_flags(tree) # re.IGNORECASE, or 0
87
+ depth = ecma_regex.star_height(tree) # ReDoS screening
88
+ ```
89
+
90
+ `star_height` reports the nesting depth of unbounded quantifiers — `a+` is
91
+ 1, `(a+)+` is 2 — which is the classic necessary condition for
92
+ catastrophic backtracking. It over-reports; it is a screen, not a proof.
93
+
94
+ Nothing is cached at the `compile` level. Put your own cache in front of it
95
+ if you compile the same pattern repeatedly.
96
+
97
+ ## Scope
98
+
99
+ The dialect is ECMA-262 §22.2 with the **`u` flag**: the Unicode-mode
100
+ pattern grammar. Annex B's web-compatibility leniencies (legacy octal
101
+ escapes, quantifiable assertions, a bare `{` as a literal, identity escapes
102
+ of arbitrary characters) are early errors under `u`, and they are errors
103
+ here too. `a{,3}`, `a**` and `(?=a)*` all raise.
104
+
105
+ | Flag | Status |
106
+ | --- | --- |
107
+ | `i` | supported (`re.IGNORECASE` at compile time) |
108
+ | `m` | supported (spelled out with lookarounds, not `re.MULTILINE`) |
109
+ | `s` | supported (spelled out, not `re.DOTALL`) |
110
+ | `u` | accepted and always implied |
111
+ | `g`, `y`, `d`, `v` | rejected — this models one stateless match |
112
+
113
+ Everything the grammar allows is parsed: lookbehind, backreferences, named
114
+ groups and `\k<name>`, `\uXXXX` (surrogate pairs combined), `\u{...}`,
115
+ `\xHH`, `\cX`, `\0`, `\t\n\v\f\r`, and `\p{...}` / `\P{...}`.
116
+
117
+ Invalid patterns raise `EcmaRegexSyntaxError`; valid-but-untranslatable
118
+ ones raise `UnsupportedPatternError`. Both subclass `EcmaRegexError` and
119
+ carry a `position`.
120
+
121
+ ## Divergence table
122
+
123
+ What the translation fixes — each row is a silent change of verdict if the
124
+ pattern goes to `re` unchanged.
125
+
126
+ | Construct | Python `re` | ECMA-262 (what is emitted) |
127
+ | --- | --- | --- |
128
+ | `^` / `$` | `$` also matches before a final `\n` | `\A` / `\Z` |
129
+ | `^` / `$` with `m` | line breaks are `\n` only | lookarounds over LF, CR, U+2028, U+2029 |
130
+ | `.` | excludes `\n` only | excludes LF, CR, U+2028, U+2029 |
131
+ | `.` with `s` | `re.DOTALL` | `[\s\S]` |
132
+ | `\d` | any Unicode decimal digit | `[0-9]` |
133
+ | `\w` | any Unicode word character | `[A-Za-z0-9_]` |
134
+ | `\s` | a different set; misses U+FEFF, includes U+001C–U+001F | ECMA WhiteSpace ∪ LineTerminator |
135
+ | `\b` / `\B` | Unicode word boundary | ASCII word boundary, as explicit lookarounds |
136
+ | `\D` `\W` `\S` | as above, negated | explicit complement ranges |
137
+ | `\D` `\W` `\S` **inside a class** | not expressible | explicit complement ranges |
138
+ | `\p{...}` / `\P{...}` | `re.error` | explicit ranges (`re`) or native (`regex`) |
139
+ | `(?<name>…)` | different spelling | `(?P<name>…)`, with `$` in names rewritten |
140
+ | `\k<name>` | different spelling | `(?P=name)` |
141
+ | literal `{`, `#`, `-`, … | may be metacharacters | escaped |
142
+
143
+ ## Residual divergences
144
+
145
+ These cannot be closed inside a backend pattern, so they are documented
146
+ rather than fixed.
147
+
148
+ | Area | Difference |
149
+ | --- | --- |
150
+ | Case folding | `i` compiles with `re.IGNORECASE`, which is Python's *full* case folding; ECMA-262 `u` mode uses *simple* case folding. They agree except on a handful of code points. |
151
+ | `\b` with `i` and `u` | ECMA-262 adds U+017F and U+212A to the word set in that combination; this package does not. |
152
+ | Variable-width lookbehind | `re` requires a fixed width, so `(?<=ab?)c` raises `UnsupportedPatternError` on the `re` backend. It works on `regex`. |
153
+ | `\p{Script=…}`, `\p{Script_Extensions=…}` | Parsed, but the standard library ships no script data: translatable only on the `regex` backend, and script *names* are not validated. |
154
+ | Some binary properties | `Alphabetic`, `Math`, `Emoji`, `Cased`, … are recognized as valid syntax but not derivable from the standard library; they raise `UnsupportedPatternError` on `re` and are emitted natively on `regex`. |
155
+ | Unicode version | Property sets come from the running interpreter's `unicodedata`, not from whatever version a given JavaScript engine ships. |
156
+
157
+ Properties that *are* derivable everywhere: every `General_Category` value
158
+ (including `\p{L}`, `\p{Letter}`, `\p{General_Category=Nd}`) plus `ASCII`,
159
+ `ASCII_Hex_Digit`, `Any`, `Assigned`, `Bidi_Control`, `Bidi_Mirrored`,
160
+ `Hex_Digit`, `Join_Control`, `Lowercase`, `Noncharacter_Code_Point`,
161
+ `Regional_Indicator`, `Uppercase`, `White_Space`, `XID_Continue`,
162
+ `XID_Start`.
163
+
164
+ ## Backends
165
+
166
+ | Backend | Notes |
167
+ | --- | --- |
168
+ | `re` (default) | Standard library, no dependencies. `\p{...}` becomes an explicit range class. |
169
+ | `regex` | Optional extra. Keeps `\p{...}` native, allows variable-width lookbehind, supports scripts. |
170
+
171
+ ```python
172
+ ecma_regex.compile(r"\p{Script=Greek}+", backend="regex")
173
+ ```
174
+
175
+ Neither backend is linear-time; both backtrack. Use `star_height` to screen
176
+ untrusted patterns.
177
+
178
+ ### Cost
179
+
180
+ The first `\p{...}` translated on the `re` backend scans every code point
181
+ to build its range list — about 40 ms on CPython 3.12. The result is cached
182
+ process-wide, so later uses are dict hits, and every later
183
+ `General_Category` property reuses the same scan.
184
+
185
+ ## License
186
+
187
+ MIT.
@@ -0,0 +1,162 @@
1
+ # ecma-regex
2
+
3
+ ECMA-262 regular expressions for Python: parse them, translate them to `re`
4
+ (or `regex`), and match with JavaScript semantics.
5
+
6
+ Several widely used data formats specify their regular expressions as
7
+ ECMA-262 patterns — JSON Schema above all. A Python program that hands
8
+ those patterns straight to `re` does not get an error; it gets *different
9
+ answers*, quietly, for `^`, `$`, `.`, `\d`, `\w`, `\s` and `\b`, and a hard
10
+ failure for `\p{...}`. This package closes that gap.
11
+
12
+ It has **no dependencies** and imports nothing from any consumer. It
13
+ happens to power [json-schema-engine][engine], but it does not depend on
14
+ it, and it is useful anywhere an ECMA-262 pattern has to run under Python.
15
+
16
+ [engine]: https://github.com/handrews/py-json-schema-engine
17
+
18
+ Produced by Henry Andrews via Claude Code.
19
+
20
+ **Status: 0.0.x.** The implementation is functional and tested against every
21
+ regular expression in the official JSON Schema test suite, but the
22
+ documentation is AI-written and not yet audited against actual usage or
23
+ human readability standards. `0.1.x` ships once the documentation is deemed
24
+ suitable for general consumption.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ pip install ecma-regex
30
+ # optional: the `regex` backend, which adds variable-width lookbehind
31
+ # and native \p{Script=...} support
32
+ pip install 'ecma-regex[regex]'
33
+ ```
34
+
35
+ Python 3.12+.
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ import ecma_regex
41
+
42
+ pattern = ecma_regex.compile(r"^\p{Letter}+$")
43
+ pattern.search("olé") # True
44
+ pattern.search("olé1") # False
45
+ pattern.translated # the emitted `re` pattern
46
+ pattern.compiled # the underlying re.Pattern
47
+
48
+ ecma_regex.compile("^a+$").search("aaa\n") # False (re says True)
49
+ ecma_regex.compile("f.o").search("f\ro") # False (re says True)
50
+ ecma_regex.compile(r"\d").search("٣") # False (re says True)
51
+ ecma_regex.compile("^b$", flags="m").search("a\u2028b") # True
52
+ ```
53
+
54
+ `search` has `RegExp.prototype.test` semantics: unanchored, boolean.
55
+
56
+ Lower-level entry points, if you want the pieces:
57
+
58
+ ```python
59
+ tree = ecma_regex.parse(r"(?<year>\d{4})-\d{2}") # an AST
60
+ source = ecma_regex.translate(tree) # a backend pattern string
61
+ flags = ecma_regex.translate_flags(tree) # re.IGNORECASE, or 0
62
+ depth = ecma_regex.star_height(tree) # ReDoS screening
63
+ ```
64
+
65
+ `star_height` reports the nesting depth of unbounded quantifiers — `a+` is
66
+ 1, `(a+)+` is 2 — which is the classic necessary condition for
67
+ catastrophic backtracking. It over-reports; it is a screen, not a proof.
68
+
69
+ Nothing is cached at the `compile` level. Put your own cache in front of it
70
+ if you compile the same pattern repeatedly.
71
+
72
+ ## Scope
73
+
74
+ The dialect is ECMA-262 §22.2 with the **`u` flag**: the Unicode-mode
75
+ pattern grammar. Annex B's web-compatibility leniencies (legacy octal
76
+ escapes, quantifiable assertions, a bare `{` as a literal, identity escapes
77
+ of arbitrary characters) are early errors under `u`, and they are errors
78
+ here too. `a{,3}`, `a**` and `(?=a)*` all raise.
79
+
80
+ | Flag | Status |
81
+ | --- | --- |
82
+ | `i` | supported (`re.IGNORECASE` at compile time) |
83
+ | `m` | supported (spelled out with lookarounds, not `re.MULTILINE`) |
84
+ | `s` | supported (spelled out, not `re.DOTALL`) |
85
+ | `u` | accepted and always implied |
86
+ | `g`, `y`, `d`, `v` | rejected — this models one stateless match |
87
+
88
+ Everything the grammar allows is parsed: lookbehind, backreferences, named
89
+ groups and `\k<name>`, `\uXXXX` (surrogate pairs combined), `\u{...}`,
90
+ `\xHH`, `\cX`, `\0`, `\t\n\v\f\r`, and `\p{...}` / `\P{...}`.
91
+
92
+ Invalid patterns raise `EcmaRegexSyntaxError`; valid-but-untranslatable
93
+ ones raise `UnsupportedPatternError`. Both subclass `EcmaRegexError` and
94
+ carry a `position`.
95
+
96
+ ## Divergence table
97
+
98
+ What the translation fixes — each row is a silent change of verdict if the
99
+ pattern goes to `re` unchanged.
100
+
101
+ | Construct | Python `re` | ECMA-262 (what is emitted) |
102
+ | --- | --- | --- |
103
+ | `^` / `$` | `$` also matches before a final `\n` | `\A` / `\Z` |
104
+ | `^` / `$` with `m` | line breaks are `\n` only | lookarounds over LF, CR, U+2028, U+2029 |
105
+ | `.` | excludes `\n` only | excludes LF, CR, U+2028, U+2029 |
106
+ | `.` with `s` | `re.DOTALL` | `[\s\S]` |
107
+ | `\d` | any Unicode decimal digit | `[0-9]` |
108
+ | `\w` | any Unicode word character | `[A-Za-z0-9_]` |
109
+ | `\s` | a different set; misses U+FEFF, includes U+001C–U+001F | ECMA WhiteSpace ∪ LineTerminator |
110
+ | `\b` / `\B` | Unicode word boundary | ASCII word boundary, as explicit lookarounds |
111
+ | `\D` `\W` `\S` | as above, negated | explicit complement ranges |
112
+ | `\D` `\W` `\S` **inside a class** | not expressible | explicit complement ranges |
113
+ | `\p{...}` / `\P{...}` | `re.error` | explicit ranges (`re`) or native (`regex`) |
114
+ | `(?<name>…)` | different spelling | `(?P<name>…)`, with `$` in names rewritten |
115
+ | `\k<name>` | different spelling | `(?P=name)` |
116
+ | literal `{`, `#`, `-`, … | may be metacharacters | escaped |
117
+
118
+ ## Residual divergences
119
+
120
+ These cannot be closed inside a backend pattern, so they are documented
121
+ rather than fixed.
122
+
123
+ | Area | Difference |
124
+ | --- | --- |
125
+ | Case folding | `i` compiles with `re.IGNORECASE`, which is Python's *full* case folding; ECMA-262 `u` mode uses *simple* case folding. They agree except on a handful of code points. |
126
+ | `\b` with `i` and `u` | ECMA-262 adds U+017F and U+212A to the word set in that combination; this package does not. |
127
+ | Variable-width lookbehind | `re` requires a fixed width, so `(?<=ab?)c` raises `UnsupportedPatternError` on the `re` backend. It works on `regex`. |
128
+ | `\p{Script=…}`, `\p{Script_Extensions=…}` | Parsed, but the standard library ships no script data: translatable only on the `regex` backend, and script *names* are not validated. |
129
+ | Some binary properties | `Alphabetic`, `Math`, `Emoji`, `Cased`, … are recognized as valid syntax but not derivable from the standard library; they raise `UnsupportedPatternError` on `re` and are emitted natively on `regex`. |
130
+ | Unicode version | Property sets come from the running interpreter's `unicodedata`, not from whatever version a given JavaScript engine ships. |
131
+
132
+ Properties that *are* derivable everywhere: every `General_Category` value
133
+ (including `\p{L}`, `\p{Letter}`, `\p{General_Category=Nd}`) plus `ASCII`,
134
+ `ASCII_Hex_Digit`, `Any`, `Assigned`, `Bidi_Control`, `Bidi_Mirrored`,
135
+ `Hex_Digit`, `Join_Control`, `Lowercase`, `Noncharacter_Code_Point`,
136
+ `Regional_Indicator`, `Uppercase`, `White_Space`, `XID_Continue`,
137
+ `XID_Start`.
138
+
139
+ ## Backends
140
+
141
+ | Backend | Notes |
142
+ | --- | --- |
143
+ | `re` (default) | Standard library, no dependencies. `\p{...}` becomes an explicit range class. |
144
+ | `regex` | Optional extra. Keeps `\p{...}` native, allows variable-width lookbehind, supports scripts. |
145
+
146
+ ```python
147
+ ecma_regex.compile(r"\p{Script=Greek}+", backend="regex")
148
+ ```
149
+
150
+ Neither backend is linear-time; both backtrack. Use `star_height` to screen
151
+ untrusted patterns.
152
+
153
+ ### Cost
154
+
155
+ The first `\p{...}` translated on the `re` backend scans every code point
156
+ to build its range list — about 40 ms on CPython 3.12. The result is cached
157
+ process-wide, so later uses are dict hits, and every later
158
+ `General_Category` property reuses the same scan.
159
+
160
+ ## License
161
+
162
+ MIT.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ecma-regex"
7
+ version = "0.0.1"
8
+ description = "ECMA-262 regular expressions for Python: parse, translate to re, and search with JavaScript semantics"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Henry Andrews" },
15
+ ]
16
+ dependencies = []
17
+ keywords = ["regex", "regexp", "ecmascript", "ecma-262", "javascript", "unicode"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Software Development :: Libraries",
27
+ "Topic :: Text Processing",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ regex = ["regex"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/handrews/py-json-schema-engine/tree/main/packages/ecma-regex"
35
+ Repository = "https://github.com/handrews/py-json-schema-engine"
36
+ Issues = "https://github.com/handrews/py-json-schema-engine/issues"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/ecma_regex"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = [
43
+ "/src/",
44
+ "/tests/",
45
+ "/README.md",
46
+ "/LICENSE",
47
+ "/pyproject.toml",
48
+ ]
@@ -0,0 +1,156 @@
1
+ """ECMA-262 regular expressions for Python.
2
+
3
+ Parse an ECMA-262 pattern, translate it into a pattern string for Python's
4
+ :mod:`re` (or the third-party ``regex`` module), and match with JavaScript
5
+ semantics. This exists because several widely used data formats -- JSON
6
+ Schema above all -- specify their regular expressions as ECMA-262
7
+ patterns, and a Python program that hands those patterns straight to
8
+ :mod:`re` silently gets different answers.
9
+
10
+ >>> import ecma_regex
11
+ >>> ecma_regex.compile("^a+$").search("aaa\\n")
12
+ False
13
+ >>> ecma_regex.compile(r"^\\p{Letter}+$").search("ol\\u00e9")
14
+ True
15
+
16
+ Scope
17
+ -----
18
+ The dialect implemented is ECMA-262 22.2 with the ``u`` flag: the
19
+ Unicode-mode pattern grammar, which is strict where the web-compatibility
20
+ grammar of Annex B is lenient. ``i``, ``m`` and ``s`` are supported and
21
+ recorded on the AST; ``u`` is accepted and implied. The stateful flags
22
+ ``g`` and ``y``, the index flag ``d`` and the ``v`` set-notation flag are
23
+ rejected: this package models a single stateless, unanchored match.
24
+
25
+ Everything the grammar allows is parsed, including lookbehind, named
26
+ groups and named backreferences, ``\\uXXXX`` (with surrogate-pair
27
+ combining), ``\\u{...}``, ``\\xHH``, ``\\cX``, ``\\0``, and ``\\p{...}`` /
28
+ ``\\P{...}`` property escapes.
29
+
30
+ What the translation fixes
31
+ --------------------------
32
+ Each of these silently changes a verdict if a pattern is handed to
33
+ :mod:`re` unchanged. The emitted pattern spells out the ECMA-262 meaning.
34
+
35
+ ================== ========================= ============================
36
+ Construct Python ``re`` ECMA-262 (what is emitted)
37
+ ================== ========================= ============================
38
+ ``^`` / ``$`` ``$`` also matches before ``\\A`` / ``\\Z``
39
+ a final ``\\n``
40
+ ``^`` / ``$`` (m) line breaks are ``\\n`` lookarounds over LF, CR,
41
+ only LS (U+2028), PS (U+2029)
42
+ ``.`` excludes ``\\n`` only excludes LF, CR, LS, PS
43
+ ``\\d`` any Unicode decimal ``[0-9]``
44
+ ``\\w`` any Unicode word char ``[A-Za-z0-9_]``
45
+ ``\\s`` a different space set ECMA WhiteSpace plus
46
+ LineTerminator, incl. U+FEFF
47
+ and excl. U+001C-U+001F
48
+ ``\\b`` Unicode word boundary ASCII word boundary
49
+ ``\\D \\W \\S`` as above, negated explicit complement ranges,
50
+ inside classes too
51
+ ``\\p{...}`` rejected outright explicit ranges (``re``) or
52
+ native (``regex``)
53
+ ``(?<name>)`` ``(?P<name>)`` rewritten, ``$`` in names
54
+ escaped
55
+ ``\\k<name>`` ``(?P=name)`` rewritten
56
+ ================== ========================= ============================
57
+
58
+ Residual divergences
59
+ --------------------
60
+ These cannot be closed inside a backend pattern and are documented rather
61
+ than fixed:
62
+
63
+ * **Case folding.** The ``i`` flag compiles with ``re.IGNORECASE``, which
64
+ applies Python's full case folding. ECMA-262 ``u`` mode uses *simple*
65
+ case folding. The two agree on everything but a handful of code points
66
+ (for example Python folds ``ß`` to ``ss``-like equivalences that
67
+ ECMA-262 does not).
68
+ * **``\\b`` with both ``i`` and ``u``.** ECMA-262 adds U+017F and U+212A
69
+ to the word-character set in that combination. This package does not.
70
+ * **Variable-width lookbehind.** Python's :mod:`re` requires a fixed
71
+ width, so a variable-width lookbehind raises
72
+ :class:`UnsupportedPatternError` on the ``re`` backend. It works on the
73
+ ``regex`` backend.
74
+ * **Unicode scripts.** ``\\p{Script=...}`` and
75
+ ``\\p{Script_Extensions=...}`` parse, but the standard library ships no
76
+ script data, so they are only translatable on the ``regex`` backend, and
77
+ script *names* are not validated. General_Category values are validated
78
+ and translatable everywhere.
79
+ * **Binary properties.** Those derivable from the standard library
80
+ (``ASCII``, ``Assigned``, ``Any``, ``White_Space``, ``Lowercase``,
81
+ ``Uppercase``, ``XID_Start``, ``XID_Continue``, ``Hex_Digit``,
82
+ ``ASCII_Hex_Digit``, ``Bidi_Control``, ``Bidi_Mirrored``,
83
+ ``Join_Control``, ``Noncharacter_Code_Point``, ``Regional_Indicator``)
84
+ are translatable on both backends. The remaining ECMA-262 binary
85
+ properties (``Alphabetic``, ``Math``, ``Emoji``, ...) are recognized as
86
+ valid syntax but raise :class:`UnsupportedPatternError` on ``re``.
87
+ * **Unicode version.** Property sets come from the running interpreter's
88
+ :mod:`unicodedata`, not from the Unicode version a given JavaScript
89
+ engine ships.
90
+
91
+ Cost
92
+ ----
93
+ The first ``\\p{...}`` translated on the ``re`` backend scans every code
94
+ point to build its range list (roughly 50 ms for a General_Category
95
+ property, up to a few hundred for a predicate-derived binary property).
96
+ The result is cached process-wide, so every later use is a dict hit.
97
+ """
98
+
99
+ from .analysis import star_height, width
100
+ from .ast import (
101
+ Alternation,
102
+ Anchor,
103
+ Backreference,
104
+ CharClass,
105
+ ClassEscape,
106
+ ClassItem,
107
+ ClassRange,
108
+ Concatenation,
109
+ Dot,
110
+ Flags,
111
+ Group,
112
+ Literal,
113
+ Lookaround,
114
+ Node,
115
+ Pattern,
116
+ PropertyEscape,
117
+ Quantifier,
118
+ WordBoundary,
119
+ )
120
+ from .errors import EcmaRegexError, EcmaRegexSyntaxError, UnsupportedPatternError
121
+ from .matcher import CompiledPattern, EcmaRegex, compile
122
+ from .parser import parse
123
+ from .translator import BackendName, translate, translate_flags
124
+
125
+ __all__ = [
126
+ "Alternation",
127
+ "Anchor",
128
+ "BackendName",
129
+ "Backreference",
130
+ "CharClass",
131
+ "ClassEscape",
132
+ "ClassItem",
133
+ "ClassRange",
134
+ "CompiledPattern",
135
+ "Concatenation",
136
+ "Dot",
137
+ "EcmaRegex",
138
+ "EcmaRegexError",
139
+ "EcmaRegexSyntaxError",
140
+ "Flags",
141
+ "Group",
142
+ "Literal",
143
+ "Lookaround",
144
+ "Node",
145
+ "Pattern",
146
+ "PropertyEscape",
147
+ "Quantifier",
148
+ "UnsupportedPatternError",
149
+ "WordBoundary",
150
+ "compile",
151
+ "parse",
152
+ "star_height",
153
+ "translate",
154
+ "translate_flags",
155
+ "width",
156
+ ]
@@ -0,0 +1,95 @@
1
+ """Static analyses over a parsed pattern: star height and match width."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .ast import (
6
+ Alternation,
7
+ Anchor,
8
+ Backreference,
9
+ CharClass,
10
+ ClassEscape,
11
+ Concatenation,
12
+ Dot,
13
+ Group,
14
+ Literal,
15
+ Lookaround,
16
+ Node,
17
+ Pattern,
18
+ PropertyEscape,
19
+ Quantifier,
20
+ WordBoundary,
21
+ )
22
+
23
+ __all__ = ["is_fixed_width", "star_height", "width"]
24
+
25
+
26
+ def star_height(pattern: Pattern) -> int:
27
+ """The nesting depth of unbounded quantifiers in ``pattern``.
28
+
29
+ ``a`` is 0, ``a+`` is 1, ``(a+)+`` is 2. Bounded quantifiers such as
30
+ ``a{2,3}`` do not count; ``a{2,}`` does. A star height of 2 or more is
31
+ the classic screen for catastrophic backtracking (ReDoS): it is a
32
+ necessary condition, not a sufficient one, so it over-reports.
33
+ """
34
+ return _height(pattern.root)
35
+
36
+
37
+ def _height(node: Node) -> int:
38
+ match node:
39
+ case Quantifier(target=target, max=None):
40
+ return 1 + _height(target)
41
+ case Quantifier(target=target):
42
+ return _height(target)
43
+ case Alternation(options=children):
44
+ return max((_height(child) for child in children), default=0)
45
+ case Concatenation(parts=children):
46
+ return max((_height(child) for child in children), default=0)
47
+ case Group(body=body) | Lookaround(body=body):
48
+ return _height(body)
49
+ case _:
50
+ return 0
51
+
52
+
53
+ def width(node: Node) -> tuple[int, int | None]:
54
+ """The ``(minimum, maximum)`` number of code points ``node`` consumes.
55
+
56
+ ``maximum`` is ``None`` when it is unbounded or not statically known
57
+ (a backreference). A node is fixed-width when the two are equal.
58
+ """
59
+ match node:
60
+ case Literal() | Dot() | CharClass() | ClassEscape() | PropertyEscape():
61
+ return 1, 1
62
+ case Anchor() | WordBoundary() | Lookaround():
63
+ return 0, 0
64
+ case Backreference():
65
+ return 0, None
66
+ case Group(body=body):
67
+ return width(body)
68
+ case Concatenation(parts=parts):
69
+ low = 0
70
+ high: int | None = 0
71
+ for part in parts:
72
+ part_low, part_high = width(part)
73
+ low += part_low
74
+ high = None if high is None or part_high is None else high + part_high
75
+ return low, high
76
+ case Alternation(options=options):
77
+ widths = [width(option) for option in options]
78
+ low = min(bounds[0] for bounds in widths)
79
+ high = (
80
+ None
81
+ if any(bounds[1] is None for bounds in widths)
82
+ else max(bounds[1] for bounds in widths if bounds[1] is not None)
83
+ )
84
+ return low, high
85
+ case Quantifier(target=target, min=minimum, max=maximum):
86
+ target_low, target_high = width(target)
87
+ if maximum is None or target_high is None:
88
+ return target_low * minimum, None
89
+ return target_low * minimum, target_high * maximum
90
+
91
+
92
+ def is_fixed_width(node: Node) -> bool:
93
+ """Whether ``node`` always consumes the same number of code points."""
94
+ low, high = width(node)
95
+ return high is not None and low == high