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.
- ecma_regex-0.0.1/.gitignore +10 -0
- ecma_regex-0.0.1/LICENSE +21 -0
- ecma_regex-0.0.1/PKG-INFO +187 -0
- ecma_regex-0.0.1/README.md +162 -0
- ecma_regex-0.0.1/pyproject.toml +48 -0
- ecma_regex-0.0.1/src/ecma_regex/__init__.py +156 -0
- ecma_regex-0.0.1/src/ecma_regex/analysis.py +95 -0
- ecma_regex-0.0.1/src/ecma_regex/ast.py +218 -0
- ecma_regex-0.0.1/src/ecma_regex/errors.py +37 -0
- ecma_regex-0.0.1/src/ecma_regex/matcher.py +102 -0
- ecma_regex-0.0.1/src/ecma_regex/parser.py +575 -0
- ecma_regex-0.0.1/src/ecma_regex/translator.py +338 -0
- ecma_regex-0.0.1/src/ecma_regex/unicode_data.py +431 -0
- ecma_regex-0.0.1/tests/test_import.py +5 -0
- ecma_regex-0.0.1/tests/test_semantics.py +423 -0
- ecma_regex-0.0.1/tests/test_suite_table.py +115 -0
ecma_regex-0.0.1/LICENSE
ADDED
|
@@ -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
|