2 Commits

Author SHA1 Message Date
7079cde15f feat: add Python and Java regex engines 2026-07-27 02:02:21 +02:00
4951c966d4 feat: publish Regex Tools 0.2.0 2026-07-27 01:06:57 +02:00
225 changed files with 44002 additions and 546 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,8 @@
node_modules/ node_modules/
dist/ dist/
release/ release/
.engine-build/
engines/java/target/
coverage/ coverage/
playwright-report/ playwright-report/
test-results/ test-results/
@@ -9,4 +11,3 @@ test-results/
.env .env
.env.* .env.*
!.env.example !.env.example

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
public/engines/pcre2/pcre2.mjs
public/engines/java/
public/engines/python/

View File

@@ -1,5 +1,83 @@
# Changelog # Changelog
## Unreleased
## 0.3.0 — 2026-07-27
- Add a self-hosted Pyodide 314.0.3 worker containing CPython 3.14.2 `re`, with
runtime identity verification, bounded compilation/matching/replacement and
exact code-point-to-editor-UTF-16 range normalization.
- Add TeaVM 0.15.0's `java.util.regex` class-library implementation as a
verified self-hosted ES module, with bounded `Pattern`/`Matcher` execution,
native replacement and exact UTF-16 ranges.
- Identify the Java engine as TeaVM class-library code rather than OpenJDK and
report its documented `UNICODE_CHARACTER_CLASS`, single-digit replacement
and unavailable named-replacement compatibility boundaries.
- Add partial application-owned Python and Java lexical syntax and replacement
providers while keeping the selected runtime compiler authoritative.
- Separate heavyweight engine loading from the per-request execution deadline,
retain worker recovery after timeout/crash/cancellation and keep every
runtime local to the release.
- Add flavour-specific quick reference and capability content for all four
executable engines.
- Keep comparison, generated cases, code generation, minimization, analysis
and formatting behind their explicitly verified ECMAScript/PCRE2 boundaries;
Python and Java are never routed through another flavour as a fallback.
- Extend deterministic engine-pack provenance, checksums, licence notices,
release verification and nested-path delivery checks to the Python and Java
runtime assets.
## 0.2.0 — 2026-07-27
- Open Quick reference and Capabilities as populated, accessible in-viewport
dialogs with keyboard dismissal, focus handling and explicit empty states.
- Add flavour-neutral version, flag, option and execution-worker registries
and expose only the ECMAScript and fully wired PCRE2 runtimes.
- Validate flavour/version/options consistently across workbench requests,
projects and test suites, including legacy schema-v1 identities.
- Add a production local-first Corpus / Apply workspace with bounded UTF-8
multi-file and pasted-text input, sequential killable worker jobs, progress,
cancellation, explicit whole-document/independent-line semantics, preserved
line separators, bounded capture extraction and replacement previews,
per-document exact/partial summaries, content-free JSON/CSV/NDJSON reports
and privacy-gated exact-output downloads and bounded ZIP export.
- Keep corpus content and results ephemeral: project schema v1 stores only the
selected workspace mode and rejects injected corpus payloads.
- Add a deterministic, offline PCRE2 10.47 WebAssembly runtime with pinned
source/toolchain identities, provenance, licences and checksums.
- Add bounded PCRE2 compile, match and native-substitution ABI calls with
match/depth/heap caps, copied caller-owned records, cleanup on every exit,
UTF-8-to-UTF-16 range normalization and lone-surrogate rejection.
- Run PCRE2 in a dedicated killable/recoverable worker, expose its real
flavour-specific flags and options, and add lexical syntax/replacement
coverage, official quick reference, conformance and browser tests.
- Add an ABI-v3 PCRE2 automatic-callout operation with caller-owned event and
mark buffers, dual event/byte caps, native truncation stop, exact UTF-8 and
UTF-16 positions, a separate killable worker and a bounded viewer that keeps
reported fields distinct from derived movement labels.
- Add ECMAScript-scoped advisory static risk diagnostics plus cancellable
bounded growth probes and cold/warm native-engine benchmarks with median/p95,
separate timing/outcome plots, runtime identity and strict
input/output/wall limits.
- Add an ECMAScript/PCRE2 comparison orchestrator with exact per-side requests,
independent worker timeout outcomes, normalized-range match/capture
alignment, replacement differences and explicit non-comparable states.
- Add a reviewed PCRE2 10.47 8-bit C17 generator with exact UTF-8 byte arrays,
native/host limits, error handling, deterministic source identities and
compile/execute golden gates against the named toolchain.
- Add deterministic, cancellable failing-subject minimization for saved-test
failures, capture ranges, exact engine timeouts and ECMAScript/PCRE2
mismatches, with real-worker verification, explicit budgets and honest
transform-local minimality.
- Add deterministic, seed-addressed ECMAScript positive/negative case
generation from the normalized AST, with bounded synthesis, actual-engine
verification, explicit unsupported coverage, cancellable workers and
provenance-preserving unit-test integration.
- Add an idempotent grammar-backed ECMAScript literal/control formatter with an
exact preview, separate source/candidate syntax and engine workers, capture
shape checks, applicable unit-test replay, explicit inconclusive states and a
mandatory confirmation before apply.
## 0.1.0 — 2026-07-24 ## 0.1.0 — 2026-07-24
- Added the open-source ECMAScript 2025 syntax provider using regexpp 4.12.2. - Added the open-source ECMAScript 2025 syntax provider using regexpp 4.12.2.

View File

@@ -0,0 +1,19 @@
Copyright (c) 2018 Emscripten authors (see AUTHORS in Emscripten)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

104
LICENSES/PCRE2.txt Normal file
View File

@@ -0,0 +1,104 @@
PCRE2 Licence
=============
| SPDX-License-Identifier: | BSD-3-Clause WITH PCRE2-exception |
|---------|-------|
PCRE2 is a library of functions to support regular expressions whose syntax
and semantics are as close as possible to those of the Perl 5 language.
Releases 10.00 and above of PCRE2 are distributed under the terms of the "BSD"
licence, as specified below, with one exemption for certain binary
redistributions. The documentation for PCRE2, supplied in the "doc" directory,
is distributed under the same terms as the software itself. The data in the
testdata directory is not copyrighted and is in the public domain.
The basic library functions are written in C and are freestanding. Also
included in the distribution is a just-in-time compiler that can be used to
optimize pattern matching. This is an optional feature that can be omitted when
the library is built. The just-in-time compiler is separately licensed under the
"2-clause BSD" licence.
COPYRIGHT
---------
### The basic library functions
Written by: Philip Hazel
Email local part: Philip.Hazel
Email domain: gmail.com
Retired from University of Cambridge Computing Service,
Cambridge, England.
Copyright (c) 1997-2007 University of Cambridge
Copyright (c) 2007-2024 Philip Hazel
All rights reserved.
### PCRE2 Just-In-Time compilation support
Written by: Zoltan Herczeg
Email local part: hzmester
Email domain: freemail.hu
Copyright (c) 2010-2024 Zoltan Herczeg
All rights reserved.
### Stack-less Just-In-Time compiler
Written by: Zoltan Herczeg
Email local part: hzmester
Email domain: freemail.hu
Copyright (c) 2009-2024 Zoltan Herczeg
All rights reserved.
The code in the `deps/sljit` directory has its own LICENSE file.
### All other contributions
Many other contributors have participated in the authorship of PCRE2. As PCRE2
has never required a Contributor Licensing Agreement, or other copyright
assignment agreement, all contributions have copyright retained by each
original contributor or their employer.
THE "BSD" LICENCE
-----------------
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notices,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notices, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the University of Cambridge nor the names of any
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
EXEMPTION FOR BINARY LIBRARY-LIKE PACKAGES
------------------------------------------
The second condition in the BSD licence (covering binary redistributions) does
not apply all the way down a chain of software. If binary package A includes
PCRE2, it must respect the condition, but if package B is software that
includes package A, the condition is not imposed on package B unless it uses
PCRE2 independently.

256
README.md
View File

@@ -8,21 +8,64 @@ Patterns, replacement templates, test text and project files stay in the
browser. There is no account, backend, upload, remote execution, telemetry, browser. There is no account, backend, upload, remote execution, telemetry,
runtime CDN, remote font, or cloud-save service. runtime CDN, remote font, or cloud-save service.
## Current milestone ## Current release — 0.3.0
Version 0.1.0 is the first production flavour slice: ECMAScript. The v0.3.0 release contains four executable engine slices:
- the current browser's native ECMAScript `RegExp`;
- official PCRE2 10.47 as a self-hosted 8-bit WebAssembly pack;
- CPython 3.14.2 `re` in the self-hosted Pyodide 314.0.3 runtime; and
- TeaVM 0.15.0's `java.util.regex` class-library implementation as a
self-hosted ES2015 module.
The Java flavour is TeaVM class-library code compiled to JavaScript. It is not
OpenJDK and is not advertised as an OpenJDK compatibility oracle.
- Syntax acceptance: `@eslint-community/regexpp` 4.12.2, ECMAScript 2025 - Syntax acceptance: `@eslint-community/regexpp` 4.12.2, ECMAScript 2025
grammar; application-owned structural explanations are partial and grammar; application-owned structural explanations are partial and
feature-matrix tested. feature-matrix tested.
- Execution engine: the current browser's native ECMAScript `RegExp`. - ECMAScript native and editor offsets are UTF-16 code units.
- Native and editor offsets: UTF-16 code units. - PCRE2 execution and substitution: pinned official PCRE2 10.47 compiled to a
self-hosted 8-bit WebAssembly pack with UTF/UCP always enabled.
- PCRE2 native UTF-8 byte ranges are retained and normalized to exact editor
UTF-16 ranges; inputs with lone surrogates are rejected instead of rewritten.
- PCRE2 match-step, depth and heap limits are configurable inside hard caps.
- PCRE2 automatic callouts are copied through a separate ABI operation and
displayed from a dedicated killable trace worker under 50,000-event and
10 MiB serialized-data hard caps.
- Python compilation, matching and replacement use the bundled CPython `re`
module. Native code-point ranges are retained and normalized to exact editor
UTF-16 ranges.
- Java compilation, matching and replacement use TeaVM's
`java.util.regex.Pattern` and `Matcher`. Native ranges are UTF-16 code units.
- TeaVM 0.15.0 replacement supports its actual single-digit `$n` subset.
`$12` is therefore `$1` followed by literal `2`; Java SE `${name}`
replacement is reported as unavailable rather than presented as OpenJDK
behavior.
- Python and Java use deliberately partial application-owned lexical syntax
providers; their actual runtime compiler remains authoritative.
- Parsing and execution run in separate workers. - Parsing and execution run in separate workers.
- Actual execution can be terminated by killing its worker. - Actual execution can be terminated by killing its worker.
- The worker is recreated after timeout, crash or cancellation. - The worker is recreated after timeout, crash or cancellation.
- Capture ranges use the engine's `d` indices result. - ECMAScript capture ranges use the engine's `d` indices result; the other
flavours return their native range records through bounded bridges.
- Named, numbered, optional, empty and repeated-final captures are represented. - Named, numbered, optional, empty and repeated-final captures are represented.
- Match, replacement, typed list/extraction and unit-test modes are available. - Match, replacement, typed list/extraction, corpus/apply and unit-test modes
are available.
- ECMAScript-specific advisory static risk findings, bounded dynamic growth
probes and cold/warm benchmarks run locally; timing and growth execution is
isolated in a separately killable worker.
- ECMAScript and PCRE2 can be compared through two exact, independently timed
requests without translating patterns, flags or replacements. Incomplete or
rejected sides are explicitly not comparable.
- The reviewed PCRE2 C17 generator emits exact UTF-8 byte arrays, configured
limits and error handling for the PCRE2 10.47 8-bit API.
- A cancellable, deterministic subject minimizer verifies saved-test failures,
capture-range failures, exact timeouts and ECMAScript/PCRE2 mismatches against
the selected real workers under explicit evaluation and wall budgets.
- Deterministic ECMAScript test-case generation samples the normalized AST from
an explicit seed, reports structural coverage and unsupported constructs, and
labels cases only after verification by the selected real engine worker.
- Project JSON import/export and optional IndexedDB save are available. - Project JSON import/export and optional IndexedDB save are available.
- Interactive test text is not persisted by default. - Interactive test text is not persisted by default.
@@ -40,39 +83,70 @@ hierarchy uses syntactic capture nesting while preserving actual engine ranges.
ECMAScript exposes only the final retained capture of a repeated group; Regex ECMAScript exposes only the final retained capture of a repeated group; Regex
Tools says so and never invents capture history. Tools says so and never invents capture history.
Browser ECMAScript engines do not expose their internal backtracking trace. Browser ECMAScript exposes no actual engine trace. PCRE2 has a separate bounded
Version 0.1.0 therefore presents no ECMAScript execution trace and no automatic-callout viewer. Its event fields are reported by PCRE2, while
educational trace disguised as one. “forward”, “same position” and “apparent backtrack” are visibly derived only
from adjacent reported subject positions. The callout stream is not described
as a complete record of every internal engine action.
## Workbench modes ## Workbench modes
- **Match** — live highlighting, extraction tree and bounded capture table. - **Match** — live highlighting, extraction tree and bounded capture table.
- **Replace** — native ECMAScript `RegExp` matches with bounded, - **Replace** — engine-native bounded substitution for all four executable
specification-compatible string substitution; output completeness is flavours;
reported separately from bounded per-match original/result, changed-range and output completeness is reported separately from bounded per-match
token-contribution previews that synchronize the replacement, pattern, original/result, changed-range and token-contribution previews that
subject and output editors. synchronize the replacement, pattern, subject and output editors.
- **List** — non-executable typed templates with text, CSV, JSON and NDJSON - **List** — non-executable typed templates with text, CSV, JSON and NDJSON
export; export is disabled when engine collection or bounded rendering is export; export is disabled when engine collection or bounded rendering is
incomplete. incomplete.
- **Corpus / Apply** — bounded multi-file UTF-8 and pasted-text input processed
sequentially in killable workers. Whole-document and independent-line
semantics are explicit; line apply preserves original separators. Progress,
cancellation, bounded capture samples, replacement previews, per-document
exact/partial summaries, content-free JSON/CSV/NDJSON reports and
privacy-gated exact-output ZIP/downloads are available. Original files are
never modified.
- **Unit tests** — editable and cloneable versioned presence, count, full-match, - **Unit tests** — editable and cloneable versioned presence, count, full-match,
capture, replacement, timing and expected-timeout assertions. Capture a capture, replacement, timing and expected-timeout assertions. Capture a
complete current result, select only cases to rerun, filter failures, and complete current result, select only cases to rerun, filter failures, and
import/export a standalone validated JSON suite; execution stays in isolated import/export a standalone validated JSON suite; execution stays in isolated
workers. workers.
Corpus, comparison, debugging, analysis, generation, minimization, formatting The auxiliary **PCRE trace**, ECMAScript **Analysis**, **Compare & code**,
and code generation remain roadmap items. Controls for them are not exposed in **Generate cases**, **Minimize** and **Format** workspaces are separate from
this release. normal matching, replacement, tests and corpus execution. Trace collection is
never included in benchmark measurements.
Comparison retains native ranges while aligning actual results by normalized
editor UTF-16 ranges. Agreement is described only as evidence for the current
subject, never as proof of pattern equivalence.
Case generation is bounded sampling, not a proof of language coverage. Version
1 supports only a complete accepted ECMAScript normalized AST; the partial
PCRE2 provider is reported as unavailable rather than passed through or
relabeled. See
[`docs/GENERATED_CASES.md`](docs/GENERATED_CASES.md).
Minimization verifies every accepted candidate against the exact selected
worker request and claims only local minimality under its documented
Unicode-scalar transforms; see
[`docs/MINIMIZATION.md`](docs/MINIMIZATION.md).
Formatting is currently ECMAScript-only and changes only parser-reported
literal slash/control representations. Apply requires independent reparse,
actual-engine current-subject/replacement comparison, every applicable exact
unit test and explicit confirmation; see
[`docs/FORMATTING.md`](docs/FORMATTING.md).
## Flags and iteration ## Flags and iteration
Pattern and flags are stored separately; delimiters are presentation only. Pattern and flags are stored separately; delimiters are presentation only.
Native `g` and `y` behavior is preserved. The optional **Scan all** action is ECMAScript preserves native `g` and `y`; PCRE2 offers the application-level
explicit. When neither `g` nor `y` is present it adds `g` only to that execution `g` iteration flag plus `i`, `m`, `s`, `x`, `U` and `J`. PCRE2 UTF/UCP mode is
request, visibly records the internal flag, and does not change the saved user mandatory because browser strings are Unicode and partial-byte ranges cannot be
flags. The `d` flag may likewise be added internally to obtain exact indices represented safely in the editor. Python exposes application-level `g` plus
without changing matching semantics. CPython `re` flags `a`, `i`, `m`, `s` and `x`. Java exposes application-level
`g` plus TeaVM `Pattern` flags `i`, `d`, `m`, `s`, `u`, `x` and the documented
`U` compatibility request. The optional **Scan all** action adds only an
internal iteration request and does not change saved flags. ECMAScript may add
internal `d` to obtain exact indices without changing match semantics.
Zero-length global iteration advances using the selected Unicode semantics and Zero-length global iteration advances using the selected Unicode semantics and
cannot loop forever. cannot loop forever.
@@ -82,7 +156,7 @@ cannot loop forever.
Important defaults: Important defaults:
| Limit | Default | | Limit | Default |
| ------------------------------ | -----------------: | | ------------------------------- | -----------------: |
| Live parse debounce | 120 ms | | Live parse debounce | 120 ms |
| Live execution debounce | 220 ms | | Live execution debounce | 220 ms |
| Live execution timeout | 250 ms | | Live execution timeout | 250 ms |
@@ -90,6 +164,16 @@ Important defaults:
| Advanced maximum timeout | 10 s | | Advanced maximum timeout | 10 s |
| Pattern hard limit | 64 Ki UTF-16 units | | Pattern hard limit | 64 Ki UTF-16 units |
| Interactive subject hard limit | 16 MiB | | Interactive subject hard limit | 16 MiB |
| Corpus documents | 256 |
| Per corpus document | 16 MiB |
| Aggregate corpus input | 256 MiB |
| Independent corpus lines | 100,000 |
| Aggregate corpus matches | 100,000 |
| Capture summaries per document | 32 |
| Samples per capture | 3 |
| Retained applied output | 64 MiB |
| Applied ZIP | 68 MiB |
| Corpus batch wall time | 5 min |
| Replacement template | 64 Ki UTF-16 units | | Replacement template | 64 Ki UTF-16 units |
| List template | 16 Ki UTF-16 units | | List template | 16 Ki UTF-16 units |
| Per-match replacement preview | 200 matches | | Per-match replacement preview | 200 matches |
@@ -104,6 +188,30 @@ Important defaults:
| Tests per suite | 1,000 | | Tests per suite | 1,000 |
| Retained message per test | 2 KiB UTF-8 | | Retained message per test | 2 KiB UTF-8 |
| Unit-test suite wall time | 60 s | | Unit-test suite wall time | 60 s |
| PCRE2 trace events | 50,000 |
| PCRE2 serialized trace | 10 MiB |
| Comparison retained differences | 5,000 |
| Comparison retained alignments | 2,000 |
| Static analysis traversal | 20,000 nodes |
| Static analysis findings | 100 |
| Growth probe steps | 24 |
| Analysis aggregate wall time | 30 s |
| Generated verified cases | 48 |
| Generated candidate attempts | 192 |
| Generation aggregate wall time | 15 s |
| Generation aggregate candidates | 1 MiB |
| Minimizer subject | 1 MiB UTF-8 |
| Minimizer candidate evaluations | 2,000 |
| Minimizer aggregate wall time | 60 s |
| Formatter validation tests | 1,000 |
| Formatter test wall time | 60 s |
| Retained minimizer history | 200 |
Analysis defaults to three warm-up plus 15 measured samples (caps: 100 and
1,000), with a 2-second per-sample worker limit that can be raised to 10
seconds. Growth generation defaults to 1 MiB, preflights the central 16 MiB
subject cap and rejects more than 10,000,000 repetitions. Replacement timing
is skipped above an 8 Mi UTF-16-unit output estimate.
A timeout terminates actual execution. It is reported as **timed out**, never as A timeout terminates actual execution. It is reported as **timed out**, never as
“no match”. The next request creates a fresh worker. “no match”. The next request creates a fresh worker.
@@ -128,8 +236,12 @@ require separate explicit opt-ins for JSON export and IndexedDB save.
Standalone test-suite JSON likewise omits subjects by default, is schema- and Standalone test-suite JSON likewise omits subjects by default, is schema- and
field-validated on import, and appends at most 1,000 total tests. Add, update, field-validated on import, and appends at most 1,000 total tests. Add, update,
clone and append-import also preflight the complete in-memory suite against a clone and append-import also preflight the complete in-memory suite against a
32 MiB aggregate bound. Corpus content is not supported or persisted in this 32 MiB aggregate bound. Added generated tests retain their validated generator
release. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8 version, seed, candidate ID and intended outcome; editing one removes that
provenance. Schema v1 can remember that the corpus workspace was selected, but
corpus documents, contents, outputs and results are always ephemeral and
excluded from JSON and IndexedDB. Imports that try to inject those fields are
rejected. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8
JSON-document limit in addition to per-field limits. The latest local save is JSON-document limit in addition to per-field limits. The latest local save is
read through an `updatedAt` index cursor; loading it does not materialize the read through an `updatedAt` index cursor; loading it does not materialize the
complete project store. complete project store.
@@ -154,6 +266,13 @@ npm run test:conformance
npm run test:browser npm run test:browser
npm run engines:build npm run engines:build
npm run engines:verify npm run engines:verify
npm run engines:pcre2:build -- --source-dir /path/to/pcre2 --emcc /path/to/emcc
npm run engines:pcre2:verify
npm run engines:pcre2:install
node scripts/python-engine-pack.mjs node_modules/pyodide .engine-build/python
node scripts/install-python-engine.mjs .engine-build/python
node scripts/java-engine-pack.mjs
node scripts/install-java-engine.mjs
npm run build npm run build
npm run toolbox:check npm run toolbox:check
npm run check npm run check
@@ -173,20 +292,49 @@ The deterministic release ZIP can be consumed by Toolbox Portal using an exact
artifact URL, manifest ID, version and SHA-256. Portal builds consume the artifact URL, manifest ID, version and SHA-256. Portal builds consume the
artifact; they do not build Regex Tools source. artifact; they do not build Regex Tools source.
Version 0.1.0 has no WebAssembly. Its minimum CSP needs self-hosted scripts and The PCRE2 and Python workers load only self-hosted JavaScript, WebAssembly and
workers, not `wasm-unsafe-eval`. See Python-standard-library assets; Java loads its self-hosted ES module.
Deployment must serve `.wasm` as `application/wasm` and allow WebAssembly
compilation in `script-src`; see
[`docs/PORTAL_REQUIREMENTS.md`](docs/PORTAL_REQUIREMENTS.md). [`docs/PORTAL_REQUIREMENTS.md`](docs/PORTAL_REQUIREMENTS.md).
## Flavour roadmap ## Flavour support and boundaries
The next flavour is official PCRE2 10.47 WebAssembly. A technical spike from the PCRE2's application-owned ABI provides bounded compilation, matching, native
signed official tag has already demonstrated Unicode matching, named captures, substitution and separate automatic-callout tracing, copied caller-owned
substitution, automatic and explicit callouts, and match/depth/heap limits. records, deterministic allocation cleanup and explicit result completeness.
PCRE2 is not shipped until the application-owned bridge, byte/UTF-16 offsets, The exact source/toolchain build is reproducible offline and the verified pack
trace caps, browser tests and source/licence material meet the acceptance gate. is bundled with its metadata, checksums and licence.
Python, Go, Rust, .NET, Java and legacy PCRE follow only through their actual The Python slice executes actual CPython 3.14.2 `re` in Pyodide 314.0.3; it does
named runtimes. No flavour is emulated through JavaScript and relabelled. not translate a Python pattern into ECMAScript. Pyodide startup is a separate
worker-load operation and is excluded from the normal execution deadline.
The Java slice executes TeaVM 0.15.0's own `java.util.regex` class library. It
does not translate a Java pattern into ECMAScript, but it is also not OpenJDK:
in particular, TeaVM 0.15.0 does not implement
`Pattern.UNICODE_CHARACTER_CLASS`. The UI reports the `U` flag as a
compatibility request and preserves TeaVM's predefined-class and word-boundary
behaviour.
Generated-case version 1 is ECMAScript-only. It traverses the complete
application-owned ECMAScript AST under node, depth, attempt, byte, repetition
and wall-time caps, then verifies every retained label through the actual
browser `RegExp` adapter. PCRE2 generation remains unavailable until its syntax
provider can expose a complete normalized tree.
Pattern formatting is likewise ECMAScript-only. It consumes the complete
regexpp token model and does not reinterpret the partial PCRE2, Python or Java
structures. Those flavours remain unavailable until complete grammar-backed
implementations pass equivalent idempotence, reparse, engine and test gates.
The first advertised generated-code target is the PCRE2 10.47 8-bit C API.
Its deterministic match and replacement fixtures compile and execute against
the exact official release. See
[`docs/COMPARISON_AND_CODEGEN.md`](docs/COMPARISON_AND_CODEGEN.md).
Go, Rust, .NET and legacy PCRE follow only through their actual named runtimes.
No flavour is emulated through JavaScript and relabelled.
## Licensing ## Licensing
@@ -198,7 +346,7 @@ notices; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) and
No regex101 application source, branding, generated explanation prose, No regex101 application source, branding, generated explanation prose,
reference text or visual design is copied. RegexLib and RGXP.RU fixtures are not reference text or visual design is copied. RegexLib and RGXP.RU fixtures are not
redistributed because a suitable fixture licence was not established. No redistributed because a suitable fixture licence was not established. No
RegexHub fixture is shipped in v0.1.0. RegexHub fixture is shipped in v0.3.0.
## Known limitations ## Known limitations
@@ -211,7 +359,25 @@ RegexHub fixture is shipped in v0.1.0.
error node, not a fabricated multi-character span or partial provider AST. error node, not a fabricated multi-character span or partial provider AST.
- Native JavaScript compile errors do not consistently expose a machine-readable - Native JavaScript compile errors do not consistently expose a machine-readable
source offset; the provider diagnostic supplies the range. source offset; the provider diagnostic supplies the range.
- Capture history and actual ECMAScript execution tracing are unavailable. - Capture history is unavailable. Browser ECMAScript exposes no engine trace;
PCRE2 exposes bounded automatic callouts, not a complete internal trace.
Movement classifications are derived and labelled as such.
- Static risk heuristics and generated-input observations apply only to
ECMAScript 2025 and are advisory. They are never proof of safety or of a
patterns general time complexity; analysis settings and results are
ephemeral.
- PCRE2 structural explanations are deliberately partial. The application
recognizes common capture forms and PCRE2-only constructs for reference, but
the actual PCRE2 compiler is authoritative. Branch-reset capture numbering is
omitted from the explanation rather than guessed.
- Python and Java structural explanations are deliberately partial lexical
models. CPython `re` and TeaVM `Pattern` respectively remain authoritative
for compilation, group count and matching.
- Python reports native Unicode code-point offsets and converts them at valid
boundaries to editor UTF-16 ranges. Java and ECMAScript report native UTF-16
offsets; PCRE2 reports native UTF-8 bytes. Native units remain visible.
- TeaVM `java.util.regex` is not OpenJDK. Its `U` compatibility request does
not add OpenJDK's `UNICODE_CHARACTER_CLASS` semantics.
- Capture-table rendering is paged at 200 rows while aggregate collection is - Capture-table rendering is paged at 200 rows while aggregate collection is
bounded separately. bounded separately.
- Explanation/extraction trees and editor decorations show bounded 2,000-node - Explanation/extraction trees and editor decorations show bounded 2,000-node
@@ -235,8 +401,20 @@ RegexHub fixture is shipped in v0.1.0.
- Unit-test failure messages retain at most 2 KiB UTF-8 per case, so 1,000 - Unit-test failure messages retain at most 2 KiB UTF-8 per case, so 1,000
results stay below the 2 MiB log budget. Oversized exact replacement results results stay below the 2 MiB log budget. Oversized exact replacement results
offer a lightweight `should-match` draft when one is valid and fits the suite. offer a lightweight `should-match` draft when one is valid and fits the suite.
- Benchmark, risk analysis, generated cases, minimization and corpus processing - Corpus input accepts recognized UTF-8 text only. It does not auto-detect
are not in this release. legacy encodings or recursively traverse directories.
- Corpus capture extraction is a bounded per-document summary: full match plus
at most 31 capture groups, three 160-unit samples each. Content-free reports
include counts and clipping metadata, never the samples themselves.
- Generated cases deliberately sample only ECMAScript and report
backreferences, lookaround, boundaries, complex Unicode sets and other
unsupported constraints instead of claiming exhaustive coverage.
- Pattern formatting is intentionally narrow and ECMAScript-only. Its bounded
source/candidate and unit-test validation is evidence for the checked
snapshots, not a proof of equivalence over all subjects.
- Cross-flavour comparison, PCRE2 C generation and subject minimization remain
scoped to their explicitly supported ECMAScript/PCRE2 paths; selecting
Python or Java does not silently substitute one of those engines.
Architecture, security, performance, provenance and flavour details live in Architecture, security, performance, provenance and flavour details live in
[`docs/`](docs/). [`docs/`](docs/).

View File

@@ -1,9 +1,10 @@
# Source identity # Source identity
- Project: Regex Tools - Project: Regex Tools
- Version: 0.1.0 - Release version: `0.3.0`
- Release tag: `v0.3.0`
- Repository: <https://git.add-ideas.de/zemion/regex-tools> - Repository: <https://git.add-ideas.de/zemion/regex-tools>
- Release tag: `v0.1.0` - Previous release tag: `v0.2.0`
- Licence: GPL-3.0-or-later - Licence: GPL-3.0-or-later
- Copyright: © 2026 Albrecht Degering - Copyright: © 2026 Albrecht Degering
@@ -19,5 +20,26 @@ runtime or build-time CDN fetch. Complete preferred-form source is the tagged
repository. The release archive contains compiled static assets plus legal, repository. The release archive contains compiled static assets plus legal,
source-identity and attribution documents. source-identity and attribution documents.
No PCRE2, Python, Go, Rust, .NET, Java or legacy-PCRE runtime is included in The engine-pack inputs and rebuild gates are documented in
version 0.1.0. `docs/ENGINE_BUILDS.md`.
- PCRE2 source is not vendored: the gate accepts only the pinned, clean
official 10.47 checkout and Emscripten 6.0.4 compiler.
- The Python pack is copied from the exact pinned Pyodide 314.0.3 npm package
and contains its CPython 3.14.2 runtime, Python standard library, loader,
WebAssembly module, metadata, checksums and licence material.
- The Java bridge source is under `engines/java/`. Its bundled module is
compiled against TeaVM 0.15.0's `java.util.regex` class library and is
identified as TeaVM rather than OpenJDK.
Generated staging is ignored. Only verified runtime packs with deterministic
metadata, closed file sets, checksums and exact licence/notice material are
installed under `public/engines/` and included in the application source and
build.
The v0.3.0 release includes native browser ECMAScript, PCRE2 10.47, CPython
3.14.2 `re` through Pyodide 314.0.3 and TeaVM 0.15.0 `java.util.regex`. Go,
Rust, .NET and legacy PCRE remain unimplemented. Earlier releases remain
available under their historical tags and artifact coordinates. A future
public artifact must bump package, manifest, tag and source identity together;
it must not reuse `v0.3.0`.

View File

@@ -1,18 +1,23 @@
# Third-party notices # Third-party notices
Regex Tools original source is GPL-3.0-or-later. The production browser bundle Regex Tools original source is GPL-3.0-or-later. The production browser bundle
also contains the compatible components below. Exact dependency resolution is contains the compatible runtime components below.
recorded in `package-lock.json`. Exact dependency resolution is recorded in `package-lock.json`.
| Component | Version | Licence | Shipped | Role and source | | Component | Version | Licence | Shipped | Role and source |
| -------------------------------- | ------------------------------------------------------------------------ | ---------- | ----------------- | ------------------------------------------------------------------------ | | -------------------------------- | ------------------------------------------------------------------------ | --------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------- |
| `@add-ideas/toolbox-contract` | 0.2.2 | Apache-2.0 | Runtime | Manifest contract; <https://git.add-ideas.de/zemion/toolbox-sdk> | | `@add-ideas/toolbox-contract` | 0.2.2 | Apache-2.0 | Runtime | Manifest contract; <https://git.add-ideas.de/zemion/toolbox-sdk> |
| `@add-ideas/toolbox-shell-react` | 0.2.2 | Apache-2.0 | Runtime | Shared shell; same source | | `@add-ideas/toolbox-shell-react` | 0.2.2 | Apache-2.0 | Runtime | Shared shell; same source |
| `@eslint-community/regexpp` | 4.12.2 | MIT | Syntax worker | ECMAScript parser; <https://github.com/eslint-community/regexpp> | | `@eslint-community/regexpp` | 4.12.2 | MIT | Syntax worker | ECMAScript parser; <https://github.com/eslint-community/regexpp> |
| CodeMirror packages | state 6.7.1; view 6.43.6; language 6.12.4; commands 6.10.4; search 6.7.1 | MIT | Runtime | Editors; <https://github.com/codemirror> | | CodeMirror packages | state 6.7.1; view 6.43.6; language 6.12.4; commands 6.10.4; search 6.7.1 | MIT | Runtime | Editors; <https://github.com/codemirror> |
| `@lezer/highlight` | 1.2.3 | MIT | Runtime | Editor highlighting support; <https://github.com/lezer-parser/highlight> | | `@lezer/highlight` | 1.2.3 | MIT | Runtime | Editor highlighting support; <https://github.com/lezer-parser/highlight> |
| React / React DOM | 19.2.7 | MIT | Runtime | User interface; <https://github.com/facebook/react> | | React / React DOM | 19.2.7 | MIT | Runtime | User interface; <https://github.com/facebook/react> |
| `fflate` | 0.8.3 | MIT | Packaging utility | Deterministic release ZIP; <https://github.com/101arrowz/fflate> | | `fflate` | 0.8.3 | MIT | Runtime and build dependency | Local corpus-output ZIP and deterministic release ZIP; <https://github.com/101arrowz/fflate> |
| Emscripten generated runtime | 6.0.4 | MIT | Generated WebAssembly glue | PCRE2 module loader/runtime; <https://github.com/emscripten-core/emscripten> |
| PCRE2 | 10.47 | BSD-3-Clause WITH PCRE2-exception | WebAssembly runtime | Pinned official 8-bit engine; <https://github.com/PCRE2Project/pcre2> |
| Pyodide | 314.0.3 | MPL-2.0 | Python runtime pack | Self-hosted CPython browser runtime; <https://github.com/pyodide/pyodide> |
| CPython | 3.14.2 | PSF-2.0 | Python runtime/stdlib | `re` execution and bundled standard library; <https://github.com/python/cpython> |
| TeaVM | 0.15.0 | Apache-2.0 | Generated Java engine module | `java.util.regex` class-library implementation; <https://github.com/konsoletyper/teavm> |
| Vite | 8.1.5 | MIT | Generated helpers | Production build; <https://github.com/vitejs/vite> | | Vite | 8.1.5 | MIT | Generated helpers | Production build; <https://github.com/vitejs/vite> |
| Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; <https://github.com/rolldown/rolldown> | | Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; <https://github.com/rolldown/rolldown> |
@@ -20,10 +25,14 @@ recorded in `package-lock.json`.
development-only and are not part of the static runtime bundle. development-only and are not part of the static runtime bundle.
`recheck` 4.5.0 and `regexp-ast-analysis` 0.7.1 were inspected but deliberately `recheck` 4.5.0 and `regexp-ast-analysis` 0.7.1 were inspected but deliberately
not installed or shipped. PCRE2 10.47 was used only in an external feasibility not installed or shipped. PCRE2 source is not vendored; its generated
spike and is not present in the v0.1.0 artifact. WebAssembly pack is shipped with exact metadata, checksums and licence. Pyodide
is pinned as an npm build input and its reviewed self-hosted runtime files are
copied into the Python pack. The TeaVM module contains its compiled class
library and is explicitly identified as TeaVM, not OpenJDK.
Corresponding licence texts and copyright notices are in `LICENSES/`. The Licence texts and copyright notices for the ordinary bundled dependencies are
release package additionally carries the exact Vite and Rolldown legal files in `LICENSES/`. Each engine pack carries its exact upstream licence/notice
under `LICENSES/build/`, including Vite's bundled-dependency notices for code files alongside the runtime. The release package additionally carries the exact
emitted by the production build. Vite and Rolldown legal files under `LICENSES/build/`, including Vite's
bundled-dependency notices for code emitted by the production build.

121
docs/ANALYSIS.md Normal file
View File

@@ -0,0 +1,121 @@
# Performance and risk analysis
Regex Tools provides one deliberately scoped analysis implementation:
ECMAScript 2025 patterns parsed into the application-owned normalized AST and
executed by the browser's native `RegExp` runtime. The analyser does not apply
ECMAScript findings to PCRE2, Python, Java or any other flavour.
Analysis is advisory. The UI uses “potential risk”, “possibly” and “observed
under selected limits”. It never turns an empty finding list into “safe”,
“guaranteed linear” or a vulnerability proof.
## Static analysis
The application-owned `regex-tools-ecmascript-heuristics` analyser walks at
most 20,000 normalized nodes and returns at most 100 findings. Each finding
contains:
- flavour and UTF-16 source range;
- stable rule identifier;
- static or dynamically observed provenance;
- explanation and example risk;
- low, medium or high confidence;
- limitations;
- a suggested investigation.
The current rules review nested and nullable repetition, ambiguous repeated
alternatives, overlapping alternative prefixes, wildcards and backreferences
inside repetition, repeated lookaround bodies, unanchored amplification,
capture count, nesting depth and replacement-output expansion. These are
structural heuristics. They do not model the native engine's complete compiled
program, optimizations or calling context.
## Dynamic growth probes
The user defines a bounded subject family:
```text
prefix + repeatedFragment.repeat(repetitions) + suffix
```
The byte and UTF-16 size are calculated before the generated string is
allocated. The repeated fragment is limited to 4,096 UTF-16 units, each affix
to 16,384 units, the run to 24 samples, and repetitions to 10,000,000. The UI
defaults to a 1 MiB generated-subject cap even though the central interactive
hard cap remains 16 MiB.
Every probe executes in a dedicated worker. The supervisor terminates and
discards that worker on per-sample timeout, cancellation or crash. Runs also
stop at:
- the selected repetition bound;
- the selected generated-byte bound;
- the selected step bound;
- the 30-second aggregate wall bound;
- a selected normalized-growth threshold.
The growth threshold compares the time ratio with the input-size ratio for two
adjacent samples. Sub-millisecond measurements below a noise floor do not
trigger it. A threshold observation characterizes only those generated inputs
and that browser. Timeout, crash, compile rejection and non-match remain
distinct states.
The UI renders time and outcome in separate plots and keeps the exact sample
table alongside them.
## Benchmark method
A benchmark begins with a fresh analysis worker. Worker creation and its
identity response form the separate cold-start measurement. The first native
engine use is retained as a cold sample. Configured warm-up samples are
discarded; configured measured samples produce the warm statistics.
Each sample measures these phases separately:
- `RegExp` construction;
- first `exec`;
- bounded all-match iteration;
- native string replacement when its conservative output bound is acceptable;
- all-match throughput and match count.
All-match iteration retains only counts, not values or captures. It follows the
same explicit scan-all, global, sticky and Unicode zero-length advancement
semantics as the workbench. The application-added indices flag remains in the
effective flags so the measured operation reflects interactive execution.
Replacement timing is skipped when match collection reaches its bound or a
conservative estimate can exceed 8 Mi UTF-16 units. This prevents a benchmark
from using an unbounded native replacement merely to obtain a timing.
It is also skipped for explicit scan-all with sticky `y`: native
`String.replace` processes only one sticky match, while the workbench's
explicit scan-all operation can process a contiguous sequence. Reporting that
different operation as the same benchmark would be misleading.
Warm metrics include sample count, minimum, conventional median, nearest-rank
p95 and maximum. Milliseconds are shown to two decimal places, with values
below 0.01 ms labelled accordingly; this does not imply nanosecond accuracy.
The result retains subject size, match count, effective flags, browser runtime
identity and engine identity.
Defaults:
| Setting | Default | Hard bound |
| ----------------------- | ------: | ---------: |
| Warm-up samples | 3 | 100 |
| Measured samples | 15 | 1,000 |
| Per-sample timeout | 2 s | 10 s |
| Aggregate wall time | 30 s | 30 s |
| Matches per sample | 10,000 | 10,000 |
| Replacement output gate | 8 MiU | 8 MiU |
One subject is not a complete performance characterization. Results from
different algorithms, pattern ports, browser engines, WebAssembly runtimes or
machines are not directly comparable. Trace-enabled execution is never used
for benchmark results.
## Privacy and persistence
Patterns, subjects, generated inputs, findings and benchmark samples remain in
the browser. No analysis request is uploaded. Analysis settings and results are
currently ephemeral and are not added to project JSON or IndexedDB; this also
keeps large benchmark subjects out of persisted projects by default.

View File

@@ -5,9 +5,36 @@ boundaries.
```text ```text
React workbench React workbench
├─ Pattern SyntaxSupervisor → syntax.worker → Regexpp provider → normalized AST ├─ Pattern SyntaxSupervisor → syntax.worker
│ ├─ Regexpp ECMAScript provider → normalized AST
│ ├─ PCRE2 lexical provider → partial normalized structure
│ ├─ Python lexical provider → partial normalized structure
│ └─ Java lexical provider → partial normalized structure
├─ Replacement SyntaxSupervisor → syntax.worker → typed replacement tokens ├─ Replacement SyntaxSupervisor → syntax.worker → typed replacement tokens
├─ EngineSupervisor → ecmascript.worker → native RegExp → match DTOs ├─ EngineSupervisor
│ ├─ ecmascript.worker → native RegExp → match DTOs
│ ├─ pcre2.worker → PCRE2 10.47 WASM ABI → match DTOs
│ ├─ python.worker → Pyodide 314.0.3 → CPython 3.14.2 re → match DTOs
│ └─ java.worker → TeaVM 0.15.0 java.util.regex module → match DTOs
├─ Pcre2TraceSupervisor → pcre2-trace.worker
│ └─ separate ABI-v3 PCRE2_AUTO_CALLOUT operation → bounded trace DTOs
├─ AnalysisPanel
│ ├─ normalized-AST ECMAScript static heuristics
│ └─ AnalysisSupervisor → analysis.worker → native RegExp benchmark/growth probes
├─ ComparisonOrchestrator
│ ├─ two exact syntax snapshots + independently killable engine workers
│ └─ normalized UTF-16 match/capture alignment + explicit incomplete states
├─ PCRE2 C17 generator → validated snapshot → exact UTF-8 byte-array source
├─ SubjectMinimizer
│ ├─ fixed syntax snapshots + exact real-engine failure or comparison oracle
│ └─ deterministic Unicode-scalar reducer → bounded local-minimal result
├─ CaseGenerationOrchestrator
│ ├─ generation.worker → deterministic bounded ECMAScript AST candidates
│ └─ EngineSupervisor → selected actual-engine label verification
├─ ECMAScript literal/control formatter
│ ├─ exact regexpp literal-token ranges → idempotent candidate preview
│ └─ PatternFormatValidator → paired syntax + actual-engine workers and exact tests
├─ Corpus EngineSupervisor → sequential bounded document jobs → summaries/outputs
├─ Test SyntaxSupervisor + EngineSupervisor → isolated cancellable test runs ├─ Test SyntaxSupervisor + EngineSupervisor → isolated cancellable test runs
└─ project validator / serializer / IndexedDB persistence └─ project validator / serializer / IndexedDB persistence
``` ```
@@ -19,7 +46,72 @@ provenance. React never imports or traverses regexpp's cyclic AST.
Every engine request carries a protocol version, monotonically increasing Every engine request carries a protocol version, monotonically increasing
request ID and worker generation. A supervisor allows one active request, request ID and worker generation. A supervisor allows one active request,
rejects stale responses, terminates on timeout/crash/cancel, and lazily creates rejects stale responses, terminates on timeout/crash/cancel, and lazily creates
a new generation. Timeout is a distinct error state. a new generation. Timeout is a distinct error state. A separate idempotent
load operation verifies a worker's runtime identity before execution; its
flavour-specific startup deadline is not charged to the 250 ms live or selected
manual execution deadline. A killed generation must load and verify again.
The Python worker imports only the self-hosted Pyodide loader and pack, then
evaluates the application-owned bounded bridge in CPython. The bridge returns
code-point ranges, which are retained as native values and converted only at
valid boundaries to editor UTF-16. The Java worker imports a verified TeaVM
ES2015 module. That module executes TeaVM's own `java.util.regex.Pattern` and
`Matcher` class-library implementation and returns native UTF-16 ranges; it is
not OpenJDK.
PCRE2 tracing is not an `EngineSupervisor` match or replacement operation. Its
own worker compiles with `PCRE2_AUTO_CALLOUT`, copies only complete fixed
records and bounded mark bytes, and stops natively at either trace cap.
Reported callout fields retain byte positions; movement classifications are a
separate derived UI layer. Normal execution, corpus, tests and benchmarks never
run through the trace operation.
Static analysis traverses the normalized ECMAScript tree on the UI side under
node/finding caps. Timing and generated-input growth probes use the independent
analysis worker. Timeout, cancellation or crash discards that worker; no trace
mode is benchmarked.
Comparison is a separate orchestration layer, not an `EngineSupervisor`
operation. It runs explicit ECMAScript and PCRE2 syntax/execution snapshots,
retains each engine identity and native range model, and aligns only normalized
editor ranges. Timeout, cancellation, compile rejection and truncation remain
distinct non-comparable outcomes. Shared patterns are sent unchanged;
per-flavour ports are user-owned variants.
The PCRE2 C generator is application-owned and never implements browser
execution. It validates one exact PCRE2 snapshot, emits user strings as
independent UTF-8 byte arrays, and states the host-level mappings that the
PCRE2 API cannot express directly. Its advertised C17 target is protected by
deterministic golden identities and an exact PCRE2 10.47 compile/execute gate.
Subject minimization is a main-thread bounded reducer whose expensive predicate
checks stay in independently terminable ECMAScript or PCRE2 workers. Fixed
pattern syntax is parsed once for capture metadata. Python and Java are blocked
at this feature boundary rather than routed to a different engine. The baseline
and every accepted candidate retain the exact target and engine identity.
Timeout, crash, cancellation, worker failure and incomplete output are separate
outcomes. Deterministic ddmin deletion is followed by a fixed-point local sweep
over Unicode-scalar deletion and lower-rank canonical replacement; only
completion of that sweep permits a transform-local minimality claim.
Generated-case synthesis and verification are separate trust boundaries. The
dedicated generation worker receives only a validated, complete ECMAScript
normalized tree plus explicit settings and seed. The main-thread orchestrator
then sends each bounded candidate to `EngineSupervisor`; only the selected real
adapter can establish a `should-match` or `should-not-match` label. Worker
timeout, crash, cancellation, compile rejection, wrong outcome and ordinary
failure stay distinct. Settings/results are ephemeral, while explicitly added
unit tests retain field-validated generator provenance.
Formatting is not an engine operation and never rewrites from heuristic text
alone. The ECMAScript formatter consumes an exact accepted regexpp snapshot and
changes only parser-reported literal/control token ranges. Its validator
reparses source and candidate separately, preserves capture shape, runs both in
separate actual-engine workers against the current replacement snapshot, then
replays every enabled test tied to the exact active pattern/version/flags/options.
Any difference, incomplete output, one-sided timeout, identity change or
inconclusive worker outcome blocks apply. The preview and observations are
ephemeral, and user confirmation is required even after all bounded gates pass.
Pattern and replacement syntax use independent supervisors. A pattern snapshot Pattern and replacement syntax use independent supervisors. A pattern snapshot
is bound to its exact pattern, ordered flags and revision; execution cannot use is bound to its exact pattern, ordered flags and revision; execution cannot use
@@ -32,10 +124,28 @@ over those actual matches, the complete replacement-token model and the bounded
output. They execute no replacement callback or user code and have separate output. They execute no replacement callback or user code and have separate
presentation and token-evaluation limits. presentation and token-evaluation limits.
Native ranges are retained in the engine's declared unit. Browser editor ranges Corpus documents are ephemeral React session state, not project state. A
are half-open UTF-16 code-unit ranges. Reusable converters cover UTF-8 byte and dedicated engine supervisor processes one document at a time and is terminated
Unicode code-point offsets for later flavours, including invalid-boundary on cancellation, timeout or workspace exit. Batch orchestration retains only
rejection and lone-surrogate detection. per-document summaries and, for apply runs, bounded outputs. It enforces
document-count, per-document byte, aggregate input byte, aggregate match,
aggregate output and batch wall-time limits. Exact-output export is unavailable
if any document is partial, failed, cancelled or not run.
Whole-document mode sends each document to the engine once. Independent-line
mode segments CRLF, CR, LF, U+2028 and U+2029 without discarding them, executes
each logical line as its own subject, and reattaches the exact original
separator during apply. This makes anchor and cross-line behavior deliberately
different rather than silently rewriting the pattern. Match DTOs are reduced to
bounded per-group participation counts and samples before the next document.
Native ranges are retained in the engine's declared unit. ECMAScript and Java
use UTF-16 code units. PCRE2 always uses UTF/UCP and exposes UTF-8 byte ranges;
its ABI copies scalar records and names out of WebAssembly before returning and
retains no code pointer. CPython `re` exposes Unicode code-point indices.
Browser editor ranges are half-open UTF-16 code-unit ranges. Reusable
converters cover UTF-8 byte and Unicode code-point offsets, including
invalid-boundary rejection and lone-surrogate detection.
The release boundary is `dist/` plus checked-in legal/source documents. The The release boundary is `dist/` plus checked-in legal/source documents. The
Portal consumes the resulting immutable ZIP and never imports React source. Portal consumes the resulting immutable ZIP and never imports React source.

View File

@@ -0,0 +1,124 @@
# ECMAScript/PCRE2 comparison and PCRE2 C generation
## Comparison contract
The comparison panel sends two explicit requests: one to the native browser
ECMAScript worker and one to the bundled PCRE2 10.47 worker. It does not
translate patterns, flags, options or replacement templates.
This is an explicit two-engine feature, not a generic active-flavour
comparison. Selecting Python or Java for the main workbench does not insert it
into either side or route it through ECMAScript/PCRE2.
Two pattern models are available:
- **Shared pattern** sends the same string unchanged to both engines.
- **Per-flavour variants** retains two explicit ports supplied by the user.
Flags are selected independently from each flavour's registered allowlist.
PCRE2 match, depth and heap bounds are part of its exact request. Both sides
share the selected subject, scan-all intent, host result bounds and wall-clock
worker timeout.
Each side retains:
- its syntax-provider request, acceptance, diagnostics and coverage;
- the exact execution or replacement request sent to its worker;
- a deterministic display key plus the complete authoritative request;
- completion, timeout, cancellation or worker-failure status;
- engine, runtime and adapter identity when the engine returns one;
- user and effective flags;
- native ranges and normalized editor UTF-16 ranges;
- compile acceptance, matches, captures and replacement output.
The orchestrator owns two syntax supervisors and the multi-engine execution
supervisor. The two engine calls run concurrently through independently
killable flavour workers. A side timeout kills only that worker. The shared
Cancel action terminates every active comparison worker. Comparison remains
outside `EngineSupervisor`, whose responsibility is one engine operation and
lifecycle boundary.
Match pairs are aligned by equal normalized UTF-16 ranges first. Unpaired
matches use an explicitly labelled ordinal fallback. Captures are aligned by
normalized range, then capture identity, then an explicitly labelled ordinal
fallback. Original native offsets remain attached to both results.
No equivalence claim is made when syntax or compilation is rejected, a worker
times out/fails/is cancelled, results or values are truncated, or replacement
output is incomplete. A completed matching result is labelled only as
agreement for the current subject; it is evidence, not a proof that two
patterns are equivalent. Timeout is never interpreted as “no match.”
The retained DTO is bounded to 5,000 displayed difference records and 2,000
match alignments while still counting all differences in the bounded engine
result. The panel renders at most 250 differences and 100 alignments at once.
## PCRE2 C17 generator
The first reviewed code-generation target is the PCRE2 10.47 8-bit C API.
Python, Java and other targets remain unavailable until their generators have
equivalent escaping, semantic and named-runtime/toolchain gates. The executable
Python and Java browser flavours do not by themselves imply a code-generation
target.
The generator consumes a validated PCRE2 snapshot and:
- requires the registered PCRE2 10.47 version and flag/option allowlists;
- refuses unpaired UTF-16 surrogates rather than emitting lossy UTF-8;
- emits pattern, subject and replacement as independent explicit UTF-8 byte
arrays, so quotes, backslashes, newlines, NUL, Unicode, dollar signs and
braces cannot escape into C source syntax;
- enables mandatory `PCRE2_UTF` and `PCRE2_UCP`;
- maps `i`, `m`, `s`, `x`, `U` and `J` to named PCRE2 compile constants;
- implements application-level `g`/scan-all behavior explicitly;
- handles the PCRE2 empty-match anchored retry before advancing one UTF-8
character;
- configures native match, depth and heap limits;
- enforces match/capture row caps in the generated match loop;
- bounds replacement output with a host buffer and withholds output when its
post-run result-row bound is exceeded;
- reports compile offsets and native API errors and cleans up all PCRE2 state;
- rejects compilation against a PCRE2 header other than 10.47 and verifies the
runtime version.
The generated program states limitations that cannot be mapped honestly:
- PCRE2 has no `g` compile flag;
- match/capture rows and replacement-output size are host-application bounds,
not PCRE2 matching options;
- replacement result-row limits can be checked only after
`pcre2_substitute()` has completed;
- PCRE2 exposes no native wall-clock timeout, so hard elapsed-time
cancellation belongs to the containing process;
- C API offsets are UTF-8 bytes, not browser editor UTF-16 units.
Generated snippets are examples only and are never used to implement the
browser runtime.
## Golden and toolchain gate
`scripts/pcre2-codegen-golden.test.ts` pins deterministic SHA-256 identities
for match and replacement fixtures:
```text
match 22b91d578a449b0ed5c30e6eadad30f19a2bcb26ac9db355c8ec7a69328675da
replacement ab90a0d9831ea7b7145aa0072ac352b42baf9a795319d81f531b0f341b04a636
```
The compile/execute gate was run against a host static build from official
signed tag `pcre2-10.47`, commit
`f454e231fe5006dd7ff8f4693fd2b8eb94333429`. Both generated fixtures compile
under C17 with `-Wall -Wextra -Werror`; the match fixture verifies native UTF-8
ranges and named captures, and the replacement fixture verifies exact Unicode
output and substitution count.
To repeat the gate with an exact local PCRE2 installation:
```sh
PCRE2_CODEGEN_PREFIX=/path/to/pcre2-10.47-prefix \
npm test -- scripts/pcre2-codegen-golden.test.ts
```
The prefix must provide `include/pcre2.h` and
`lib64/libpcre2-8.a`. A different static-library location can be supplied
through `PCRE2_CODEGEN_LIBRARY`.

View File

@@ -1,28 +1,165 @@
# Engine builds # Engine builds
Version 0.1.0 ships no WebAssembly engine. The production application uses the browser's native `RegExp` for ECMAScript
and bundles verified PCRE2, Python and Java runtime packs. Normal application
builds never download or compile an engine: reviewed files under
`public/engines/` are copied into `dist/` and verified there.
`npm run engines:build` verifies that this release has no external pack to ## Rebuilding PCRE2
build. After the production build, `npm run engines:verify` rejects any
undeclared engine asset and confirms the packaged ECMAScript-only notice.
Neither command accesses the network.
## PCRE2 feasibility record The reproducible build accepts only:
The next flavour was spiked outside the application repository against official - PCRE2 tag `pcre2-10.47`, signed tag object
PCRE2 10.47: `cd007b4466798f66d479d1442a407099e7c40050`, peeled commit
`f454e231fe5006dd7ff8f4693fd2b8eb94333429` and tree
`81a83a3552bd68d0ea7b7004f8bb6e7892f583ba`;
- Emscripten 6.0.4, compiler revision
`fe5be6afdff43ad58860d821fcc8572a23f92d19`, from emsdk commit
`224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f`;
- CMake 4.3.4 and Ninja 1.13.2;
- 8-bit PCRE2 with Unicode enabled and JIT, threads and filesystem disabled.
- signed tag object `cd007b4466798f66d479d1442a407099e7c40050`; The source and compiler checkouts must already exist locally and be completely
- peeled commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429`; clean. No command clones, downloads or updates them:
- licence `BSD-3-Clause WITH PCRE2-exception`;
- Emscripten 6.0.4;
- 8-bit library, Unicode enabled, JIT disabled.
Node and Chromium tests demonstrated version/config reporting, Unicode, ```sh
named groups, global substitution, automatic and explicit callouts, and npm run engines:pcre2:build -- \
match/depth/heap errors. This was a technical spike only. No spike source, --source-dir /absolute/path/to/pcre2 \
binary or PCRE2 source is included in v0.1.0. --emcc /absolute/path/to/emscripten/emcc
npm run engines:pcre2:verify
npm run engines:pcre2:install
```
Production PCRE2 support remains gated on a reviewed bridge, copied DTOs, Use `--force` only to replace the exact ignored `.engine-build/pcre2` output.
allocation cleanup, exact source build, UTF-8/UTF-16 mapping, trace/output caps, The explicit install command first re-verifies that pack and then atomically
worker recovery, deterministic assets, licences and browser conformance. replaces only `public/engines/pcre2`.
The builder verifies Git objects and critical source/compiler hashes,
sanitizes build flags, sets `SOURCE_DATE_EPOCH=1760997684`, builds serially and
emits exactly:
- `pcre2.mjs` and `pcre2.wasm`;
- deterministic `engine-metadata.json`;
- the exact upstream `LICENSE.txt`;
- `SHA256SUMS`.
The verifier checks the closed file set, metadata, source bridge hashes and all
checksums. It validates and instantiates WebAssembly, checks ABI/engine/config
identity, then executes native Unicode compile, bounded match and bounded
substitution smoke cases plus normal and cap-stopped automatic-callout traces.
The immutable signed-tag object is pinned. OpenPGP trust validation remains a
release-operator step because the offline builder does not install or trust a
key.
## Installing the Python pack
The Python pack is derived from the exact `pyodide@314.0.3` npm package locked
with integrity:
```text
sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==
```
That Pyodide release is tag/commit
`ac57031be7564f864d061cb37c5c152e59f83ad4`. Direct executable identity
verification reports CPython 3.14.2; the shipped `pyodide-lock.json`
independently reports `info.python` as 3.14.0, and the verifier records rather
than conflates those two upstream identities. The pack copies only the reviewed
loader, WebAssembly runtime, Python standard library and lock metadata required
for a local CPython `re` worker. It adds deterministic engine metadata, a
closed-file-set checksum manifest and exact Pyodide/CPython licence material.
The install operation reads from the already installed pinned npm package. It
does not contact a CDN or package index. It verifies source package identity,
file hashes, runtime metadata and licence inputs before atomically replacing
only `public/engines/python/`. The distribution verifier checks the same closed
set under `dist/`, validates WebAssembly magic and confirms that all runtime
references remain relative and self-hosted.
```sh
node scripts/python-engine-pack.mjs \
node_modules/pyodide \
.engine-build/python
node scripts/install-python-engine.mjs .engine-build/python
```
The installed closed set is:
- `pyodide.mjs`, `pyodide.asm.mjs` and `pyodide.asm.wasm`;
- `python_stdlib.zip` and `pyodide-lock.json`;
- `engine-metadata.json` and `SHA256SUMS`; and
- `LICENSE.pyodide.txt` and `LICENSE.cpython.txt`.
## Building the Java pack
The Java bridge under `engines/java/` wraps TeaVM 0.15.0
`java.util.regex.Pattern` and `Matcher`. TeaVM tag/commit
`ee91b03e616c4b45401cd11fb0cd7eb0daf6649b` is pinned through the Maven build.
The build produces a minified ES2015 module without source maps, source files,
remote imports or an OpenJDK runtime.
The pack gate verifies the Maven coordinates, bridge source identity, TeaVM
engine identity and self-test, generated module, deterministic metadata,
checksums, Apache-2.0 licence and TeaVM NOTICE. Only the verified pack is
installed under `public/engines/java/`.
```sh
node scripts/java-engine-pack.mjs
node scripts/install-java-engine.mjs
```
The pack command performs two clean offline Maven builds and requires identical
output. The installed closed set is `java-regex.mjs`,
`engine-metadata.json`, `SHA256SUMS`, `LICENSE.txt` and `NOTICE.txt`.
TeaVM supplies its own Apache Harmony-derived class-library implementation.
The resulting engine is therefore identified as
`TeaVM java.util.regex 0.15.0`, not OpenJDK. TeaVM 0.15.0 does not implement
OpenJDK's `Pattern.UNICODE_CHARACTER_CLASS`; the bridge treats the application
`U` flag as a compatibility request, implies Unicode case folding and reports
that predefined-character classes and word boundaries retain TeaVM behaviour.
## Runtime boundaries
### PCRE2
ABI version 3 has no retained native handles. Each call compiles, obtains
capture names, matches/substitutes or traces and releases its code, match data
and match context before returning. The caller supplies fixed-capacity
result/name/output/event/mark buffers. Hard maxima are 1 MiB pattern, 16 MiB
subject, 1,000 captures, 10,000 matches, 100,000 capture rows, 50,000 trace
events, 10 MiB serialized trace data and 128 MiB PCRE2 heap limit; the
workbench uses stricter pattern and replacement input limits where applicable.
PCRE2 always runs with UTF and UCP. Native UTF-8 byte offsets are retained in
DTOs and converted only at valid code-point boundaries to half-open editor
UTF-16 ranges. Lone UTF-16 surrogates are rejected before encoding.
Normal execution lives only in `pcre2.worker`. Instrumented
`PCRE2_AUTO_CALLOUT` collection is a separate ABI operation loaded only by
`pcre2-trace.worker`; normal match, replacement, tests, corpus and benchmarks
never call it. Each supervisor terminates its worker on timeout, crash,
cancellation or supersession and lazily creates a clean generation for the
next request.
### Python
The Python worker loads the self-hosted Pyodide pack and verifies Pyodide,
CPython and `re` identities before accepting work. The application bridge is
evaluated inside that interpreter and exposes bounded compile, match and native
replacement operations. It reports CPython's Unicode code-point positions;
the TypeScript adapter retains them and converts only valid boundaries to
half-open editor UTF-16 ranges. Runtime load has a separate startup deadline.
Execution timeout, cancellation or crash terminates the worker and its complete
Python/WebAssembly heap.
### Java
The Java worker imports the self-hosted TeaVM ES module and checks the bridge
ABI, declared engine identity and compiled self-test before accepting work.
Every request compiles a `Pattern`, obtains bounded `Matcher` results or native
replacement output, and returns native half-open UTF-16 ranges. No Java virtual
machine, OpenJDK classes, remote service or ECMAScript regex fallback is
involved. Runtime load has a separate startup deadline; termination discards
the complete worker module state.

View File

@@ -1,13 +1,43 @@
# Execution traces # Execution traces
Version 0.1.0 exposes no execution trace. Browser ECMAScript APIs expose compilation, match, capture and replacement
results, but no V8, SpiderMonkey or JavaScriptCore execution trace. Regex Tools
does not relabel structural explanations, static findings or timing
observations as an ECMAScript engine trace.
Browser ECMAScript APIs return compilation, match, capture and replacement The bundled CPython `re` and TeaVM `java.util.regex` bridges likewise expose
results but not the runtime's internal backtracking events. Regex Tools does compilation, bounded match/capture and replacement results, not internal engine
not label structural explanations, static hints or adjacent-result inference steps. Regex Tools does not label their partial lexical syntax trees as traces.
as an actual engine trace.
The next PCRE2 slice will use actual automatic/explicit callout events from an PCRE2 10.47 has a separate actual automatic-callout vertical. ABI version 3
application-owned bridge. Event count, serialized bytes, match/depth/heap compiles the requested pattern with `PCRE2_AUTO_CALLOUT`, registers an
limits and wall-clock termination must all be enforced. Classifications such application callback, and copies only:
as “apparent backtrack” will remain visibly derived from adjacent events.
- callout number;
- pattern byte position and next-item byte length;
- subject byte position;
- capture-top and capture-last numbers;
- bounded current mark text.
These fields have `reported` provenance. Pattern and subject byte boundaries
are validated and normalized to exact editor UTF-16 positions while the native
values remain visible.
Collection runs in `pcre2-trace.worker`, not the normal execution worker. It
uses the selected PCRE2 match, depth and heap limits plus the supervisor wall
clock. The callback retains only complete 32-byte records and mark slices under
both a 50,000-event and 10 MiB serialized-data hard cap. At a cap it returns
PCRE2s reserved `PCRE2_ERROR_CALLOUT`, abandoning the native match; the result
retains that native status, the last complete event and an explicit truncation
flag. Timeout, cancellation or crash discards the complete worker.
The viewer derives “forward”, “same position” and “apparent backtrack” solely
from the difference between adjacent reported subject positions. Each label
has `derived` provenance. This is useful navigation, not a claim that the
stream reports every internal action. PCRE2 start optimizations can also
conclude some requests without any callout.
Tracing represents one exact `pcre2_match()` invocation. The application-level
`g` iteration flag is not applied, and that fact is reported. Normal match,
replacement, tests, corpus, comparisons and benchmarks never use the
instrumented trace path.

View File

@@ -1,9 +1,9 @@
# Explanation model # Explanation model
The community provider converts regexpp nodes into a stable application AST The ECMAScript community provider converts regexpp nodes into a stable
inside the syntax worker. Nodes carry deterministic IDs, UTF-16 ranges, raw application AST inside the syntax worker. Nodes carry deterministic IDs,
source, capture metadata, quantifier bounds, assertion kind, support status, UTF-16 ranges, raw source, capture metadata, quantifier bounds, assertion kind,
provider identity and provenance. support status, provider identity and provenance.
Explanations are original deterministic text selected by node type. Nullable Explanations are original deterministic text selected by node type. Nullable
and minimum/maximum consumed-length properties are derived recursively where and minimum/maximum consumed-length properties are derived recursively where
@@ -15,6 +15,21 @@ regexpp 4.12.2 does not expose tolerant recovery. A malformed pattern therefore
gets the provider's exact diagnostic plus an application-owned error node. No gets the provider's exact diagnostic plus an application-owned error node. No
partial regexpp AST is claimed. partial regexpp AST is claimed.
The PCRE2 provider currently emits a deliberately partial lexical tree for
common capture forms. It does not infer branch-reset numbering or claim full
nesting; authoritative compilation, capture counts, names and result ranges
come from PCRE2 itself.
The Python and Java providers likewise emit deliberately partial lexical trees.
They recognize their documented capture and replacement forms for navigation
and display, but do not claim full grammar acceptance. CPython `re` and TeaVM
`Pattern` remain authoritative for compilation. Java capture names displayed
with native numbered spans come from the bounded lexical metadata; they are not
invented as a TeaVM-reported name table.
The explanation tree is structural. It is never called an actual execution The explanation tree is structural. It is never called an actual execution
trace. Selecting a node selects and scrolls its exact editor range; selecting trace. Selecting a node selects and scrolls its exact editor range; selecting
pattern text chooses the smallest enclosing node. pattern text chooses the smallest enclosing node. PCRE2s separate trace viewer
contains actual bounded automatic callouts; its movement labels remain a
distinct derived layer and are never copied into the structural explanation.
Python and Java expose no actual execution trace.

View File

@@ -18,10 +18,14 @@ budget. Consumers must inspect the preview status rather than treating a
preview as complete. In particular, list export is disabled if its template preview as complete. In particular, list export is disabled if its template
would consume an incomplete value. would consume an incomplete value.
Repeated ECMAScript captures expose only the final retained capture. The UI Repeated ECMAScript, PCRE2, Python and Java captures expose only the final
labels this limitation and does not invent history. Later .NET support may add retained capture. The UI labels this limitation and does not invent history.
actual engine capture-history nodes without degrading them to this model. Later .NET support may add actual engine capture-history nodes without
degrading them to this model.
The syntactic hierarchy remains useful even if actual spans overlap or fall The syntactic hierarchy remains useful even if actual spans overlap or fall
outside a parent's geometry; native and normalized ranges are preserved rather outside a parent's geometry; native and normalized ranges are preserved rather
than “fixed”. than “fixed”. ECMAScript and Java native offsets are UTF-16 code units, PCRE2
native offsets are UTF-8 bytes and Python native offsets are Unicode code
points. All editor ranges are normalized to UTF-16 without discarding the
native unit.

View File

@@ -1,20 +1,75 @@
# Flavour support # Flavour support
| Flavour | Syntax | Execution | Replacement | Captures | Trace | Native offsets | | Flavour | Syntax | Execution | Replacement | Captures | Trace | Analysis | Generated cases | Native offsets |
| ------------ | ------------------------------------------------------------------ | ----------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- | ----------- | ------------------- | | ---------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------- | -------------- |
| ECMAScript | regexpp 4.12.2 grammar; explanations partial/feature-matrix tested | Current browser `RegExp`; feature-matrix tested | Native matches; bounded ECMAScript `GetSubstitution` | Named/numbered; final repeated capture; no history | Unavailable | UTF-16 | | ECMAScript | regexpp 4.12.2; explanations partial/feature-matrix tested | Current browser `RegExp`; conformance tested | Bounded ECMAScript `GetSubstitution` | Named/numbered; final repeated capture; no history | Unavailable | Experimental static heuristics + bounded native probes | Bounded normalized-AST sampling; actual-engine verified | UTF-16 |
| PCRE2 | Unavailable in release | Technical spike only; not shipped | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | | PCRE2 10.47 | Application lexical provider partial; compiler authoritative | Pinned 8-bit WASM; bounded/killable/conformance tested | Native bounded `pcre2_substitute()` loop | Native named/numbered records; no capture history | Bounded reported automatic callouts; derived movement labels | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | UTF-8 bytes |
| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | | Python `re` | Application lexical provider partial; compiler authoritative | CPython 3.14.2 in self-hosted Pyodide 314.0.3 | Native bounded `re` expansion | Native named/numbered spans; no capture history | Unavailable | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | Code points |
| Python `re` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned code points | | Java `java.util.regex` | Application lexical provider partial; compiler authoritative | TeaVM 0.15.0 class library; bounded/killable; not OpenJDK | TeaVM single-digit `$n`; no `${name}` | Native numbered ranges; lexical names; no history | Unavailable | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | UTF-16 |
| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | | PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable |
| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | | Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned bytes |
| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Planned UTF-16 | | Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned bytes |
| Java | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-16 | | .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Unavailable | Unavailable | Planned UTF-16 |
ECMAScript tests cover `g`, `y`, `u`, internal `d`, zero-length iteration, ECMAScript tests cover its complete exposed flag matrix, internal indices,
named groups, unmatched/empty/repeated groups, lookbehind, backreferences, zero-length iteration, captures, lookbehind, backreferences, Unicode properties
Unicode properties, astral input, replacement and result truncation. and bounded replacement.
Actual browser identity is displayed because ECMAScript behavior can evolve PCRE2 tests cover `g`, `i`, `m`, `s`, `x`, `U`, `J`, mandatory UTF/UCP,
with the runtime. Syntax acceptance and engine compilation are separate; branch-reset groups, recursion, `\K`, duplicate names, native substitution,
neither silently rewrites the pattern. astral byte/UTF-16 ranges, zero-length iteration, compile errors, match limits
and result/output truncation. Trace tests cover reported automatic callouts,
bounded mark copying, native stop at the complete-event cap, compile ranges,
worker loading and browser rendering.
Python tests cover application-level `g`, CPython flags `a`, `i`, `m`, `s` and
`x`, Python-only named-group/replacement syntax, native `re` substitution,
astral code-point/UTF-16 normalization, zero-length iteration, compile errors,
result limits, runtime identity and worker recovery.
Java tests cover application-level `g`, TeaVM `Pattern` flags, Java named-group
syntax, TeaVM's native single-digit `$n` `Matcher` replacement, explicit
rejection of unsupported `${name}`, astral UTF-16 ranges, zero-length
iteration, compile errors, result limits, module identity and worker recovery.
The runtime and UI explicitly report that TeaVM 0.15.0 is not OpenJDK and does
not implement OpenJDK `Pattern.UNICODE_CHARACTER_CLASS`; `U` is a compatibility
request rather than an exact implementation of that flag.
ECMAScript static findings and generated-input timing observations are
advisory. They describe only the selected ECMAScript 2025 pattern, browser
runtime and bounded inputs; they are not proofs of safety or general
complexity.
ECMAScript generated-case version 1 uses only a complete accepted normalized
AST, an explicit deterministic seed and bounded sampling strategies. Every
retained positive or negative label is confirmed by the actual selected
browser engine. PCRE2, Python and Java generation are unavailable because
their current lexical providers are intentionally partial; ECMAScript behavior
is never relabelled as another flavour.
The PCRE2, Python and Java explanation providers deliberately do not invent
full structural coverage. PCRE2 branch-reset numbering is omitted from the
syntax tree, while actual capture numbers, names and ranges still come from the
selected runtime result.
ECMAScript and PCRE2 support exact-request comparison for syntax acceptance,
engine compilation, matches, captures and engine-native replacement. Results
align by normalized UTF-16 ranges while retaining each native offset model.
Timeout, cancellation, rejection and truncation are never presented as
equivalent results.
Pattern formatting is available only for an exact accepted ECMAScript regexpp
snapshot. It canonicalizes literal slashes and raw control/line-separator
representations, then requires paired actual-engine and applicable exact-test
validation before apply. PCRE2, Python and Java formatting is unavailable
because those providers are deliberately partial; their syntax is never
treated as ECMAScript.
Subject minimization accepts only the verified ECMAScript and PCRE2 paths.
Python and Java selections are disabled rather than being minimized through a
different flavour. Semantic comparison is likewise an explicit
ECMAScript/PCRE2 operation; it is not a generic four-way equivalence checker.
PCRE2 is the only current code-generation target. The reviewed C17 generator is
pinned to the PCRE2 10.47 8-bit API; other language generators remain
unavailable until they pass equivalent escaping and named-toolchain gates.

58
docs/FORMATTING.md Normal file
View File

@@ -0,0 +1,58 @@
# Pattern formatting
Regex Tools includes a deliberately narrow, local ECMAScript formatter. It
canonicalizes representations that can be changed without inventing a visual
layout:
- an unescaped literal `/` becomes `\/`, making the pattern safe to copy into
an ECMAScript regex literal;
- raw NUL and control code units become fixed-width `\xNN` or their canonical
short escape (`\t`, `\n`, `\v`, `\f`, `\r`);
- raw U+2028 and U+2029 line separators become `\u2028` and `\u2029`;
- legacy escaped raw controls are replaced as one parser-reported literal
token, so the formatter does not introduce an extra backslash.
Existing explicit escape spellings, groups, alternatives, quantifiers, inline
flags and literal whitespace are otherwise preserved byte for byte. ECMAScript
regex whitespace is normally significant, so the tool does not insert
indentation, line breaks or comments and does not claim to be a structural
pretty-printer. The transform is idempotent.
## Eligibility
Formatting requires the exact current, accepted, non-recovered syntax snapshot
from the bundled `@eslint-community/regexpp` provider. Transformations are
derived from the provider's literal-token ranges rather than from a heuristic
text scan. A stale parse, a malformed pattern, a partial provider, PCRE2,
Python, Java or any other flavour is reported as unavailable and is never
silently rewritten.
## Mandatory validation before apply
The preview cannot be applied directly. Regex Tools first:
1. reparses source and candidate independently and checks capture numbering,
names, repetition and parent-capture shape;
2. compiles and runs the source and candidate in separate actual ECMAScript
engine workers against the current subject and replacement;
3. compares exact normalized match, capture and replacement output, including
the engine identity;
4. reruns every enabled unit test whose flavour, version, pattern, flags and
engine options exactly match the active source snapshot, and compares both
semantic output and assertion outcome.
A compile rejection, one-sided timeout, crash, worker error, cancellation,
truncated result, engine-identity change, incomplete test suite or semantic
difference blocks application. Matching fixed timeouts are comparable only for
an exact unit-test pair; two timeouts on the current interactive snapshot are
inconclusive. If no unit test is applicable, the UI says so and still requires
the current subject/replacement gate.
Validation is bounded to 1,000 tests, the normal worker/input limits and a
60-second aggregate test wall budget. A test is not started unless its complete
configured timeout still fits that budget. The user must explicitly confirm
the exact validated candidate before the Apply button is enabled.
Passing these gates is strong, reproducible evidence for the current snapshot
and exact tests. It is not a mathematical proof of equivalence for every
possible subject.

138
docs/GENERATED_CASES.md Normal file
View File

@@ -0,0 +1,138 @@
# Generated test cases
Regex Tools generator version 1 creates deterministic candidate subjects from
the application-owned normalized AST and then verifies every candidate through
the selected actual engine adapter. Generation is local-only and bounded. It is
sampling, not proof of language coverage, equivalence or safety.
## Supported scope
Version 1 is scoped to an accepted ECMAScript 2025 normalized AST from the
bundled regexpp-based provider. It requests:
- a shortest structural candidate using the shortest visible alternative and
each quantifier minimum;
- each structurally visible alternative;
- quantifier minimum, adjacent and bounded upper values;
- optional-absent and optional-present values;
- bounded character-class, dot and Unicode-property representatives;
- newline and non-boundary context around anchors;
- deletion, replacement and boundary mutations as likely near misses;
- deterministic random alternative, quantifier and class choices.
The coverage report says `covered`, `partial`, `unsupported` or
`not applicable` for each category. “Covered” means that the documented bounded
sampling strategy was requested. It does not mean every string in the regular
language was enumerated.
PCRE2, Python and Java generation is unavailable in version 1. Their actual
engines are complete for the advertised execution operations, but their
current structural syntax providers are intentionally partial. Regex Tools
does not feed their syntax to the ECMAScript generator or relabel ECMAScript
behaviour.
## Unsupported and partial constructs
The report includes exact normalized source ranges and a reason for every
recognized gap, capped at 250 rendered entries. Version 1 does not solve:
- capture-dependent backreferences;
- lookaround or word-boundary constraints symbolically;
- recursion, subroutine calls or conditional execution;
- branch-reset and atomic grouping;
- engine control verbs or callouts;
- complex Unicode-set algebra and properties of strings;
- recovered or provider-unsupported syntax.
Surrounding candidates may still happen to satisfy a lookaround or boundary.
They are retained only when the actual selected engine confirms the requested
match outcome. That confirmation does not upgrade the generators structural
coverage claim.
## Determinism and seed
The seed is explicit text of 1128 UTF-16 units. Generator version 1 hashes its
UTF-16 code units with a documented stable 32-bit FNV-1a step and drives an
application-owned 32-bit deterministic sequence. It never calls
`Math.random()`.
The same generator version, normalized AST, flags, settings and seed produce
the same candidate order and candidate identifiers. Verification timing is
environment-dependent, and strict wall-time or engine-timeout bounds can
therefore stop two runs at different points. The UI records both the original
seed and its eight-digit hash. Generated unit tests retain:
- generator id and version;
- the original seed;
- candidate id;
- intended positive or negative outcome.
That provenance survives project and test-suite JSON export when subjects are
included. Editing a generated test turns it into a user-authored test and
removes the generation provenance.
## Actual-engine verification
Candidate synthesis runs in a dedicated killable worker. The verifier then
sends each exact candidate, pattern, flavour version, flags, engine options and
scan mode through `EngineSupervisor`, which selects the registered real engine
worker. Version 1's eligibility gate accepts only the exact current ECMAScript
snapshot; the generic supervisor boundary does not make partial-provider
flavours eligible.
- An intended positive is labelled `should-match` only after the engine returns
at least one match.
- An intended negative is labelled `should-not-match` only after the engine
returns no match.
- A candidate with the opposite result is discarded and retained only as a
bounded advanced diagnostic.
- Engine compile rejection, timeout, worker crash, cancellation and ordinary
execution error remain distinct outcomes.
- Cancellation terminates both candidate-synthesis and active engine workers.
- Configuration changes invalidate results immediately; old cases are never
displayed against a new pattern or engine configuration.
The engine name, version, adapter, runtime and native offset unit shown with the
result come from the actual execution result, not static marketing metadata.
## Bounds
Defaults:
| Bound | Default |
| -------------------------------------- | ------------------------: |
| Retained verified cases | 48 |
| Candidate attempts | 192 |
| Seeded random variants | 16 |
| Per-case engine timeout | 500 ms |
| Aggregate wall time | 15 s |
| Bytes per candidate subject | 64 KiB |
| Aggregate candidate subject bytes | 1 MiB |
| Synthesized repetitions per quantifier | 32 |
| Normalized AST traversal | 20,000 nodes / 512 levels |
Hard caps:
| Bound | Hard cap |
| -------------------------------------- | -------: |
| Retained verified cases | 1,000 |
| Candidate attempts | 4,000 |
| Per-case engine timeout | 10 s |
| Aggregate wall time | 30 s |
| Bytes per candidate subject | 16 MiB |
| Aggregate candidate subject bytes | 16 MiB |
| Synthesized repetitions per quantifier | 1,024 |
| Advanced discard diagnostics | 250 |
UTF-8 sizes are checked before a candidate is retained for verification.
Quantifier expansion is preflighted before `String.prototype.repeat` allocates
the output. Reaching a case, attempt, byte, AST, repetition or wall-time bound
is visible in the result; it is never reported as complete coverage.
## Privacy and persistence
No pattern, candidate, verified subject, diagnostic or seed is uploaded.
Results and settings are ephemeral until the user explicitly adds cases to the
unit-test suite or downloads the verified JSON artifact. Test-suite export
omits subjects by default because generated subjects may still encode literal
pattern content. Enabling subject export is an explicit user choice.

75
docs/MINIMIZATION.md Normal file
View File

@@ -0,0 +1,75 @@
# Subject minimization
The **Minimize** workspace reduces one failing subject locally against the
selected real ECMAScript or PCRE2 worker. It can preserve:
- the exact failure class of any supported saved unit-test assertion;
- an identified capture's wrong participation, value or UTF-16 range;
- an engine worker timeout at one fixed deadline;
- an ECMAScript/PCRE2 semantic mismatch with the same mismatch-kind set; or
- the same one-sided comparison timeout while the other engine completes with
authoritative, untruncated output.
Python and Java are not supported minimization targets in v0.3.0. The workspace
disables reduction for those active flavours; it does not substitute
ECMAScript, PCRE2 or a lexical provider as an execution oracle.
The fixed pattern, ordered flags, options, replacement and engine versions form
the target identity. Syntax is parsed once to obtain capture metadata. Every
accepted subject candidate is then verified through the selected execution
worker; the reducer does not contain a substitute regex implementation.
## Deterministic transforms
Subjects must be well-formed Unicode. The reducer never splits a surrogate
pair and rejects input containing a lone surrogate.
Reduction order is stable:
1. contiguous ddmin chunk deletion, left to right;
2. single Unicode-scalar deletion, left to right; then
3. lower-rank replacement by `a`, `0`, space, newline and `!`, in that order.
The last two passes restart after every accepted change and continue to a fixed
point. A complete pass with no accepted candidate establishes only **local
minimality under those transforms**. It is not a proof of the shortest,
globally minimal or semantically simplest reproducer. Evaluation or wall-budget
exhaustion, an inconclusive candidate, cancellation and worker failure always
return a result without a minimality claim.
## Exact predicates
A baseline run must reproduce the requested failure before reduction starts.
Unit-test failures retain their assertion class; a wrong capture value cannot
turn into a missing capture, and a semantic comparison retains the exact sorted
set of baseline mismatch kinds. Rejected patterns and truncated match,
capture or replacement data are not accepted as semantic evidence.
Timeout is a worker deadline outcome, never a no-match. An explicit timeout
predicate accepts only `WorkerRequestError("timeout")` at the configured fixed
deadline. Comparison timeout reduction additionally requires the same flavour
to time out and the peer flavour to complete with the same engine identity and
complete data. Crash, unreadable worker response, ordinary worker error and
cancellation have separate statuses. Crash minimization is unsupported and a
crash never satisfies a timeout predicate.
For a `must-time-out` unit-test assertion, an ordinary completion is the test
failure being minimized; a timeout makes that assertion pass. The separate
**Exact engine timeout** target is used to minimize an observed timeout.
## Bounds and progress
- Subject: 1 MiB UTF-8, separate from the larger interactive editor cap.
- Candidate evaluations: at most 2,000, including the baseline.
- Aggregate syntax-setup and reduction wall time: at most 60 seconds.
- Candidate worker deadline: fixed for the complete run, at most 10 seconds.
- Retained accepted-transform history: 200 entries; totals remain exact.
A candidate starts only when its full fixed deadline fits inside the remaining
aggregate wall budget. This prevents the aggregate deadline from being
misreported as an engine timeout. Progress exposes phase, evaluation and wall
budgets, accepted reductions, current scalar/byte sizes and the latest
observation. Cancellation terminates syntax and execution workers.
The minimized subject can be copied or deliberately applied to the main editor.
Inputs and results stay in memory and are neither uploaded nor persisted.

View File

@@ -7,10 +7,19 @@ community
``` ```
It uses only reviewed open-source dependencies and builds without private It uses only reviewed open-source dependencies and builds without private
registry credentials. Version 0.1.0 parses ECMAScript with regexpp 4.12.2. registry credentials. ECMAScript uses regexpp 4.12.2. PCRE2 uses an
application-owned lexical provider for partial explanations and the actual
bundled PCRE2 compiler for authoritative acceptance. Python and Java use
application-owned lexical providers for partial explanations and
flavour-native replacement tokens; bundled CPython `re` and TeaVM
`java.util.regex` respectively remain authoritative for compilation.
The Java provider follows the bundled TeaVM 0.15.0 replacement subset:
single-digit `$n` is executable, `$12` is `$1` followed by literal `2`, and
Java SE `${name}` references are surfaced as a compatibility limitation.
The product deliberately does not implement or reference an `@r101/parser` The product deliberately does not implement or reference an `@r101/parser`
profile. No commercial package alias, import, tarball, credential, licence key profile. No commercial package alias, import, tarball, credential, licence key
or private CI path exists. Additional flavour syntax will be implemented or private CI path exists. Additional flavour syntax is implemented
incrementally as open-source providers behind the same application-owned incrementally as open-source providers behind the same application-owned
interface. interface. Partial coverage is exposed as metadata and never presented as a
complete parser.

66
docs/PCRE2_NEXT_SLICES.md Normal file
View File

@@ -0,0 +1,66 @@
# PCRE2 advanced slices
This document records the reviewed boundaries for advanced PCRE2 work. A
feature is exposed only after its complete runtime and UI path passes the
stated gates.
## Automatic-callout trace — implemented
ABI version 3 and the dedicated trace worker now provide:
- a new ABI operation separate from normal matching, compiled with
`PCRE2_AUTO_CALLOUT`;
- a fixed copied event record containing only reported callout number, pattern
byte position, next-item length, subject byte position, capture top/last and
bounded mark text;
- caller-supplied event and text buffers capped by both
`maximumTraceEvents` and `maximumTraceBytes`;
- explicit `eventsTruncated`, native status and last-complete-event fields;
- the existing match/depth/heap limits and supervisor timeout;
- cleanup identical to normal ABI calls.
Tracing runs in its own worker and never replaces the normal result used by
Match, Replace, Tests, Corpus or Analysis. “Forward”, “same position” and
“apparent backtrack” are derived from adjacent reported positions only and
carry derived provenance. The viewer explicitly states that a callout stream
is not a complete account of every internal engine action.
## ECMAScript versus PCRE2 comparison — implemented
The comparison orchestrator issues two explicit requests with
`Promise.allSettled` through independently killable flavour workers. It does
not translate flags, patterns or replacement syntax.
The comparison DTO retains:
- each exact request identity, engine identity and independently complete or
timed-out result;
- match/capture differences aligned by editor UTF-16 ranges, while retaining
each native offset model;
- explicit “not comparable” states for syntax/compile failure, truncation,
timeout, cancellation, worker failure or flavour-only constructs;
- a shared wall-clock cancellation action that terminates both workers.
The orchestrator remains outside `EngineSupervisor`, preserving the
single-engine lifecycle boundary. Shared patterns are sent unchanged and
explicit per-flavour variants remain user-owned. Agreement is labelled only as
evidence for the current subject.
## PCRE2 C code generation — implemented
The first reviewed target consumes a validated request snapshot and uses an
application-owned C17 template. It:
- identifies exact PCRE2 10.47, 8-bit, UTF/UCP semantics and selected flags;
- emits pattern, subject and replacement independently as exact UTF-8 byte
arrays;
- includes match/depth/heap limits and error handling in generated C examples;
- states when a target binding cannot express an application-level `g` flag or
a Regex Tools output/result cap directly;
- ships deterministic golden match/replacement fixtures that compile and
execute against official PCRE2 10.47 before the target is advertised.
No generated snippet may be used as the implementation of the browser runtime.
Other target languages remain unavailable until their own escaping and
toolchain gates pass. See
[`COMPARISON_AND_CODEGEN.md`](COMPARISON_AND_CODEGEN.md).

View File

@@ -10,6 +10,28 @@ browser, engine version, pattern and subject. The UI therefore reports observed
elapsed time and exact runtime identity but makes no universal throughput or elapsed time and exact runtime identity but makes no universal throughput or
safety claim. safety claim.
PCRE2 cold worker startup includes loading and instantiating the self-hosted
WebAssembly module; warm requests reuse that instance.
Every request still compiles and frees its pattern so no unbounded compiled-code
cache can accumulate. Native match-step, depth and heap limits complement the
supervisor wall clock, and cancellation discards the complete worker heap.
Python has a materially larger cold start because its worker loads the
self-hosted Pyodide JavaScript/WebAssembly runtime and Python standard library
before verifying CPython and `re`. Java imports and self-tests its bundled
TeaVM ES module. Engine loading is an explicit operation with a
flavour-specific startup deadline; it is not charged to the 250 ms live or
selected manual execution timeout. Warm requests reuse the verified worker.
Killing a worker for timeout, crash or cancellation discards its complete
runtime, so the next request pays the cold-load cost and verifies identity
again.
Automatic-callout tracing is deliberately excluded from every benchmark. It
compiles an independently instrumented PCRE2 pattern in a separate worker and
can materially change runtime work. The trace path retains at most 50,000
complete 32-byte event records and 10 MiB total serialized event/mark bytes;
the viewer renders at most 2,000 retained events.
The production bundle separates syntax and execution workers. Interactive The production bundle separates syntax and execution workers. Interactive
syntax/extraction trees and editor decoration sets render at most 2,000 items syntax/extraction trees and editor decoration sets render at most 2,000 items
each and report both the rendered and actual totals. These are presentation each and report both the rendered and actual totals. These are presentation
@@ -32,12 +54,12 @@ pages of 200. Individual result-value previews are capped at 64 Ki UTF-16 units
and share an 8 Mi-unit aggregate engine-result budget. The extraction tree and share an 8 Mi-unit aggregate engine-result budget. The extraction tree
shows 100 UTF-16 units per value and capture-table cells show 512, with explicit shows 100 UTF-16 units per value and capture-table cells show 512, with explicit
range lengths and separate engine-prefix labels; copy and eligible JSON export range lengths and separate engine-prefix labels; copy and eligible JSON export
retain the bounded engine value. List export is disabled when a source value is incomplete, so a bounded retain the bounded engine value. List export is disabled when a source value is
preview cannot be mistaken for the full value. List generation additionally incomplete, so a bounded preview cannot be mistaken for the full value. List
stops at 250,000 template-token evaluations, a 64 Ki UTF-16 row preview, or an generation additionally stops at 250,000 template-token evaluations, a 64 Ki
8 Mi-unit aggregate materialization bound. The UI shows 500 list rows while an UTF-16 row preview, or an 8 Mi-unit aggregate materialization bound. The UI
eligible export still contains every generated row; any incomplete result shows 500 list rows while an eligible export still contains every generated
disables export. row; any incomplete result disables export.
Replacement output has a separate 64 MiB bound. Match/capture-limit Replacement output has a separate 64 MiB bound. Match/capture-limit
incompleteness and output-byte truncation are tracked separately; either makes incompleteness and output-byte truncation are tracked separately; either makes
the aggregate replacement result incomplete. the aggregate replacement result incomplete.
@@ -62,5 +84,57 @@ representation against a byte budget before materializing or writing it.
IndexedDB schema version 2 indexes `updatedAt`; latest-project loading opens one IndexedDB schema version 2 indexes `updatedAt`; latest-project loading opens one
descending cursor and validates only that record instead of calling `getAll()`. descending cursor and validates only that record instead of calling `getAll()`.
Formal cold/warm benchmarks, p95 statistics and growth charts are deferred to Corpus runs accept at most 256 documents, 16 MiB per document and 256 MiB of
the analysis milestone and will run in killable workers. aggregate UTF-8 input. Jobs run sequentially so only one engine request is
active at a time. A batch retains at most 100,000 matches and 64 MiB of applied
output, and stops after five minutes of aggregate wall time. Each document also
uses the selected manual timeout. Progress is reported after every document;
cancellation kills the current worker and labels all remaining documents.
Result tables contain summaries, not match/capture objects. ZIP export is
available only for an all-exact apply batch and is created asynchronously.
Independent-line mode preflights at most 100,000 logical lines and processes
each as an isolated engine subject while preserving line separators for exact
apply output. Per document, corpus extraction retains at most 32 group summaries
(including the full match), three 160-UTF-16-unit samples per group and 100
unique diagnostics. Output previews show 2,000 units. Content-free summary
exports are capped at 4 MiB; the applied archive is capped at 68 MiB in addition
to the 64 MiB uncompressed-output budget.
ECMAScript analysis reports cold compilation separately from warm native
execution. The default benchmark uses three warm-up and 15 measured samples;
user caps are 100 warm-ups and 1,000 samples. It reports median and
nearest-rank p95 values plus separate timing and outcome/growth plots with the
exact runtime identity. Each sample defaults to a 2-second killable-worker
limit (10-second cap), and the aggregate analysis wall time is 30 seconds.
Growth probes retain at most 24 steps, generate at most 1 MiB by default under
the central 16 MiB subject cap, and reject more than 10,000,000 repetitions
before string allocation. Replacement timing is skipped when match collection
is truncated or the estimated output exceeds 8 Mi UTF-16 units. Results are
observations for the chosen runtime and generated inputs, never a general
complexity proof. See [`ANALYSIS.md`](ANALYSIS.md).
Generated-case synthesis defaults to 48 retained cases from at most 192
candidates, 16 seeded-random variants, 64 KiB per candidate, 1 MiB aggregate
candidate bytes and 15 seconds of aggregate wall time. Every candidate then
uses a separate selected-engine request with a 500 ms default timeout.
Generation preflights UTF-8 size and repetition bounds, caps normalized-AST
traversal at 20,000 nodes / 512 levels and retains only bounded diagnostics.
Hard caps are documented in [`GENERATED_CASES.md`](GENERATED_CASES.md).
Subject minimization accepts at most 1 MiB UTF-8, performs at most 2,000 real
engine candidate evaluations and stops after at most 60 seconds including
syntax setup. One fixed worker timeout of at most 10 seconds applies to every
candidate. A candidate starts only when that complete deadline fits in the
remaining aggregate budget. Accepted-transform history retains 200 entries;
evaluation and accepted totals remain exact. See
[`MINIMIZATION.md`](MINIMIZATION.md).
Formatter validation uses two syntax supervisors and two actual-engine
supervisors so source/candidate outcomes cannot share compiled state. The
current subject/replacement snapshot always runs; at most 1,000 enabled tests
with the exact active identity are replayed. Test validation has a 60-second
aggregate wall budget, and a test starts only when its complete configured
worker timeout still fits. Preview details retain at most 10,000 transforms and
the UI renders the first 200. See [`FORMATTING.md`](FORMATTING.md).

View File

@@ -1,6 +1,6 @@
# Portal requirements # Portal requirements
Regex Tools 0.1.0 is a static nested-path-safe application. Regex Tools is a static nested-path-safe application.
Required delivery behavior: Required delivery behavior:
@@ -9,11 +9,23 @@ Required delivery behavior:
revalidation/no-cache resources. revalidation/no-cache resources.
- Hashed Vite assets may use immutable long-term caching. - Hashed Vite assets may use immutable long-term caching.
- CSP must allow `worker-src 'self' blob:`. - CSP must allow `worker-src 'self' blob:`.
- No WebAssembly MIME rule or `wasm-unsafe-eval` is required by v0.1.0. - `engines/pcre2/pcre2.wasm` and
`engines/python/pyodide.asm.wasm` must use `application/wasm`.
- Engine `.mjs` files must use a JavaScript MIME type;
`engines/python/python_stdlib.zip`, JSON, licence, notice and checksum files
must be delivered without content transformation.
- `script-src` must permit self-hosted WebAssembly compilation, normally with
`'wasm-unsafe-eval'`; broad `'unsafe-eval'` is neither needed nor recommended.
- `connect-src 'self'` must allow the Pyodide worker to fetch its own
same-origin WebAssembly, standard-library and lock files.
- Every file below `engines/pcre2/`, `engines/python/` and `engines/java/`
should use revalidation or a release-scoped immutable cache. These are not
content-hashed filenames and must never drift under one release identity.
The Portal must consume the immutable release ZIP, verify its SHA-256, verify The Portal must consume the immutable release ZIP, verify its SHA-256, verify
manifest ID `de.add-ideas.regex-tools` and version `0.1.0`, and mount it at a manifest ID `de.add-ideas.regex-tools` and version `0.3.0`, and mount it at a
relative target such as `apps/regex/`. relative target such as `apps/regex/`.
When PCRE2 ships later, the smallest additional change will be The package, manifest, tag, artifact URL and checksum must all use the v0.3.0
`application/wasm` delivery and `script-src 'self' 'wasm-unsafe-eval'`. identity. Historical artifact coordinates remain immutable and must not be
overwritten or reused.

View File

@@ -1,14 +1,17 @@
# Reference implementations and sources # Reference implementations and sources
Inspection date: 2026-07-24. Inspection date: 2026-07-27.
| Reference | Exact revision/version | Licence | Use and copying status | | Reference | Exact revision/version | Licence | Use and copying status |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Regex Tools | Unborn repository at initial inspection; release identity `v0.1.0` | GPL-3.0-or-later | Application source written for this project. | | Regex Tools | Release identity `v0.3.0` | GPL-3.0-or-later | Application source written for this project. |
| Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. | | Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. |
| Toolbox Portal | `a9c31c8986c40a0097966318e925083302e91e13` / 0.5.0 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. | | Toolbox Portal | `a9c31c8986c40a0097966318e925083302e91e13` / 0.5.0 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
| regexpp | 4.12.2; npm integrity `sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==` | MIT | Pinned ECMAScript syntax provider. Provider AST is normalized in a worker. | | regexpp | 4.12.2; npm integrity `sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==` | MIT | Pinned ECMAScript syntax provider. Provider AST is normalized in a worker. |
| PCRE2 | signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | External technical spike only; not shipped or copied. | | PCRE2 | signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | Pinned offline build; generated verified WebAssembly pack is shipped, source is not vendored. |
| Pyodide | tag 314.0.3; commit `ac57031be7564f864d061cb37c5c152e59f83ad4`; pinned npm integrity | MPL-2.0 | Reviewed self-hosted runtime pack; no CDN use. |
| CPython | executable runtime identity 3.14.2 in Pyodide 314.0.3 | PSF-2.0 | Actual `re` compiler, matcher and replacement runtime; Python source is not vendored. |
| TeaVM | tag/commit `ee91b03e616c4b45401cd11fb0cd7eb0daf6649b` / 0.15.0 | Apache-2.0 | `java.util.regex` class library compiled into the verified module; not OpenJDK. |
| ECMAScript specification | <https://tc39.es/ecma262/>; living specification inspected 2026-07-24 | ECMA terms | Semantics reference; no prose copied. | | ECMAScript specification | <https://tc39.es/ecma262/>; living specification inspected 2026-07-24 | ECMA terms | Semantics reference; no prose copied. |
| regex101 | <https://regex101.com/> and public parser documentation | Product/commercial terms | Product reference only. No source, branding, explanations, reference prose or visual design copied. | | regex101 | <https://regex101.com/> and public parser documentation | Product/commercial terms | Product reference only. No source, branding, explanations, reference prose or visual design copied. |
| `recheck` | 4.5.0 | MIT | Metadata inspected; not installed or shipped. | | `recheck` | 4.5.0 | MIT | Metadata inspected; not installed or shipped. |
@@ -17,5 +20,5 @@ Inspection date: 2026-07-24.
| RegexLib | Website inspected | Redistribution licence not established | No fixture copied. | | RegexLib | Website inspected | Redistribution licence not established | No fixture copied. |
| RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. | | RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. |
All v0.1.0 conformance cases are project-authored. See All v0.1.0, v0.2.0 and v0.3.0 conformance cases are project-authored. See
`tests/fixtures/README.md`. `tests/fixtures/README.md`.

View File

@@ -18,6 +18,13 @@ on publication failure, replaces an existing exact version only with
`--force`, and produces: `--force`, and produces:
```text ```text
release/regex-tools-0.1.0.zip release/regex-tools-0.3.0.zip
release/regex-tools-0.1.0.zip.sha256 release/regex-tools-0.3.0.zip.sha256
``` ```
Before packaging, the engine verifier checks the closed PCRE2, Python and Java
file sets in `dist/`, including checksums, metadata, licences/notices, module
and WebAssembly identities and self-hosted relative asset references.
The existing v0.1.0 and v0.2.0 ZIPs and matching sidecars are historical
immutable artifacts. Creating v0.3.0 must not replace, rename or remove them.

View File

@@ -9,6 +9,28 @@ untrusted.
- A terminated worker is never reused. - A terminated worker is never reused.
- Pattern, subject, replacement/list template, match, capture and output sizes - Pattern, subject, replacement/list template, match, capture and output sizes
are bounded. are bounded.
- PCRE2 adds native match-step, depth and heap caps. Its C ABI returns only
caller-owned bounded records and releases code, match data and match context
on every exit path.
- PCRE2 automatic-callout tracing is a separate ABI and worker path. It copies
only complete fixed records and bounded mark text, stops matching at the
50,000-event or 10 MiB serialized trace cap, and is killed on timeout,
cancellation or crash.
- PCRE2 runs only in mandatory UTF/UCP mode. Exact UTF-8 byte boundaries are
normalized to editor UTF-16 offsets; lone surrogates are rejected instead of
being silently replaced by browser encoding.
- Python starts only after the worker verifies Pyodide, CPython and `re`
identities. Pattern, subject, flags and replacement remain string data passed
to the fixed application bridge; no user-supplied Python source is evaluated.
Native code-point boundaries are checked before conversion to editor UTF-16.
- Java starts only after the worker verifies the TeaVM bridge ABI, engine
identity and compiled self-test. It exposes only fixed bounded
`Pattern`/`Matcher` entry points; no JVM, Java class loading or user-supplied
JavaScript is available. The runtime is TeaVM, not OpenJDK.
- Every bundled engine directory has a closed file set, deterministic
metadata/checksums and licence material verified both before installation and
in `dist/`. Runtime imports and fetches resolve only against the same-origin
release path.
- Replacement templates are strings; executable callbacks are unavailable. - Replacement templates are strings; executable callbacks are unavailable.
- Replacement/list token decorations, rows, chips, diagnostics and list - Replacement/list token decorations, rows, chips, diagnostics and list
evaluation work have separate presentation/operation caps; incomplete list evaluation work have separate presentation/operation caps; incomplete list
@@ -25,14 +47,55 @@ untrusted.
- Unit-test assertion diagnostics never stringify complete large values and - Unit-test assertion diagnostics never stringify complete large values and
retain at most 2 KiB UTF-8 per case. Oversized replacement output is not retain at most 2 KiB UTF-8 per case. Oversized replacement output is not
copied into an exact current-result draft. copied into an exact current-result draft.
- No backend, uploads, telemetry, remote corpus URL, CDN, remote font, `eval`, - Corpus file input is local, recognized UTF-8 text only. It is bounded before
`Function`, arbitrary command line or unsafe HTML rendering exists. decoding, rejects NUL/binary input, runs sequentially in a dedicated
cancellable supervisor and never modifies source files.
- Corpus JSON/CSV/NDJSON summaries never contain source, capture-sample or
applied-output content. CSV cells are formula-injection protected. Local UI
previews are explicitly bounded. Applied files require an explicit
content-export acknowledgement; names are normalized against archive path
traversal and collisions. Incomplete applied output cannot be downloaded or
included in the all-document ZIP, whose bytes have a separate hard limit.
- Corpus documents, contents, outputs and results are never placed in project
JSON or IndexedDB. Schema-v1 imports that attempt to inject corpus payloads
are rejected.
- ECMAScript growth analysis preflights repetition count and UTF-8 bytes before
allocating a generated string. The analysis worker is terminated on timeout,
cancellation or crash; replacement timing also preflights its output bound.
Analysis settings and results are neither persisted nor uploaded.
- Generated-case synthesis runs in its own killable worker under AST, depth,
attempt, repetition and UTF-8 byte caps. Labels come only from bounded
requests to the selected real engine worker; a wrong outcome, timeout, crash,
cancellation or compile rejection is never converted into a verified case.
Settings, discarded candidates and unselected results are ephemeral and
never uploaded.
- Imported generated-test provenance is accepted only for the exact supported
generator id/version and must agree with the test assertion outcome. Unknown
provenance fields and future versions are rejected; editing a case removes
its provenance.
- ECMAScript formatting consumes only exact accepted regexpp literal-token
ranges and never treats PCRE2 as ECMAScript. Source/candidate syntax and
execution use separate killable workers; compile failure, truncation,
timeout asymmetry, engine-identity drift, incomplete tests or any semantic
difference fails closed. Preview/results are ephemeral and apply requires an
explicit confirmation.
- Subject minimization rejects lone surrogates, caps input at 1 MiB UTF-8,
evaluation count at 2,000 and aggregate wall time at 60 seconds. Each
predicate check runs in the selected killable engine worker. Timeout, crash,
cancellation and worker error remain distinct; truncated results fail
closed. Subjects and results are ephemeral and never uploaded or persisted.
- No backend, uploads, telemetry, remote corpus URL, CDN, remote font,
arbitrary command line or unsafe HTML rendering exists. Application code
does not evaluate user-controlled JavaScript or Python. The generic generated
Pyodide loader contains an Emscripten dynamic-link branch that uses
JavaScript `eval`, but the shipped self-contained CPython/stdlib path does not
exercise it and loads under the CSP below without broad `'unsafe-eval'`.
Version 0.1.0 needs a CSP equivalent to: The engine-enabled build needs a CSP equivalent to:
```text ```text
default-src 'self'; default-src 'self';
script-src 'self'; script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:; worker-src 'self' blob:;
connect-src 'self'; connect-src 'self';
img-src 'self' data:; img-src 'self' data:;
@@ -44,8 +107,11 @@ base-uri 'none';
form-action 'none'; form-action 'none';
``` ```
The Toolbox shell currently requires inline styles. No `unsafe-eval` or The Toolbox shell currently requires inline styles. Corpus ZIP generation,
`wasm-unsafe-eval` is required until an actual WebAssembly flavour ships. PCRE2, Pyodide and TeaVM use self-hosted modules/assets and local workers under
the existing `worker-src` and `connect-src` policy. Broad `'unsafe-eval'` is not
required; `'wasm-unsafe-eval'` remains required for the two WebAssembly
runtimes.
Report vulnerabilities privately to the repository owner before public issue Report vulnerabilities privately to the repository owner before public issue
details are posted. details are posted.

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,11 @@
This product includes software developed by Alexey Andreev
(http://teavm.org).
This product includes software developed by The Apache Software
Foundation (http://www.apache.org/).
=============================================================================
= NOTICE file corresponding to section 4d of the Apache License Version 2.0 =
=============================================================================
This product includes software developed by
Joda.org (http://www.joda.org/).

25
engines/java/README.md Normal file
View File

@@ -0,0 +1,25 @@
# TeaVM Java regular-expression engine
This project compiles a bounded bridge around TeaVM 0.15.0's
`java.util.regex.Pattern` and `Matcher` class-library implementation to a
minified ES2015 module.
The runtime is intentionally identified as `TeaVM java.util.regex 0.15.0`.
TeaVM supplies its own Apache Harmony-derived Java class-library subset; the
generated engine is not OpenJDK and must not be presented as exact OpenJDK
semantics.
The `U` application flag is a compatibility request. TeaVM 0.15.0 does not
implement OpenJDK's `Pattern.UNICODE_CHARACTER_CLASS`; the bridge implies
Unicode case folding but retains TeaVM's predefined-character-class and word
boundary behaviour. The TypeScript adapter reports this boundary.
Replacement is delegated to TeaVM 0.15.0's native `Matcher` implementation.
That implementation recognizes only a single decimal digit after `$` (`$0`
through `$9`, subject to the compiled group count). It does not implement
OpenJDK's `${name}` named-replacement syntax; the bridge returns a
`replacement-error` when such a replacement is attempted.
Maven builds `target/generated/java-regex.mjs`. The repository pack script
verifies the pinned source identity, bridge source hashes, licence/notice and
generated module before it can be installed under `public/engines/java`.

84
engines/java/pom.xml Normal file
View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.addideas.regextools</groupId>
<artifactId>java-regex-engine</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.outputTimestamp>2026-06-06T15:27:10Z</project.build.outputTimestamp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<teavm.version>0.15.0</teavm.version>
</properties>
<dependencies>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-classlib</artifactId>
<version>${teavm.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso</artifactId>
<version>${teavm.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<outputTimestamp>${project.build.outputTimestamp}</outputTimestamp>
</configuration>
</plugin>
<plugin>
<groupId>org.teavm</groupId>
<artifactId>teavm-maven-plugin</artifactId>
<version>${teavm.version}</version>
<executions>
<execution>
<id>java-regex-browser-engine</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<mainClass>de.addideas.regextools.javaengine.RegexBridge</mainClass>
<targetDirectory>${project.build.directory}/generated</targetDirectory>
<targetFileName>java-regex.mjs</targetFileName>
<targetType>JAVASCRIPT</targetType>
<jsModuleType>ES2015</jsModuleType>
<minifying>true</minifying>
<optimizationLevel>ADVANCED</optimizationLevel>
<debugInformationGenerated>false</debugInformationGenerated>
<sourceMapsGenerated>false</sourceMapsGenerated>
<sourceFilesCopied>false</sourceFilesCopied>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,346 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* The generated module links TeaVM 0.15.0 class-library code distributed
* under Apache-2.0. Its runtime identity is TeaVM java.util.regex, not OpenJDK.
*/
package de.addideas.regextools.javaengine;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.teavm.jso.JSExport;
public final class RegexBridge {
private static final int BRIDGE_ABI_VERSION = 1;
private static final String ENGINE_IDENTITY = "TeaVM java.util.regex 0.15.0";
private static final int MAXIMUM_CAPTURE_GROUPS = 1_000;
private static final int MAXIMUM_NATIVE_OUTPUT_UTF16 = 64 * 1024 * 1024;
private static final int FLAG_CASE_INSENSITIVE = 1 << 0;
private static final int FLAG_MULTILINE = 1 << 1;
private static final int FLAG_DOTALL = 1 << 2;
private static final int FLAG_UNICODE_CASE = 1 << 3;
private static final int FLAG_COMMENTS = 1 << 4;
private static final int FLAG_UNICODE_CHARACTER_COMPATIBILITY = 1 << 5;
private static final int FLAG_UNIX_LINES = 1 << 6;
private RegexBridge() {
}
public static void main(String[] arguments) {
// TeaVM requires a main class. All useful entry points are JS exports.
}
@JSExport
public static int bridgeAbiVersion() {
return BRIDGE_ABI_VERSION;
}
@JSExport
public static String engineIdentity() {
return ENGINE_IDENTITY;
}
@JSExport
public static int selfTest() {
try {
Matcher matcher = Pattern.compile("(?<=a)(b)\\1").matcher("abb");
if (!matcher.find() || matcher.start() != 1 || matcher.end() != 3
|| matcher.start(1) != 1 || matcher.end(1) != 2) {
return 1;
}
StringBuffer output = new StringBuffer();
matcher.appendReplacement(output, "<$1>");
matcher.appendTail(output);
return "a<b>".contentEquals(output) ? 0 : 2;
} catch (RuntimeException failure) {
return 3;
}
}
@JSExport
public static String execute(
String pattern,
String subject,
int applicationFlags,
boolean iterate,
int maximumMatches,
int maximumCaptureRows) {
return run(
pattern,
subject,
applicationFlags,
iterate,
maximumMatches,
maximumCaptureRows,
null);
}
@JSExport
public static String replace(
String pattern,
String subject,
String replacement,
int applicationFlags,
boolean iterate,
int maximumMatches,
int maximumCaptureRows) {
return run(
pattern,
subject,
applicationFlags,
iterate,
maximumMatches,
maximumCaptureRows,
replacement);
}
private static String run(
String patternText,
String subject,
int applicationFlags,
boolean iterate,
int maximumMatches,
int maximumCaptureRows,
String replacement) {
if (patternText == null || subject == null) {
return failure("bridge-error", "NullPointerException",
"Pattern and subject must not be null.", -1);
}
if (maximumMatches < 1 || maximumCaptureRows < 1) {
return failure("limit-error", "IllegalArgumentException",
"Result limits must be positive.", -1);
}
Pattern pattern;
try {
pattern = Pattern.compile(patternText, nativeFlags(applicationFlags));
} catch (PatternSyntaxException failure) {
String message = failure.getDescription();
if (message == null || message.length() == 0) {
message = failure.getMessage();
}
return failure("compile-error", "PatternSyntaxException", message, failure.getIndex());
} catch (RuntimeException failure) {
return failure("compile-error", failure.getClass().getSimpleName(),
safeMessage(failure, "TeaVM rejected the pattern."), -1);
}
Matcher matcher = pattern.matcher(subject);
int groupCount = matcher.groupCount();
if (groupCount > MAXIMUM_CAPTURE_GROUPS) {
return failure("limit-error", "CaptureGroupLimit",
"The pattern exceeds the 1,000 capture-group execution limit.", -1);
}
StringBuilder records = new StringBuilder();
StringBuffer output = replacement == null ? null : new StringBuffer();
int matchCount = 0;
int captureRows = 0;
int lastReplacementEnd = 0;
boolean resultsTruncated = false;
boolean outputTruncated = false;
try {
while (matcher.find()) {
int rowsForMatch = Math.max(1, groupCount);
if (matchCount >= maximumMatches
|| captureRows + rowsForMatch > maximumCaptureRows) {
resultsTruncated = true;
break;
}
appendNumber(records, matcher.start());
appendNumber(records, matcher.end());
for (int group = 1; group <= groupCount; group++) {
appendNumber(records, matcher.start(group));
appendNumber(records, matcher.end(group));
}
matchCount++;
captureRows += rowsForMatch;
if (output != null && !outputTruncated) {
long expansionLength = replacementExpansionLength(matcher, replacement);
long projectedLength = (long) output.length()
+ matcher.start() - lastReplacementEnd
+ expansionLength;
if (projectedLength > MAXIMUM_NATIVE_OUTPUT_UTF16) {
appendBounded(
output,
subject,
lastReplacementEnd,
matcher.start(),
MAXIMUM_NATIVE_OUTPUT_UTF16);
outputTruncated = true;
} else {
matcher.appendReplacement(output, replacement);
lastReplacementEnd = matcher.end();
}
}
if (!iterate) {
break;
}
}
if (output != null && !outputTruncated) {
long projectedLength = (long) output.length()
+ subject.length() - lastReplacementEnd;
if (projectedLength > MAXIMUM_NATIVE_OUTPUT_UTF16) {
appendBounded(
output,
subject,
lastReplacementEnd,
subject.length(),
MAXIMUM_NATIVE_OUTPUT_UTF16);
outputTruncated = true;
} else {
matcher.appendTail(output);
}
}
} catch (RuntimeException failure) {
String kind = replacement == null ? "execution-error" : "replacement-error";
String fallback = replacement == null
? "TeaVM failed while matching."
: "TeaVM Matcher rejected the replacement.";
return failure(kind, failure.getClass().getSimpleName(),
safeMessage(failure, fallback), -1);
}
FieldWriter result = new FieldWriter();
result.field("1");
result.field("ok");
result.number(groupCount);
result.bool(resultsTruncated);
result.number(matchCount);
result.raw(records);
if (output != null) {
result.bool(outputTruncated);
result.field(output.toString());
}
return result.toString();
}
private static int nativeFlags(int applicationFlags) {
int flags = 0;
if ((applicationFlags & FLAG_CASE_INSENSITIVE) != 0) {
flags |= Pattern.CASE_INSENSITIVE;
}
if ((applicationFlags & FLAG_MULTILINE) != 0) {
flags |= Pattern.MULTILINE;
}
if ((applicationFlags & FLAG_DOTALL) != 0) {
flags |= Pattern.DOTALL;
}
if ((applicationFlags & FLAG_UNICODE_CASE) != 0
|| (applicationFlags & FLAG_UNICODE_CHARACTER_COMPATIBILITY) != 0) {
flags |= Pattern.UNICODE_CASE;
}
if ((applicationFlags & FLAG_COMMENTS) != 0) {
flags |= Pattern.COMMENTS;
}
if ((applicationFlags & FLAG_UNIX_LINES) != 0) {
flags |= Pattern.UNIX_LINES;
}
return flags;
}
private static long replacementExpansionLength(Matcher matcher, String replacement) {
long length = 0;
for (int index = 0; index < replacement.length(); index++) {
char current = replacement.charAt(index);
if (current == '\\') {
index++;
if (index >= replacement.length()) {
throw new IndexOutOfBoundsException();
}
length++;
} else if (current == '$') {
index++;
if (index >= replacement.length()) {
throw new IllegalArgumentException("");
}
int group = Character.digit(replacement.charAt(index), 10);
if (group < 0) {
throw new IllegalArgumentException("");
}
String value = matcher.group(group);
// TeaVM 0.15.0's native replacement path dereferences this
// value too, including its unmatched-capture behaviour.
length += value.length();
} else {
length++;
}
if (length > MAXIMUM_NATIVE_OUTPUT_UTF16) {
return length;
}
}
return length;
}
private static void appendBounded(
StringBuffer output,
String value,
int start,
int end,
int maximumLength) {
int remaining = maximumLength - output.length();
if (remaining <= 0 || start >= end) {
return;
}
int boundedEnd = Math.min(end, start + remaining);
if (boundedEnd < end
&& boundedEnd > start
&& Character.isHighSurrogate(value.charAt(boundedEnd - 1))
&& Character.isLowSurrogate(value.charAt(boundedEnd))) {
boundedEnd--;
}
output.append(value, start, boundedEnd);
}
private static String failure(String status, String name, String message, int index) {
FieldWriter result = new FieldWriter();
result.field("1");
result.field(status);
result.field(name == null ? "Error" : name);
result.field(message == null ? "" : message);
result.number(index);
return result.toString();
}
private static String safeMessage(RuntimeException failure, String fallback) {
String message = failure.getMessage();
return message == null || message.length() == 0 ? fallback : message;
}
private static void appendNumber(StringBuilder output, int value) {
String encoded = Integer.toString(value);
output.append(encoded.length()).append(':').append(encoded);
}
private static final class FieldWriter {
private final StringBuilder output = new StringBuilder();
void field(String value) {
output.append(value.length()).append(':').append(value);
}
void number(int value) {
field(Integer.toString(value));
}
void bool(boolean value) {
field(value ? "1" : "0");
}
void raw(StringBuilder encodedFields) {
output.append(encodedFields);
}
@Override
public String toString() {
return output.toString();
}
}
}

View File

@@ -0,0 +1,71 @@
# SPDX-License-Identifier: GPL-3.0-or-later
cmake_minimum_required(VERSION 3.25)
project(regex_pcre2_bridge LANGUAGES C)
if(NOT EMSCRIPTEN)
message(FATAL_ERROR "The staged PCRE2 pack must be built with Emscripten.")
endif()
if(NOT DEFINED PCRE2_SOURCE_DIR OR NOT IS_DIRECTORY "${PCRE2_SOURCE_DIR}")
message(FATAL_ERROR "PCRE2_SOURCE_DIR must identify the verified PCRE2 source checkout.")
endif()
if(NOT DEFINED REGEX_PCRE2_OUTPUT_DIR)
message(FATAL_ERROR "REGEX_PCRE2_OUTPUT_DIR must identify the private staging directory.")
endif()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2_8 ON CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2_16 OFF CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2_32 OFF CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2GREP OFF CACHE BOOL "" FORCE)
set(PCRE2_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(PCRE2_DEBUG OFF CACHE STRING "" FORCE)
set(PCRE2_REBUILD_CHARTABLES OFF CACHE BOOL "" FORCE)
set(PCRE2_SHOW_REPORT OFF CACHE BOOL "" FORCE)
set(PCRE2_STATIC_PIC OFF CACHE BOOL "" FORCE)
set(PCRE2_SUPPORT_JIT OFF CACHE BOOL "" FORCE)
set(PCRE2_SUPPORT_UNICODE ON CACHE BOOL "" FORCE)
add_subdirectory("${PCRE2_SOURCE_DIR}" pcre2-source EXCLUDE_FROM_ALL)
add_executable(regex-pcre2-pack pcre2_bridge.c)
target_compile_features(regex-pcre2-pack PRIVATE c_std_11)
target_compile_definitions(regex-pcre2-pack PRIVATE PCRE2_CODE_UNIT_WIDTH=8)
target_compile_options(
regex-pcre2-pack
PRIVATE
-O3
-fno-ident
-fvisibility=hidden
)
target_link_libraries(regex-pcre2-pack PRIVATE pcre2-8-static)
target_link_options(
regex-pcre2-pack
PRIVATE
-O3
--no-entry
"SHELL:-s ALLOW_MEMORY_GROWTH=1"
"SHELL:-s ASSERTIONS=0"
"SHELL:-s ENVIRONMENT=web,worker,node"
"SHELL:-s EXPORT_ES6=1"
"SHELL:-s EXPORT_NAME=createRegexPcre2"
"SHELL:-s EXPORTED_FUNCTIONS=['_regex_pcre2_bridge_abi_version','_regex_pcre2_compile_probe','_regex_pcre2_config_flags','_regex_pcre2_error_message','_regex_pcre2_execute','_regex_pcre2_self_test','_regex_pcre2_substitute','_regex_pcre2_trace','_regex_pcre2_version','_malloc','_free']"
"SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPU8','UTF8ToString']"
"SHELL:-s FILESYSTEM=0"
"SHELL:-s INITIAL_MEMORY=16777216"
"SHELL:-s MALLOC=emmalloc"
"SHELL:-s MAXIMUM_MEMORY=268435456"
"SHELL:-s MODULARIZE=1"
"SHELL:-s NO_EXIT_RUNTIME=1"
"SHELL:-s STRICT=1"
)
set_target_properties(
regex-pcre2-pack
PROPERTIES
OUTPUT_NAME pcre2
RUNTIME_OUTPUT_DIRECTORY "${REGEX_PCRE2_OUTPUT_DIR}"
SUFFIX ".mjs"
)

27
engines/pcre2/README.md Normal file
View File

@@ -0,0 +1,27 @@
# PCRE2 engine bridge
This directory contains the GPL-3.0-or-later application bridge and
deterministic build description for the bundled PCRE2 flavour. It deliberately
does not vendor the upstream PCRE2 source tree or generated binaries.
ABI version 3 exposes:
- exact engine identity and configuration;
- compile diagnostics with UTF-8 byte offsets;
- bounded non-overlapping matching with copied group-zero/capture records and
copied capture-name metadata;
- bounded native `pcre2_substitute()` semantics over the same retained match
sequence;
- bounded automatic-callout tracing from one native `pcre2_match()` invocation,
with copied event and mark data and an immediate native stop at either trace
cap;
- match-step, depth, heap, match-count, capture-row and output limits;
- no native pointer or compiled-code handle across a call boundary.
All PCRE2 allocations are released before an ABI call returns. JavaScript owns
and frees every input/result buffer in `finally`, and the containing worker is
terminated on timeout, crash, cancellation or supersession.
The explicit offline source/toolchain identity, build, verification and local
asset-install workflow is documented in
[`docs/ENGINE_BUILDS.md`](../../docs/ENGINE_BUILDS.md).

View File

@@ -0,0 +1,910 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#define PCRE2_CODE_UNIT_WIDTH 8
#include "pcre2_bridge.h"
#include <pcre2.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#define REGEX_PCRE2_COMPILE_FLAGS \
(REGEX_PCRE2_CASELESS | REGEX_PCRE2_MULTILINE | REGEX_PCRE2_DOTALL | \
REGEX_PCRE2_EXTENDED | REGEX_PCRE2_UNGREEDY | REGEX_PCRE2_UTF | \
REGEX_PCRE2_UCP | REGEX_PCRE2_DUPNAMES)
#define REGEX_PCRE2_ALL_FLAGS \
(REGEX_PCRE2_COMPILE_FLAGS | REGEX_PCRE2_GLOBAL)
_Static_assert(sizeof(regex_pcre2_compile_result) == 20,
"Compile result is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_limits) == 20,
"Limits record is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_match_record) == 16,
"Match record is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_name_record) == 12,
"Name record is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_run_result) == 60,
"Run result is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_trace_limits) == 20,
"Trace limits are part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_trace_event) == 32,
"Trace event is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_trace_result) == 52,
"Trace result is part of ABI version 3.");
typedef struct regex_pcre2_run_buffers {
regex_pcre2_match_record *records;
uint32_t record_capacity;
regex_pcre2_name_record *name_records;
uint32_t name_record_capacity;
uint8_t *name_bytes;
uint32_t name_bytes_capacity;
uint8_t *output;
uint32_t output_capacity;
} regex_pcre2_run_buffers;
typedef struct regex_pcre2_trace_state {
regex_pcre2_trace_event *events;
uint32_t event_capacity;
uint8_t *mark_bytes;
uint32_t mark_bytes_capacity;
uint32_t maximum_events;
uint32_t maximum_trace_bytes;
uint32_t event_count;
uint32_t total_event_count;
uint32_t mark_bytes_length;
uint32_t events_truncated;
uint32_t marks_truncated;
} regex_pcre2_trace_state;
static const uint8_t empty_bytes[1] = {0};
static uint32_t to_pcre2_options(uint32_t application_flags) {
uint32_t options = 0;
if ((application_flags & REGEX_PCRE2_CASELESS) != 0) {
options |= PCRE2_CASELESS;
}
if ((application_flags & REGEX_PCRE2_MULTILINE) != 0) {
options |= PCRE2_MULTILINE;
}
if ((application_flags & REGEX_PCRE2_DOTALL) != 0) {
options |= PCRE2_DOTALL;
}
if ((application_flags & REGEX_PCRE2_EXTENDED) != 0) {
options |= PCRE2_EXTENDED;
}
if ((application_flags & REGEX_PCRE2_UNGREEDY) != 0) {
options |= PCRE2_UNGREEDY;
}
if ((application_flags & REGEX_PCRE2_UTF) != 0) {
options |= PCRE2_UTF;
}
if ((application_flags & REGEX_PCRE2_UCP) != 0) {
options |= PCRE2_UCP;
}
if ((application_flags & REGEX_PCRE2_DUPNAMES) != 0) {
options |= PCRE2_DUPNAMES;
}
return options;
}
static int32_t fail_compile(regex_pcre2_compile_result *result,
int32_t status) {
if (result != NULL) {
memset(result, 0, sizeof(*result));
result->status = status;
}
return status;
}
static int32_t fail_run(regex_pcre2_run_result *result,
int32_t status,
uint32_t phase,
uint32_t offset) {
if (result != NULL) {
memset(result, 0, sizeof(*result));
result->status = status;
result->error_phase = phase;
result->error_offset = offset;
}
return status;
}
static int32_t fail_trace(regex_pcre2_trace_result *result,
int32_t status,
uint32_t phase,
uint32_t offset) {
if (result != NULL) {
memset(result, 0, sizeof(*result));
result->status = status;
result->error_phase = phase;
result->error_offset = offset;
result->last_complete_event = UINT32_MAX;
}
return status;
}
static int input_pointer_valid(const uint8_t *value, uint32_t length) {
return value != NULL || length == 0;
}
static const uint8_t *nonnull_input(const uint8_t *value) {
return value == NULL ? empty_bytes : value;
}
static int limits_valid(const regex_pcre2_limits *limits) {
return limits != NULL && limits->maximum_matches > 0 &&
limits->maximum_matches <= REGEX_PCRE2_MAX_MATCHES &&
limits->maximum_capture_rows > 0 &&
limits->maximum_capture_rows <= REGEX_PCRE2_MAX_CAPTURE_ROWS &&
limits->match_limit > 0 &&
limits->match_limit <= REGEX_PCRE2_MAX_MATCH_LIMIT &&
limits->depth_limit > 0 &&
limits->depth_limit <= REGEX_PCRE2_MAX_DEPTH_LIMIT &&
limits->heap_limit_kib > 0 &&
limits->heap_limit_kib <= REGEX_PCRE2_MAX_HEAP_LIMIT_KIB;
}
static int buffers_valid(const regex_pcre2_run_buffers *buffers,
int substitution) {
if (buffers == NULL ||
(buffers->records == NULL && buffers->record_capacity != 0) ||
(buffers->name_records == NULL &&
buffers->name_record_capacity != 0) ||
(buffers->name_bytes == NULL && buffers->name_bytes_capacity != 0)) {
return 0;
}
/*
* PCRE2 writes a trailing NUL even when the semantic output capacity is
* zero, so the bridge contract always requires the advertised +1 byte.
*/
if (substitution && buffers->output == NULL) {
return 0;
}
return 1;
}
static uint32_t bounded_offset(PCRE2_SIZE value) {
return value > UINT32_MAX ? UINT32_MAX : (uint32_t)value;
}
static int32_t copy_names(const pcre2_code *code,
regex_pcre2_run_buffers *buffers,
regex_pcre2_run_result *result) {
uint32_t entry_size = 0;
PCRE2_SPTR table = NULL;
uint32_t index = 0;
uint32_t name_offset = 0;
if (result->name_count == 0) {
return 0;
}
if (pcre2_pattern_info(code, PCRE2_INFO_NAMEENTRYSIZE, &entry_size) != 0 ||
pcre2_pattern_info(code, PCRE2_INFO_NAMETABLE, &table) != 0 ||
entry_size < 3 || table == NULL) {
return REGEX_PCRE2_ERROR_CONFIG;
}
for (index = 0; index < result->name_count; index += 1) {
const uint8_t *entry = table + (index * entry_size);
uint32_t group_number =
((uint32_t)entry[0] << 8) | (uint32_t)entry[1];
uint32_t maximum_name = entry_size - 2;
uint32_t name_length = 0;
while (name_length < maximum_name && entry[2 + name_length] != 0) {
name_length += 1;
}
if (result->name_record_count >= buffers->name_record_capacity ||
name_length > buffers->name_bytes_capacity - name_offset) {
result->names_truncated = 1;
break;
}
buffers->name_records[result->name_record_count].group_number =
group_number;
buffers->name_records[result->name_record_count].name_offset =
name_offset;
buffers->name_records[result->name_record_count].name_length =
name_length;
if (name_length > 0) {
memcpy(buffers->name_bytes + name_offset, entry + 2, name_length);
}
result->name_record_count += 1;
name_offset += name_length;
}
result->name_bytes_length = name_offset;
return 0;
}
static int append_utf8_prefix(regex_pcre2_run_buffers *buffers,
regex_pcre2_run_result *result,
const uint8_t *source,
uint32_t length) {
uint32_t remaining = buffers->output_capacity - result->output_length;
uint32_t copied = length < remaining ? length : remaining;
if (copied < length) {
while (copied > 0 && (source[copied] & 0xc0u) == 0x80u) {
copied -= 1;
}
result->output_truncated = 1;
}
if (copied > 0) {
memcpy(buffers->output + result->output_length, source, copied);
result->output_length += copied;
}
return copied == length;
}
static int32_t configure_match_context_values(pcre2_match_context *context,
uint32_t match_limit,
uint32_t depth_limit,
uint32_t heap_limit_kib) {
if (pcre2_set_match_limit(context, match_limit) != 0 ||
pcre2_set_depth_limit(context, depth_limit) != 0 ||
pcre2_set_heap_limit(context, heap_limit_kib) != 0) {
return REGEX_PCRE2_ERROR_CONFIG;
}
return 0;
}
static int32_t configure_match_context(pcre2_match_context *context,
const regex_pcre2_limits *limits) {
return configure_match_context_values(
context, limits->match_limit, limits->depth_limit,
limits->heap_limit_kib);
}
static int trace_limits_valid(const regex_pcre2_trace_limits *limits) {
return limits != NULL && limits->maximum_events > 0 &&
limits->maximum_events <= REGEX_PCRE2_MAX_TRACE_EVENTS &&
limits->maximum_trace_bytes >= sizeof(regex_pcre2_trace_event) &&
limits->maximum_trace_bytes <= REGEX_PCRE2_MAX_TRACE_BYTES &&
limits->match_limit > 0 &&
limits->match_limit <= REGEX_PCRE2_MAX_MATCH_LIMIT &&
limits->depth_limit > 0 &&
limits->depth_limit <= REGEX_PCRE2_MAX_DEPTH_LIMIT &&
limits->heap_limit_kib > 0 &&
limits->heap_limit_kib <= REGEX_PCRE2_MAX_HEAP_LIMIT_KIB;
}
static uint32_t bounded_mark_length(PCRE2_SPTR mark, uint32_t *truncated) {
uint32_t length = 0;
if (mark == NULL) {
return 0;
}
while (length < REGEX_PCRE2_MAX_TRACE_MARK_BYTES && mark[length] != 0) {
length += 1;
}
if (length == REGEX_PCRE2_MAX_TRACE_MARK_BYTES && mark[length] != 0) {
*truncated = 1;
while (length > 0 && (mark[length] & 0xc0u) == 0x80u) {
length -= 1;
}
}
return length;
}
static int trace_callout(pcre2_callout_block *block, void *data) {
regex_pcre2_trace_state *state = (regex_pcre2_trace_state *)data;
regex_pcre2_trace_event *event = NULL;
uint32_t mark_length = 0;
uint32_t mark_was_truncated = 0;
uint64_t required_trace_bytes = 0;
if (block == NULL || state == NULL) {
return PCRE2_ERROR_CALLOUT;
}
if (state->total_event_count < UINT32_MAX) {
state->total_event_count += 1;
}
mark_length = bounded_mark_length(block->mark, &mark_was_truncated);
required_trace_bytes =
((uint64_t)state->event_count + 1u) *
sizeof(regex_pcre2_trace_event) +
state->mark_bytes_length + mark_length;
if (state->event_count >= state->maximum_events ||
state->event_count >= state->event_capacity ||
required_trace_bytes > state->maximum_trace_bytes ||
mark_length > state->mark_bytes_capacity - state->mark_bytes_length) {
state->events_truncated = 1;
return PCRE2_ERROR_CALLOUT;
}
event = &state->events[state->event_count];
event->callout_number = block->callout_number;
event->pattern_position_byte = bounded_offset(block->pattern_position);
event->next_item_length_byte = bounded_offset(block->next_item_length);
event->subject_position_byte = bounded_offset(block->current_position);
event->capture_top = block->capture_top;
event->capture_last = block->capture_last;
event->mark_offset = state->mark_bytes_length;
event->mark_length = mark_length;
if (mark_length > 0) {
memcpy(state->mark_bytes + state->mark_bytes_length, block->mark,
mark_length);
state->mark_bytes_length += mark_length;
}
state->marks_truncated |= mark_was_truncated;
state->event_count += 1;
return 0;
}
static int32_t run_bounded(const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
const uint8_t *replacement,
uint32_t replacement_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_run_buffers *buffers,
int substitution,
regex_pcre2_run_result *result) {
pcre2_code *code = NULL;
pcre2_match_data *match_data = NULL;
pcre2_match_context *match_context = NULL;
int compile_error = 0;
PCRE2_SIZE compile_offset = 0;
uint32_t capture_count = 0;
uint32_t name_count = 0;
uint32_t options = 0;
uint32_t search_offset = 0;
uint32_t match_options = 0;
uint32_t copied_until = 0;
uint32_t capture_rows = 0;
int32_t status = 0;
const uint8_t *safe_pattern = nonnull_input(pattern);
const uint8_t *safe_subject = nonnull_input(subject);
const uint8_t *safe_replacement = nonnull_input(replacement);
if (result == NULL || !input_pointer_valid(pattern, pattern_length) ||
!input_pointer_valid(subject, subject_length) ||
(substitution &&
!input_pointer_valid(replacement, replacement_length)) ||
!buffers_valid(buffers, substitution)) {
return fail_run(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
memset(result, 0, sizeof(*result));
if ((application_flags & ~REGEX_PCRE2_ALL_FLAGS) != 0) {
return fail_run(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) {
return fail_run(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (subject_length > REGEX_PCRE2_MAX_SUBJECT_BYTES) {
return fail_run(result, REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (substitution &&
replacement_length > REGEX_PCRE2_MAX_REPLACEMENT_BYTES) {
return fail_run(result, REGEX_PCRE2_ERROR_REPLACEMENT_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (!limits_valid(limits)) {
return fail_run(result, REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
options = to_pcre2_options(application_flags);
result->effective_options = options;
code = pcre2_compile(safe_pattern, pattern_length, options, &compile_error,
&compile_offset, NULL);
if (code == NULL) {
status = compile_error;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_COMPILE;
result->error_offset = bounded_offset(compile_offset);
goto cleanup;
}
if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0 ||
pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
result->capture_count = capture_count;
result->name_count = name_count;
if (capture_count > REGEX_PCRE2_MAX_CAPTURE_GROUPS) {
status = REGEX_PCRE2_ERROR_CAPTURE_LIMIT;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
status = copy_names(code, buffers, result);
if (status != 0) {
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
match_data = pcre2_match_data_create_from_pattern(code, NULL);
match_context = pcre2_match_context_create(NULL);
if (match_data == NULL || match_context == NULL) {
status = PCRE2_ERROR_NOMEMORY;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
status = configure_match_context(match_context, limits);
if (status != 0) {
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
while (search_offset <= subject_length) {
int match_status =
pcre2_match(code, safe_subject, subject_length, search_offset,
match_options, match_data, match_context);
PCRE2_SIZE *ovector = NULL;
uint32_t required_records = capture_count + 1;
uint32_t required_capture_rows = capture_count == 0 ? 1 : capture_count;
uint32_t group = 0;
uint32_t start = 0;
uint32_t end = 0;
if (match_status == PCRE2_ERROR_NOMATCH) {
break;
}
if (match_status < 0) {
status = match_status;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_MATCH;
result->error_offset = search_offset;
goto cleanup;
}
if (match_status == 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
ovector = pcre2_get_ovector_pointer(match_data);
start = bounded_offset(ovector[0]);
end = bounded_offset(ovector[1]);
if (start > end || end > subject_length) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
if (result->match_count >= limits->maximum_matches ||
capture_rows + required_capture_rows >
limits->maximum_capture_rows ||
required_records > buffers->record_capacity - result->record_count) {
result->results_truncated = 1;
break;
}
for (group = 0; group <= capture_count; group += 1) {
regex_pcre2_match_record *record =
&buffers->records[result->record_count];
PCRE2_SIZE native_start = ovector[group * 2];
PCRE2_SIZE native_end = ovector[group * 2 + 1];
record->match_number = result->match_count + 1;
record->group_number = group;
if (native_start == PCRE2_UNSET || native_end == PCRE2_UNSET) {
record->start_byte = REGEX_PCRE2_UNSET_OFFSET;
record->end_byte = REGEX_PCRE2_UNSET_OFFSET;
} else {
record->start_byte = bounded_offset(native_start);
record->end_byte = bounded_offset(native_end);
}
result->record_count += 1;
}
result->match_count += 1;
capture_rows += required_capture_rows;
if (substitution && result->output_truncated == 0) {
PCRE2_SIZE replacement_output_length =
buffers->output_capacity - result->output_length + 1;
int substitute_status = 0;
if (!append_utf8_prefix(buffers, result, safe_subject + copied_until,
start - copied_until)) {
break;
}
replacement_output_length =
buffers->output_capacity - result->output_length + 1;
substitute_status = pcre2_substitute(
code, safe_subject, subject_length, search_offset,
match_options | PCRE2_SUBSTITUTE_MATCHED |
PCRE2_SUBSTITUTE_REPLACEMENT_ONLY |
PCRE2_SUBSTITUTE_UNSET_EMPTY,
match_data, match_context, safe_replacement, replacement_length,
buffers->output + result->output_length, &replacement_output_length);
if (substitute_status == PCRE2_ERROR_NOMEMORY) {
result->output_truncated = 1;
break;
}
if (substitute_status < 0) {
status = substitute_status;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_SUBSTITUTE;
result->error_offset = bounded_offset(replacement_output_length);
goto cleanup;
}
result->output_length += bounded_offset(replacement_output_length);
result->substitution_count += (uint32_t)substitute_status;
copied_until = end;
}
if ((application_flags & REGEX_PCRE2_GLOBAL) == 0) {
break;
}
{
PCRE2_SIZE next_offset = search_offset;
uint32_t next_options = 0;
if (!pcre2_next_match(match_data, &next_offset, &next_options)) {
break;
}
search_offset = bounded_offset(next_offset);
match_options = next_options;
}
}
if (substitution && result->output_truncated == 0) {
(void)append_utf8_prefix(buffers, result, safe_subject + copied_until,
subject_length - copied_until);
}
cleanup:
if (match_context != NULL) {
pcre2_match_context_free(match_context);
}
if (match_data != NULL) {
pcre2_match_data_free(match_data);
}
if (code != NULL) {
pcre2_code_free(code);
}
return status;
}
uint32_t regex_pcre2_bridge_abi_version(void) {
return REGEX_PCRE2_BRIDGE_ABI_VERSION;
}
uint32_t regex_pcre2_config_flags(void) {
uint32_t unicode = 0;
uint32_t jit = 0;
uint32_t flags = 0;
if (pcre2_config(PCRE2_CONFIG_UNICODE, &unicode) != 0 ||
pcre2_config(PCRE2_CONFIG_JIT, &jit) != 0) {
return 0;
}
if (unicode != 0) {
flags |= REGEX_PCRE2_CONFIG_UNICODE;
}
if (jit != 0) {
flags |= REGEX_PCRE2_CONFIG_JIT;
}
return flags;
}
const uint8_t *regex_pcre2_version(void) {
static uint8_t version[64];
if (pcre2_config(PCRE2_CONFIG_VERSION, version) <= 0) {
version[0] = '\0';
}
return version;
}
int32_t regex_pcre2_error_message(int32_t error_code,
uint8_t *output,
uint32_t output_capacity) {
int status = 0;
if (output == NULL || output_capacity == 0) {
return REGEX_PCRE2_ERROR_INVALID_ARGUMENT;
}
status = pcre2_get_error_message(error_code, output, output_capacity);
if (status < 0) {
output[0] = '\0';
}
return status;
}
int32_t regex_pcre2_compile_probe(const uint8_t *pattern,
uint32_t pattern_length,
uint32_t application_flags,
regex_pcre2_compile_result *result) {
pcre2_code *code = NULL;
int error_code = 0;
PCRE2_SIZE error_offset = 0;
uint32_t capture_count = 0;
uint32_t name_count = 0;
uint32_t options = 0;
if (result == NULL || !input_pointer_valid(pattern, pattern_length)) {
return fail_compile(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT);
}
memset(result, 0, sizeof(*result));
if ((application_flags & ~REGEX_PCRE2_COMPILE_FLAGS) != 0) {
return fail_compile(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS);
}
if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) {
return fail_compile(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE);
}
options = to_pcre2_options(application_flags);
result->effective_options = options;
code = pcre2_compile(nonnull_input(pattern), pattern_length, options,
&error_code, &error_offset, NULL);
if (code == NULL) {
result->status = error_code;
result->error_offset = bounded_offset(error_offset);
return error_code;
}
if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0 ||
pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0) {
pcre2_code_free(code);
return fail_compile(result, REGEX_PCRE2_ERROR_CONFIG);
}
result->capture_count = capture_count;
result->name_count = name_count;
pcre2_code_free(code);
return 0;
}
int32_t regex_pcre2_execute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
regex_pcre2_run_result *result) {
regex_pcre2_run_buffers buffers = {
records, record_capacity, name_records, name_record_capacity,
name_bytes, name_bytes_capacity, NULL, 0};
return run_bounded(pattern, pattern_length, subject, subject_length, NULL, 0,
application_flags, limits, &buffers, 0, result);
}
int32_t regex_pcre2_substitute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
const uint8_t *replacement,
uint32_t replacement_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
uint8_t *output,
uint32_t output_capacity,
regex_pcre2_run_result *result) {
regex_pcre2_run_buffers buffers = {
records, record_capacity, name_records, name_record_capacity,
name_bytes, name_bytes_capacity, output, output_capacity};
return run_bounded(pattern, pattern_length, subject, subject_length,
replacement, replacement_length, application_flags,
limits, &buffers, 1, result);
}
int32_t regex_pcre2_trace(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_trace_limits *limits,
regex_pcre2_trace_event *events,
uint32_t event_capacity,
uint8_t *mark_bytes,
uint32_t mark_bytes_capacity,
regex_pcre2_trace_result *result) {
pcre2_code *code = NULL;
pcre2_match_data *match_data = NULL;
pcre2_match_context *match_context = NULL;
regex_pcre2_trace_state state;
int compile_error = 0;
PCRE2_SIZE compile_offset = 0;
uint32_t capture_count = 0;
uint32_t options = 0;
int32_t match_status = 0;
int32_t status = 0;
if (result == NULL || !input_pointer_valid(pattern, pattern_length) ||
!input_pointer_valid(subject, subject_length) || events == NULL ||
event_capacity == 0 ||
(mark_bytes == NULL && mark_bytes_capacity != 0)) {
return fail_trace(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
memset(result, 0, sizeof(*result));
result->last_complete_event = UINT32_MAX;
if ((application_flags & ~REGEX_PCRE2_ALL_FLAGS) != 0) {
return fail_trace(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) {
return fail_trace(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (subject_length > REGEX_PCRE2_MAX_SUBJECT_BYTES) {
return fail_trace(result, REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (!trace_limits_valid(limits) ||
event_capacity < limits->maximum_events ||
event_capacity > REGEX_PCRE2_MAX_TRACE_EVENTS ||
mark_bytes_capacity > REGEX_PCRE2_MAX_TRACE_BYTES) {
return fail_trace(result, REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
options = to_pcre2_options(application_flags) | PCRE2_AUTO_CALLOUT;
result->effective_options = options;
code = pcre2_compile(nonnull_input(pattern), pattern_length, options,
&compile_error, &compile_offset, NULL);
if (code == NULL) {
status = compile_error;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_COMPILE;
result->error_offset = bounded_offset(compile_offset);
goto cleanup;
}
if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
if (capture_count > REGEX_PCRE2_MAX_CAPTURE_GROUPS) {
status = REGEX_PCRE2_ERROR_CAPTURE_LIMIT;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
match_data = pcre2_match_data_create_from_pattern(code, NULL);
match_context = pcre2_match_context_create(NULL);
if (match_data == NULL || match_context == NULL) {
status = PCRE2_ERROR_NOMEMORY;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
memset(&state, 0, sizeof(state));
state.events = events;
state.event_capacity = event_capacity;
state.mark_bytes = mark_bytes;
state.mark_bytes_capacity = mark_bytes_capacity;
state.maximum_events = limits->maximum_events;
state.maximum_trace_bytes = limits->maximum_trace_bytes;
status = configure_match_context_values(
match_context, limits->match_limit, limits->depth_limit,
limits->heap_limit_kib);
if (status != 0 ||
pcre2_set_callout(match_context, trace_callout, &state) != 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
/*
* The global flag is intentionally application-level for normal execution.
* A trace represents one exact pcre2_match() invocation.
*/
match_status = pcre2_match(code, nonnull_input(subject), subject_length, 0, 0,
match_data, match_context);
result->native_match_status = match_status;
result->event_count = state.event_count;
result->total_event_count = state.total_event_count;
result->mark_bytes_length = state.mark_bytes_length;
result->trace_bytes_length =
state.event_count * (uint32_t)sizeof(regex_pcre2_trace_event) +
state.mark_bytes_length;
result->events_truncated = state.events_truncated;
result->marks_truncated = state.marks_truncated;
result->last_complete_event =
state.event_count == 0 ? UINT32_MAX : state.event_count - 1;
if (match_status >= 0) {
result->matched = 1;
} else if (match_status == PCRE2_ERROR_NOMATCH ||
(match_status == PCRE2_ERROR_CALLOUT &&
state.events_truncated != 0)) {
status = 0;
} else {
status = match_status;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_MATCH;
if (state.event_count > 0) {
result->error_offset =
events[state.event_count - 1].subject_position_byte;
}
}
cleanup:
if (match_context != NULL) {
pcre2_match_context_free(match_context);
}
if (match_data != NULL) {
pcre2_match_data_free(match_data);
}
if (code != NULL) {
pcre2_code_free(code);
}
return status;
}
int32_t regex_pcre2_self_test(void) {
static const uint8_t pattern[] = "(?<word>\\p{L}+)-(?<number>\\d+)";
static const uint8_t subject[] = "x Grüße-42 y";
static const uint8_t replacement[] = "$<number>:$<word>";
static const uint8_t trace_pattern[] = "a+b";
static const uint8_t trace_subject[] = "aaab";
regex_pcre2_compile_result compile_result;
regex_pcre2_run_result run_result;
regex_pcre2_trace_result trace_result;
regex_pcre2_limits limits = {10, 100, 1000000, 1000, 32768};
regex_pcre2_trace_limits trace_limits = {100, 4096, 1000000, 1000,
32768};
regex_pcre2_match_record records[3];
regex_pcre2_name_record names[2];
regex_pcre2_trace_event trace_events[100];
uint8_t name_bytes[64];
uint8_t trace_marks[64];
uint8_t output[65];
int32_t status = regex_pcre2_compile_probe(
pattern, (uint32_t)(sizeof(pattern) - 1),
REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &compile_result);
if (status != 0 || compile_result.capture_count != 2 ||
compile_result.name_count != 2) {
return 1;
}
status = regex_pcre2_substitute(
pattern, (uint32_t)(sizeof(pattern) - 1), subject,
(uint32_t)(sizeof(subject) - 1), replacement,
(uint32_t)(sizeof(replacement) - 1),
REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &limits, records, 3, names, 2,
name_bytes, sizeof(name_bytes), output, sizeof(output) - 1, &run_result);
if (status != 0 || run_result.match_count != 1 ||
run_result.record_count != 3 || run_result.substitution_count != 1 ||
run_result.output_truncated != 0 ||
run_result.output_length != sizeof("x 42:Grüße y") - 1 ||
memcmp(output, "x 42:Grüße y", sizeof("x 42:Grüße y") - 1) != 0) {
return 2;
}
status = regex_pcre2_trace(
trace_pattern, (uint32_t)(sizeof(trace_pattern) - 1), trace_subject,
(uint32_t)(sizeof(trace_subject) - 1),
REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &trace_limits, trace_events, 100,
trace_marks, sizeof(trace_marks), &trace_result);
if (status != 0 || trace_result.status != 0 ||
trace_result.native_match_status <= 0 || trace_result.matched != 1 ||
trace_result.event_count == 0 || trace_result.events_truncated != 0 ||
trace_result.last_complete_event != trace_result.event_count - 1) {
return 3;
}
if (regex_pcre2_bridge_abi_version() != REGEX_PCRE2_BRIDGE_ABI_VERSION ||
regex_pcre2_config_flags() != REGEX_PCRE2_CONFIG_UNICODE) {
return 4;
}
if (strcmp((const char *)regex_pcre2_version(), "10.47 2025-10-21") != 0) {
return 5;
}
return 0;
}

View File

@@ -0,0 +1,236 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef REGEX_TOOLS_PCRE2_BRIDGE_H
#define REGEX_TOOLS_PCRE2_BRIDGE_H
#include <stdint.h>
#define REGEX_PCRE2_BRIDGE_ABI_VERSION 3u
#define REGEX_PCRE2_MAX_PATTERN_BYTES 1048576u
#define REGEX_PCRE2_MAX_SUBJECT_BYTES 16777216u
#define REGEX_PCRE2_MAX_REPLACEMENT_BYTES 262144u
#define REGEX_PCRE2_MAX_MATCHES 10000u
#define REGEX_PCRE2_MAX_CAPTURE_ROWS 100000u
#define REGEX_PCRE2_MAX_CAPTURE_GROUPS 1000u
#define REGEX_PCRE2_MAX_MATCH_LIMIT 100000000u
#define REGEX_PCRE2_MAX_DEPTH_LIMIT 100000u
#define REGEX_PCRE2_MAX_HEAP_LIMIT_KIB 131072u
#define REGEX_PCRE2_MAX_TRACE_EVENTS 50000u
#define REGEX_PCRE2_MAX_TRACE_BYTES 10485760u
#define REGEX_PCRE2_MAX_TRACE_MARK_BYTES 1024u
#define REGEX_PCRE2_UNSET_OFFSET UINT32_MAX
enum regex_pcre2_flag {
REGEX_PCRE2_CASELESS = 1u << 0,
REGEX_PCRE2_MULTILINE = 1u << 1,
REGEX_PCRE2_DOTALL = 1u << 2,
REGEX_PCRE2_EXTENDED = 1u << 3,
REGEX_PCRE2_UNGREEDY = 1u << 4,
REGEX_PCRE2_UTF = 1u << 5,
REGEX_PCRE2_UCP = 1u << 6,
REGEX_PCRE2_DUPNAMES = 1u << 7,
REGEX_PCRE2_GLOBAL = 1u << 8
};
enum regex_pcre2_config_flag {
REGEX_PCRE2_CONFIG_UNICODE = 1u << 0,
REGEX_PCRE2_CONFIG_JIT = 1u << 1
};
enum regex_pcre2_bridge_error {
REGEX_PCRE2_ERROR_INVALID_ARGUMENT = -10001,
REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS = -10002,
REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE = -10003,
REGEX_PCRE2_ERROR_CONFIG = -10004,
REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE = -10005,
REGEX_PCRE2_ERROR_REPLACEMENT_TOO_LARGE = -10006,
REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE = -10007,
REGEX_PCRE2_ERROR_CAPTURE_LIMIT = -10008,
REGEX_PCRE2_ERROR_OUTPUT_BUFFER = -10009
};
enum regex_pcre2_error_phase {
REGEX_PCRE2_PHASE_NONE = 0,
REGEX_PCRE2_PHASE_BRIDGE = 1,
REGEX_PCRE2_PHASE_COMPILE = 2,
REGEX_PCRE2_PHASE_MATCH = 3,
REGEX_PCRE2_PHASE_SUBSTITUTE = 4
};
/*
* This 20-byte, five-u32 record is retained for deterministic compile-only
* build verification. Runtime callers should use the bounded APIs below.
*/
typedef struct regex_pcre2_compile_result {
int32_t status;
uint32_t error_offset;
uint32_t capture_count;
uint32_t name_count;
uint32_t effective_options;
} regex_pcre2_compile_result;
/* All values are required and validated against the bridge hard maxima. */
typedef struct regex_pcre2_limits {
uint32_t maximum_matches;
uint32_t maximum_capture_rows;
uint32_t match_limit;
uint32_t depth_limit;
uint32_t heap_limit_kib;
} regex_pcre2_limits;
/*
* One record is emitted for group zero and every capture group of each
* retained match. Unset captures use REGEX_PCRE2_UNSET_OFFSET for both bounds.
*/
typedef struct regex_pcre2_match_record {
uint32_t match_number;
uint32_t group_number;
uint32_t start_byte;
uint32_t end_byte;
} regex_pcre2_match_record;
/* Names are UTF-8 slices into the caller-owned name byte buffer. */
typedef struct regex_pcre2_name_record {
uint32_t group_number;
uint32_t name_offset;
uint32_t name_length;
} regex_pcre2_name_record;
/*
* Trace collection has an independent bound from match materialization. The
* byte cap covers the fixed event records plus copied mark bytes.
*/
typedef struct regex_pcre2_trace_limits {
uint32_t maximum_events;
uint32_t maximum_trace_bytes;
uint32_t match_limit;
uint32_t depth_limit;
uint32_t heap_limit_kib;
} regex_pcre2_trace_limits;
/*
* Every field except the mark slice is copied directly from a PCRE2 callout
* block. Positions and lengths are offsets in the 8-bit UTF-8 pattern or
* subject supplied to the engine.
*/
typedef struct regex_pcre2_trace_event {
uint32_t callout_number;
uint32_t pattern_position_byte;
uint32_t next_item_length_byte;
uint32_t subject_position_byte;
uint32_t capture_top;
uint32_t capture_last;
uint32_t mark_offset;
uint32_t mark_length;
} regex_pcre2_trace_event;
/*
* A zero status means that a bounded trace was returned. native_match_status
* retains pcre2_match()'s exact result, including PCRE2_ERROR_CALLOUT when the
* bridge deliberately stopped at a trace cap. last_complete_event is
* UINT32_MAX when no complete event was copied.
*/
typedef struct regex_pcre2_trace_result {
int32_t status;
uint32_t error_phase;
uint32_t error_offset;
int32_t native_match_status;
uint32_t effective_options;
uint32_t event_count;
uint32_t total_event_count;
uint32_t mark_bytes_length;
uint32_t trace_bytes_length;
uint32_t events_truncated;
uint32_t marks_truncated;
uint32_t last_complete_event;
uint32_t matched;
} regex_pcre2_trace_result;
/*
* This fixed 60-byte record contains no native pointers. `status` is zero on
* success. Otherwise `error_phase` distinguishes bridge validation, compile,
* match and replacement errors; `error_offset` is a UTF-8 byte offset when the
* phase provides one.
*/
typedef struct regex_pcre2_run_result {
int32_t status;
uint32_t error_phase;
uint32_t error_offset;
uint32_t capture_count;
uint32_t name_count;
uint32_t match_count;
uint32_t record_count;
uint32_t name_record_count;
uint32_t name_bytes_length;
uint32_t effective_options;
uint32_t results_truncated;
uint32_t names_truncated;
uint32_t output_length;
uint32_t substitution_count;
uint32_t output_truncated;
} regex_pcre2_run_result;
uint32_t regex_pcre2_bridge_abi_version(void);
uint32_t regex_pcre2_config_flags(void);
const uint8_t *regex_pcre2_version(void);
int32_t regex_pcre2_error_message(int32_t error_code,
uint8_t *output,
uint32_t output_capacity);
int32_t regex_pcre2_compile_probe(const uint8_t *pattern,
uint32_t pattern_length,
uint32_t application_flags,
regex_pcre2_compile_result *result);
int32_t regex_pcre2_execute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
regex_pcre2_run_result *result);
int32_t regex_pcre2_substitute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
const uint8_t *replacement,
uint32_t replacement_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
/* `output` must have output_capacity + 1 writable bytes for PCRE2's NUL. */
uint8_t *output,
uint32_t output_capacity,
regex_pcre2_run_result *result);
int32_t regex_pcre2_trace(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_trace_limits *limits,
regex_pcre2_trace_event *events,
uint32_t event_capacity,
uint8_t *mark_bytes,
uint32_t mark_bytes_capacity,
regex_pcre2_trace_result *result);
int32_t regex_pcre2_self_test(void);
#endif

277
engines/python/bridge.py Normal file
View File

@@ -0,0 +1,277 @@
"""Fixed, bounded CPython ``re`` bridge used by the Python engine worker.
This file is bundled as source by Vite and evaluated once while the trusted
Pyodide runtime starts. User-controlled values are passed as function
arguments; they are never evaluated as Python source.
"""
import json
import re
import sys
_BRIDGE_ABI_VERSION = 1
_MAX_CAPTURE_GROUPS = 1_000
_MAX_NATIVE_EXPANSION_CODE_POINTS = 16 * 1024 * 1024
def _json_result(value):
return json.dumps(value, ensure_ascii=True, separators=(",", ":"))
def _error(phase, code, error, position=None):
message = str(error)
if len(message) > 1_024:
message = message[:1_023] + "\N{HORIZONTAL ELLIPSIS}"
result = {
"abi": _BRIDGE_ABI_VERSION,
"ok": False,
"phase": phase,
"code": code,
"message": message,
}
if isinstance(position, int) and position >= 0:
result["position"] = position
return _json_result(result)
def _flag_bits(flags):
bits = 0
if "a" in flags:
bits |= re.ASCII
if "i" in flags:
bits |= re.IGNORECASE
if "m" in flags:
bits |= re.MULTILINE
if "s" in flags:
bits |= re.DOTALL
if "x" in flags:
bits |= re.VERBOSE
return bits
def _match_record(match, group_count):
captures = []
for group_number in range(1, group_count + 1):
start, end = match.span(group_number)
captures.append(None if start < 0 else [start, end])
start, end = match.span(0)
return {"span": [start, end], "captures": captures}
def _utf8_width(character):
code_point = ord(character)
if code_point <= 0x7F:
return 1
if code_point <= 0x7FF:
return 2
if code_point <= 0xFFFF:
return 3
return 4
def _append_utf8_bounded(chunks, value, byte_count, maximum_bytes):
remaining = maximum_bytes - byte_count
if not value:
return byte_count, True
end = 0
for character in value:
width = _utf8_width(character)
if width > remaining:
break
remaining -= width
byte_count += width
end += 1
if end:
chunks.append(value[:end])
return byte_count, end == len(value)
def _expansion_code_points(parsed_template, match):
total = 0
for part in parsed_template:
if isinstance(part, int):
start, end = match.span(part)
if start >= 0:
total += end - start
else:
total += len(part)
if total > _MAX_NATIVE_EXPANSION_CODE_POINTS:
return total
return total
def _identity():
return _json_result(
{
"abi": _BRIDGE_ABI_VERSION,
"ok": True,
"identity": {
"implementation": sys.implementation.name,
"pythonVersion": ".".join(
str(part) for part in sys.version_info[:3]
),
"reModule": re.__name__,
},
}
)
def _regex_tools_run(
operation,
pattern,
flags,
subject,
maximum_matches,
maximum_capture_rows,
replacement,
maximum_output_bytes,
):
if operation == "identity":
return _identity()
if operation not in ("execute", "replace"):
return _error("bridge", "invalid-operation", "Unsupported bridge operation.")
try:
compiled = re.compile(pattern, _flag_bits(flags))
except re.PatternError as error:
return _error(
"compile",
"compile-error",
error,
getattr(error, "pos", None),
)
except Exception as error:
return _error("compile", "compile-error", error)
group_count = compiled.groups
if group_count > _MAX_CAPTURE_GROUPS:
return _error(
"compile",
"capture-group-limit",
(
f"The pattern declares {group_count} capture groups; "
f"the bridge limit is {_MAX_CAPTURE_GROUPS}."
),
)
group_names = sorted(
([group_number, name] for name, group_number in compiled.groupindex.items()),
key=lambda item: item[0],
)
parsed_template = None
if operation == "replace":
try:
parsed_template = re._parser.parse_template(replacement, compiled)
except (re.PatternError, IndexError) as error:
return _error(
"replacement",
"replacement-error",
error,
getattr(error, "pos", None),
)
except Exception as error:
return _error("replacement", "replacement-error", error)
iterate = "g" in flags
try:
matches = compiled.finditer(subject) if iterate else (
candidate for candidate in (compiled.search(subject),) if candidate
)
records = []
capture_rows = 0
results_truncated = False
output_chunks = []
output_bytes = 0
output_truncated = False
output_stopped = False
next_source_position = 0
for match in matches:
rows_for_match = max(1, group_count)
if (
len(records) >= maximum_matches
or capture_rows + rows_for_match > maximum_capture_rows
):
results_truncated = True
break
records.append(_match_record(match, group_count))
capture_rows += rows_for_match
if operation == "replace" and not output_stopped:
start, end = match.span(0)
output_bytes, complete = _append_utf8_bounded(
output_chunks,
subject[next_source_position:start],
output_bytes,
maximum_output_bytes,
)
if not complete:
output_truncated = True
output_stopped = True
else:
expansion_size = _expansion_code_points(
parsed_template, match
)
if expansion_size > _MAX_NATIVE_EXPANSION_CODE_POINTS:
return _error(
"replacement",
"replacement-expansion-limit",
(
"One native match expansion exceeds the "
f"{_MAX_NATIVE_EXPANSION_CODE_POINTS} "
"code-point safety limit."
),
)
expanded = match.expand(replacement)
output_bytes, complete = _append_utf8_bounded(
output_chunks,
expanded,
output_bytes,
maximum_output_bytes,
)
if not complete:
output_truncated = True
output_stopped = True
next_source_position = end
if operation == "replace" and not output_stopped:
output_bytes, complete = _append_utf8_bounded(
output_chunks,
subject[next_source_position:],
output_bytes,
maximum_output_bytes,
)
if not complete:
output_truncated = True
result = {
"abi": _BRIDGE_ABI_VERSION,
"ok": True,
"groupCount": group_count,
"groupNames": group_names,
"matches": records,
"resultsTruncated": results_truncated,
}
if operation == "replace":
result.update(
{
"output": "".join(output_chunks),
"outputBytes": output_bytes,
"outputTruncated": output_truncated,
}
)
return _json_result(result)
except re.PatternError as error:
phase = "replacement" if operation == "replace" else "match"
code = "replacement-error" if operation == "replace" else "match-error"
return _error(phase, code, error, getattr(error, "pos", None))
except Exception as error:
phase = "replacement" if operation == "replace" else "match"
code = "replacement-error" if operation == "replace" else "match-error"
return _error(phase, code, error)
_regex_tools_run

View File

@@ -12,6 +12,7 @@ export default tseslint.config(
"coverage", "coverage",
"test-results", "test-results",
"playwright-report", "playwright-report",
".engine-build",
], ],
}, },
{ {

51
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "regex-tools", "name": "regex-tools",
"version": "0.1.0", "version": "0.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "regex-tools", "name": "regex-tools",
"version": "0.1.0", "version": "0.3.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "0.2.2", "@add-ideas/toolbox-contract": "0.2.2",
@@ -18,6 +18,7 @@
"@codemirror/view": "6.43.6", "@codemirror/view": "6.43.6",
"@eslint-community/regexpp": "4.12.2", "@eslint-community/regexpp": "4.12.2",
"@lezer/highlight": "1.2.3", "@lezer/highlight": "1.2.3",
"fflate": "0.8.3",
"react": "19.2.7", "react": "19.2.7",
"react-dom": "19.2.7" "react-dom": "19.2.7"
}, },
@@ -35,10 +36,10 @@
"eslint": "10.7.0", "eslint": "10.7.0",
"eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-react-refresh": "0.5.2", "eslint-plugin-react-refresh": "0.5.2",
"fflate": "0.8.3",
"globals": "17.7.0", "globals": "17.7.0",
"jsdom": "29.1.1", "jsdom": "29.1.1",
"prettier": "3.9.5", "prettier": "3.9.5",
"pyodide": "314.0.3",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.64.0", "typescript-eslint": "8.64.0",
"vite": "8.1.5", "vite": "8.1.5",
@@ -1385,6 +1386,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/emscripten": {
"version": "1.41.5",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/esrecurse": { "node_modules/@types/esrecurse": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -2442,7 +2450,6 @@
"version": "0.8.3", "version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
@@ -3406,6 +3413,20 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pyodide": {
"version": "314.0.3",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-314.0.3.tgz",
"integrity": "sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"@types/emscripten": "^1.41.4",
"ws": "^8.5.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "19.2.7", "version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
@@ -4102,6 +4123,28 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ws": {
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": { "node_modules/xml-name-validator": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "regex-tools", "name": "regex-tools",
"version": "0.1.0", "version": "0.3.0",
"description": "Develop, explain, test and apply regular expressions locally in the browser.", "description": "Develop, explain, test and apply regular expressions locally in the browser.",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"author": "Albrecht Degering", "author": "Albrecht Degering",
@@ -32,6 +32,15 @@
"test:conformance": "vitest run tests/conformance", "test:conformance": "vitest run tests/conformance",
"test:browser": "playwright test", "test:browser": "playwright test",
"engines:build": "node scripts/build-engines.mjs", "engines:build": "node scripts/build-engines.mjs",
"engines:pcre2:build": "node scripts/build-engines.mjs --pcre2",
"engines:pcre2:install": "node scripts/install-pcre2-engine.mjs",
"engines:pcre2:verify": "node scripts/verify-engine-assets.mjs --pcre2-pack .engine-build/pcre2",
"engines:python:build": "node scripts/python-engine-pack.mjs",
"engines:python:install": "node scripts/install-python-engine.mjs",
"engines:python:verify": "node scripts/verify-engine-assets.mjs --python-pack public/engines/python",
"engines:java:build": "node scripts/java-engine-pack.mjs",
"engines:java:install": "node scripts/install-java-engine.mjs",
"engines:java:verify": "node scripts/verify-engine-assets.mjs --java-pack public/engines/java",
"engines:verify": "node scripts/verify-engine-assets.mjs", "engines:verify": "node scripts/verify-engine-assets.mjs",
"manifest:generate": "node scripts/generate-toolbox-manifest.mjs", "manifest:generate": "node scripts/generate-toolbox-manifest.mjs",
"manifest:check": "node scripts/generate-toolbox-manifest.mjs --check", "manifest:check": "node scripts/generate-toolbox-manifest.mjs --check",
@@ -51,6 +60,7 @@
"@codemirror/view": "6.43.6", "@codemirror/view": "6.43.6",
"@eslint-community/regexpp": "4.12.2", "@eslint-community/regexpp": "4.12.2",
"@lezer/highlight": "1.2.3", "@lezer/highlight": "1.2.3",
"fflate": "0.8.3",
"react": "19.2.7", "react": "19.2.7",
"react-dom": "19.2.7" "react-dom": "19.2.7"
}, },
@@ -68,10 +78,10 @@
"eslint": "10.7.0", "eslint": "10.7.0",
"eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-react-refresh": "0.5.2", "eslint-plugin-react-refresh": "0.5.2",
"fflate": "0.8.3",
"globals": "17.7.0", "globals": "17.7.0",
"jsdom": "29.1.1", "jsdom": "29.1.1",
"prettier": "3.9.5", "prettier": "3.9.5",
"pyodide": "314.0.3",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.64.0", "typescript-eslint": "8.64.0",
"vite": "8.1.5", "vite": "8.1.5",

View File

@@ -1,7 +1,13 @@
# Engine assets # Engine assets
Regex Tools 0.1.0 executes ECMAScript through the browser's native `RegExp` Regex Tools executes ECMAScript through the browser's native `RegExp` engine.
engine. It therefore ships no external engine binary or WebAssembly pack. It also ships three declared, self-hosted runtime packs:
Future flavour packs will be built from pinned, documented open-source - `pcre2/`: official PCRE2 10.47, 8-bit WebAssembly;
revisions and placed in versioned subdirectories here. - `python/`: Pyodide 314.0.3 with CPython 3.14.2 `re`; and
- `java/`: TeaVM 0.15.0's `java.util.regex` class library compiled to an
ES2015 module. This module is not OpenJDK.
The packs come from pinned, documented open-source inputs. Runtime files travel
with deterministic metadata, closed-set checksums and the applicable upstream
licence/notice material, all of which are verified by the release gate.

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,11 @@
This product includes software developed by Alexey Andreev
(http://teavm.org).
This product includes software developed by The Apache Software
Foundation (http://www.apache.org/).
=============================================================================
= NOTICE file corresponding to section 4d of the Apache License Version 2.0 =
=============================================================================
This product includes software developed by
Joda.org (http://www.joda.org/).

View File

@@ -0,0 +1,4 @@
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE.txt
71bbe79ff78d0e5e64096acdd418589abfe886b8f827048dc655df438a57e477 NOTICE.txt
719cb99761c8f0964835bf7b954fcf6e0df30ec14d2c06050c07c23a978d6d22 engine-metadata.json
fd7974e7bde911ef0682cdc432d28e93c5db9d56f7268482a62e40a2ad86d62f java-regex.mjs

View File

@@ -0,0 +1,118 @@
{
"schemaVersion": 1,
"engine": "java",
"engineName": "TeaVM java.util.regex",
"engineVersion": "0.15.0",
"semanticIdentity": "TeaVM 0.15.0 classlib java.util.regex; Apache Harmony-derived; not OpenJDK",
"status": "production-compatibility-runtime",
"bridge": {
"abiVersion": 1,
"maximumPatternUtf16": 65536,
"maximumSubjectBytes": 16777216,
"maximumReplacementUtf16": 65536,
"maximumMatches": 10000,
"maximumCaptureRows": 100000,
"maximumCaptureGroups": 1000,
"maximumOutputBytes": 67108864,
"offsetUnit": "utf16",
"exports": [
"bridgeAbiVersion",
"engineIdentity",
"execute",
"replace",
"selfTest"
],
"sourceFiles": [
{
"path": "engines/java/pom.xml",
"sha256": "8f70247241da0e1a1db9eb64a604206a69eb6a573b5634f1faa196fa9bb07b15",
"bytes": 2995
},
{
"path": "engines/java/README.md",
"sha256": "7ce93387533e889221179515b1c36c67fd54f490edd2d7624c4ee5744bf42cf1",
"bytes": 1309
},
{
"path": "engines/java/LICENSE-TeaVM.txt",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"bytes": 11358
},
{
"path": "engines/java/NOTICE-TeaVM.txt",
"sha256": "71bbe79ff78d0e5e64096acdd418589abfe886b8f827048dc655df438a57e477",
"bytes": 493
},
{
"path": "engines/java/src/main/java/de/addideas/regextools/javaengine/RegexBridge.java",
"sha256": "8512906fa000cc8ef10f5c8d7e01b7037bbfca24882e834d2de5a706bc1e1cd5",
"bytes": 12469
}
]
},
"source": {
"repository": "https://github.com/konsoletyper/teavm.git",
"tag": "0.15.0",
"commitSha1": "ee91b03e616c4b45401cd11fb0cd7eb0daf6649b",
"treeSha1": "1cdc5c332d5809828c8952f96d73f9e7504a0cec",
"license": "Apache-2.0",
"upstreamLicenseSha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"upstreamNoticeSha256": "6c2b3372a6beee7fbaad482248ac0f5ad2b099d92ff2a528c04261cc9c0636f4"
},
"toolchain": {
"mavenVersion": "3.9.15",
"javaVersion": "25.0.3",
"teaVmArtifacts": [
{
"coordinate": "org.teavm:teavm-classlib:0.15.0",
"sha256": "7d805be4670a892a34c89d9d750a8afd65e2f8201b7b3a10343634ea19d1c410"
},
{
"coordinate": "org.teavm:teavm-jso:0.15.0",
"sha256": "58b337b93924106bf819c45890c7c231a0c740145afe77795f1e0f099f621d71"
},
{
"coordinate": "org.teavm:teavm-maven-plugin:0.15.0",
"sha256": "1133e16e45f1fd3cb93129fd96d142e1f5ef3608891d83b2273442d5446a3879"
}
]
},
"configuration": {
"target": "JavaScript ES2015 module",
"minified": true,
"optimizationLevel": "ADVANCED",
"sourceMaps": false,
"filesystem": false,
"supportedApplicationFlags": [
"g",
"i",
"d",
"m",
"s",
"u",
"x",
"U"
],
"unixLines": "d maps to TeaVM Pattern.UNIX_LINES.",
"unicodeCharacterClassCompatibility": "TeaVM 0.15.0 lacks OpenJDK UNICODE_CHARACTER_CLASS; U implies Unicode case folding only.",
"replacement": "TeaVM 0.15.0 Matcher appendReplacement/appendTail semantics: only single-digit $n references; ${name} is rejected; not OpenJDK Matcher parity."
},
"sourceDateEpoch": 1780759630,
"files": [
{
"path": "LICENSE.txt",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"bytes": 11358
},
{
"path": "NOTICE.txt",
"sha256": "71bbe79ff78d0e5e64096acdd418589abfe886b8f827048dc655df438a57e477",
"bytes": 493
},
{
"path": "java-regex.mjs",
"sha256": "fd7974e7bde911ef0682cdc432d28e93c5db9d56f7268482a62e40a2ad86d62f",
"bytes": 136962
}
]
}

View File

@@ -0,0 +1,441 @@
"use strict";
let Js=2463534242,OY=()=>{let x=Js;x^=x<<13;x^=x>>>17;x^=x<<5;Js=x;return x;},M=f=>function(){return f(this);},O=f=>function(p1){return f(this,p1);},Bq=f=>function(p1,p2){return f(this,p1,p2);},S=f=>function(p1,p2,p3){return f(this,p1,p2,p3);},Bz=f=>function(p1,p2,p3,p4){return f(this,p1,p2,p3,p4);},JT=f=>(args,callback)=>{if(!args){args=[];}let javaArgs=K(CH(),args.length);for(let i=0;i<args.length;++i){javaArgs.data[i]=Bt(args[i]);}M7(()=>{f.call(null,javaArgs);},callback);},B3=target=>target.$clinit=()=>
{},RB=obj=>{let cls=obj.constructor;let arrayDegree=0;while(cls[Bc]&&cls[Bc].item){++arrayDegree;cls=cls[Bc].item;}let clsName="";if(cls[Bc].primitiveKind!==0){clsName=cls[Bc].name;}else {clsName=cls[Bc]?cls[Bc].name||"a/"+cls.name:"@"+cls.name;}while(arrayDegree-->0){clsName+="[]";}return clsName;},L=superclass=>{if(superclass===0){return function(){};}if(superclass===void 0){superclass=CH();}return function(){superclass.call(this);};},Bc=Symbol("teavm_meta"),B9=cls=>{if(cls[Bc].classObject===null){cls[Bc].classObject
=K1(cls);}return cls[Bc].classObject;},CH=()=>J,Fl=source=>{return Object.assign({name:null,binaryName:null,parent:null,superinterfaces:[],modifiers:0,primitiveKind:0,itemType:null,arrayType:null,enclosingClass:null,declaringClass:null,simpleName:null,clinit:()=>{},constructor:null,enumConstants:()=>null,resolvedEnumConstants:null,reflection:null,classObject:null,assignableCache:null,valueToObject:o=>o,objectToValue:o=>o},source||{});},Dl=(name,binaryName,kind,config)=>{let cls=()=>{};let meta=Fl({name:name,
binaryName:binaryName,modifiers:1|1<<4,primitiveKind:kind});cls[Bc]=meta;if(typeof config==='function'){config(meta);}return cls;},JO=Dl("byte","B",2,meta=>{}),HC=Dl("char","C",4,meta=>{}),Gs=Dl("int","I",5,meta=>{}),N$=Dl("void","V",9),B1=(a,b)=>a===b?0:a<b? -1:1,DF=Math.imul||function(a,b){let ah=a>>>16&0xFFFF;let al=a&0xFFFF;let bh=b>>>16&0xFFFF;let bl=b&0xFFFF;return al*bl+(ah*bl+al*bh<<16>>>0)|0;},EP=(a,b)=>(a>>>0)/(b>>>0)>>>0,RG=(a,b)=>(a>>>0)%(b>>>0)>>>0,Es=(a,b)=>{a>>>=0;b>>>=0;return a<b? -1:a>b?1:
0;},RN=BigInt(0),BM=val=>BigInt.asIntN(64,BigInt(val|0)),JK=(a,b)=>a>b,FW=(a,b)=>a<=b,Cv=(a,b)=>BigInt.asIntN(64,a+b),HU=(a,b)=>BigInt.asIntN(64,a -b),K=(cls,sz)=>{let data=new Array(sz);data.fill(null);return new (CG(cls))(data);},BJ=sz=>new Pn(new Uint16Array(sz)),I0=sz=>new Pf(new Int8Array(sz)),BC=sz=>new Iu(new Int32Array(sz)),HV=data=>{let buffer=new Int32Array(data.length);buffer.set(data);return new Iu(buffer);},CG=cls=>{let result=cls[Bc].arrayType;if(result===null){function JavaArray(data){(CH()).call(this);this.data
=data;}JavaArray.prototype=Object.create((CH()).prototype);JavaArray.prototype.type=cls;JavaArray.prototype.constructor=JavaArray;JavaArray.prototype.toString=function(){let str="[";for(let i=0;i<this.data.length;++i){if(i>0){str+=", ";}str+=this.data[i].toString();}str+="]";return str;};JavaArray.prototype.ix=function(){let dataCopy;if('slice' in this.data){dataCopy=this.data.slice();}else {dataCopy=new this.data.constructor(this.data.length);for(let i=0;i<dataCopy.length;++i){dataCopy[i]=this.data[i];}}return new (CG(this.type))(dataCopy);};let name
="["+cls[Bc].binaryName;JavaArray[Bc]=Fl({name:name,binaryName:name,parent:CH(),itemType:cls});result=JavaArray;cls[Bc].arrayType=JavaArray;}return result;};
function Qn(array){return array.data.length;}
let E4,K$=strings=>{P2();E4=new Array(strings.length);for(let i=0;i<strings.length;++i){E4[i]=Gd(Bt(strings[i]));}},I=index=>E4[index],Hh=(array,offset,count)=>{let result="";let limit=offset+count;for(let i=offset;i<limit;i=i+1024|0){let next=Math.min(limit,i+1024|0);result+=String.fromCharCode.apply(null,array.subarray(i,next));}return result;},Bt=str=>str===null?null:HS(str),D$=str=>str===null?null:str.eh,P2=()=>Ce(),Gd;
{Gd=str=>str;}let Di=(from,to)=>{if(from===to){return true;}let map=from[Bc].assignableCache;if(map===null){map=new Map();from[Bc].assignableCache=map;}let cachedResult=map.get(to);if(typeof cachedResult!=='undefined'){return cachedResult;}if(to[Bc].itemType!==null){let result=from[Bc].itemType!==null&&Di(from[Bc].itemType,to[Bc].itemType);map.set(to,result);return result;}let parent=from[Bc].parent;if(parent!==null&&parent!==from){if(Di(parent,to)){map.set(to,true);return true;}}let superinterfaces=from[Bc].superinterfaces;for
(let i=0;i<superinterfaces.length;i=i+1|0){if(Di(superinterfaces[i],to)){map.set(to,true);return true;}}map.set(to,false);return false;},Q=ex=>{throw LG(ex);},Cw=Symbol("javaException"),LG=ex=>{if(!ex.$jsException){G0(ex);}return ex.$jsException;},G0=ex=>{let javaCause=R7(ex);let jsCause=javaCause!==null?javaCause.$jsException:void 0;let cause=typeof jsCause==="object"?{cause:jsCause}:void 0;let err=new B_("Java exception thrown",cause);if(typeof Error.captureStackTrace==="function"){Error.captureStackTrace(err);}err[Cw]
=ex;ex.$jsException=err;I7(err,ex);},I7=(err,ex)=>{if(typeof Kx==="function"&&err.stack){let stack=Kx(err.stack);let javaStack=K(KY(),stack.length);let elem;let noStack=false;for(let i=0;i<stack.length;++i){let element=stack[i];elem=Pk(Bt(element.className),Bt(element.methodName),Bt(element.fileName),element.lineNumber);if(elem==null){noStack=true;break;}javaStack.data[i]=elem;}if(!noStack){Lv(ex,javaStack);}}},B_;
if(typeof Reflect==='object'){let defaultMessage=Symbol("defaultMessage");B_=function B_(message,cause){let self=Reflect.construct(Error,[void 0,cause],B_);Object.setPrototypeOf(self,B_.prototype);self[defaultMessage]=message;return self;}
;B_.prototype=Object.create(Error.prototype,{constructor:{configurable:true,writable:true,value:B_},message:{get(){try {let javaException=this[Cw];if(typeof javaException==='object'){let javaMessage=NK(javaException);if(typeof javaMessage==="object"){return javaMessage!==null?javaMessage.toString():null;}}return this[defaultMessage];}catch(e){return "Exception occurred trying to extract Java exception message: "+e;}}}});}else {B_=Error;}let N_=e=>e instanceof Error&&typeof e[Cw]==='object'?e[Cw]:null,BS=err=>
{let ex=err[Cw];if(!ex){ex=Oy(Bt("(JavaScript) "+err.toString()));err[Cw]=ex;ex.$jsException=err;I7(err,ex);}return ex;},Oy=message=>Rd(message),NK=t=>RV(t),R7=t=>R8(t),KY=()=>CH(),Pk=(className,methodName,fileName,lineNumber)=>{{return null;}},Lv=(e,stack)=>{},FY=null,LQ=data=>{let i=0;let packages=new Array(data.length);for(let j=0;j<data.length;++j){let prefixIndex=data[i++];let prefix=prefixIndex>=0?packages[prefixIndex]:"";packages[j]=prefix+data[i++]+".";}FY=packages;},Ld=[],Cu=data=>{let packages=FY;let i
=0;while(i<data.length){let cls=data[i++];Ld.push(cls);let m=Fl();cls[Bc]=m;let className=data[i++];m.name=className!==0?className:null;if(m.name!==null){let packageIndex=data[i++];if(packageIndex>=0){m.name=packages[packageIndex]+m.name;}}m.binaryName="L"+m.name+";";let superclass=data[i++];m.parent=superclass!==0?superclass:null;m.superinterfaces=data[i++];if(m.parent){cls.prototype=Object.create(m.parent.prototype);}else {cls.prototype={};}cls.prototype.constructor=cls;m.modifiers=data[i++];m.primitiveKind
=0;let innerClassInfo=data[i++];if(innerClassInfo!==0){let enclosingClass=innerClassInfo[0];m.enclosingClass=enclosingClass!==0?enclosingClass:null;let declaringClass=innerClassInfo[1];m.declaringClass=declaringClass!==0?declaringClass:null;let simpleName=innerClassInfo[2];m.simpleName=simpleName!==0?simpleName:null;}let clinit=data[i++];m.clinit=clinit!==0?()=>{m.clinit=()=>{};clinit();}:()=>{};let virtualMethods=data[i++];if(virtualMethods!==0){for(let j=0;j<virtualMethods.length;j+=2){let name=virtualMethods[j];let func
=virtualMethods[j+1];if(typeof name==='string'){name=[name];}for(let k=0;k<name.length;++k){cls.prototype[name[k]]=func;}}}}},M7=(runner,callback)=>{let result;try {result=runner();}catch(e){result=e;}if(typeof callback!=='undefined'){callback(result);}else if(result instanceof Error){throw result;}};
function J(){this.$id$=0;}
let Co=a=>{return B9(Mw(a));},Mw=a=>{return a.constructor;},Is=a=>{let b,c,d,e,f,g,h,i;b=a;if(!b.$id$)b.$id$=OY();c=a.$id$;BN();if(!c)d=I(0);else{e=(((32-Hp(c)|0)+4|0)-1|0)/4|0;f=BJ(e);g=f.data;e=(e-1|0)*4|0;h=0;while(e>=0){i=h+1|0;g[h]=Fm((c>>>e|0)&15,16);e=e-4|0;h=i;}d=Dk(f);}b=new W;Y(b);T(T(b,I(1)),d);return X(b);},B$=L(0),DX=L(0),EY=L(0);
function Br(){J.call(this);this.fF=0;}
let N9=null,Gh=null,KS=null,Ce=()=>{Ce=B3(Br);MV();},GJ=(a,b)=>{Ce();a.eh=Hh(b.data,0,b.data.length);},Dk=a=>{let b=new Br();GJ(b,a);return b;},ML=(a,b)=>{a.eh=b;},HS=a=>{let b=new Br();ML(b,a);return b;},Jy=(a,b,c,d)=>{let e,f;Ce();e=b.data.length;if(c>=0&&d>=0&&d<=(e-c|0)){a.eh=Hh(b.data,c,d);return;}f=new Bo;Z(f);Q(f);},D3=(a,b,c)=>{let d=new Br();Jy(d,a,b,c);return d;},P=(a,b)=>{let c;if(b>=0&&b<a.eh.length)return a.eh.charCodeAt(b);c=new CJ;Z(c);Q(c);},Dz=a=>{return a.eh.length;},KN=(a,b)=>{let c;if(a.eh.length
!=b.er)return 0;c=0;while(c<a.eh.length){if(a.eh.charCodeAt(c)!=Er(b,c))return 0;c=c+1|0;}return 1;},H6=(a,b,c)=>{let d,e,f;if((c+b.eh.length|0)>a.eh.length)return 0;d=0;while(d<b.eh.length){e=P(b,d);f=c+1|0;if(e!=P(a,c))return 0;d=d+1|0;c=f;}return 1;},II=(a,b)=>{if(a===b)return 1;return H6(a,b,0);},DH=(a,b,c)=>{let d,e,f,g;d=BR(0,c);if(b<65536){e=b&65535;while(true){if(d>=a.eh.length)return (-1);if(a.eh.charCodeAt(d)==e)break;d=d+1|0;}return d;}f=Ft(b);g=EH(b);while(true){if(d>=(a.eh.length-1|0))return (-1);if
(a.eh.charCodeAt(d)==f&&a.eh.charCodeAt((d+1|0))==g)break;d=d+1|0;}return d;},C0=(a,b,c)=>{let d,e,f,g;d=Bw(c,a.eh.length-1|0);if(b<65536){e=b&65535;while(true){if(d<0)return (-1);if(a.eh.charCodeAt(d)==e)break;d=d+(-1)|0;}return d;}f=Ft(b);g=EH(b);while(true){if(d<1)return (-1);if(a.eh.charCodeAt(d)==g){c=d-1|0;if(a.eh.charCodeAt(c)==f)break;}d=d+(-1)|0;}return c;},HW=(a,b)=>{return C0(a,b,a.eh.length-1|0);},FJ=(a,b,c)=>{let d,e,f;d=BR(0,c);e=a.eh.length-b.eh.length|0;a:while(true){if(d>e)return (-1);f=0;while
(true){if(f>=b.eh.length)break a;if(P(a,d+f|0)!=P(b,f))break;f=f+1|0;}d=d+1|0;}return d;},BW=(a,b,c)=>{let d,e,f;d=a.eh.length;e=B1(b,c);if(!e)return Gh;if(!b&&c==d)return a;if(b>=0&&e<=0&&c<=d)return HS(a.eh.substring(b,c));f=new CJ;Z(f);Q(f);},DV=(a,b)=>{return BW(a,b,a.eh.length);},Px=a=>{return a;},D4=a=>{let b,c,d,e;b=BJ(a.eh.length);c=b.data;d=0;e=c.length;while(d<e){c[d]=P(a,d);d=d+1|0;}return b;},Df=b=>{Ce();return b===null?I(2):b.g();},Hg=b=>{let c;Ce();c=new W;Y(c);return X(BY(c,b));},C1=(a,b)=>{let c;if
(a===b)return 1;if(!(b instanceof Br))return 0;c=b;return a.eh!==c.eh?0:1;},MV=()=>{let b;N9=BJ(0);b=new Br;Ce();b.eh="";Gh=b;KS=new Hw;},Fk=L();
function Dw(){Fk.call(this);this.ij=0;}
let Lb=null,Ea=null,BN=()=>{BN=B3(Dw);KR();},JL=(a,b)=>{BN();a.ij=b;},GU=a=>{let b=new Dw();JL(b,a);return b;},C7=b=>{BN();return (G7(OD(20),b,10)).g();},DN=(b,c)=>{BN();if(b!==null)return N5(b,0,b.eh.length,c);b=new BK;Bu(b,I(3));Q(b);},N5=(b,c,d,e)=>{let f,g,h,i,j,k,l,m;BN();if(c==d){b=new BK;Bu(b,I(4));Q(b);}if(e>=2&&e<=36){a:{f=0;b=b;switch(P(b,c)){case 43:g=c+1|0;break a;case 45:f=1;g=c+1|0;break a;default:}g=c;}h=0;i=1+(2147483647/e|0)|0;if(g==d){b=new BK;Z(b);Q(b);}while(g<d){j=g+1|0;k=P(b,g);k=k>=48
&&k<=57?k-48|0:k>=97&&k<=122?(k-97|0)+10|0:k>=65&&k<=90?(k-65|0)+10|0:(-1);if(k<0){l=new BK;b=Df(BW(b,c,d));m=new W;Y(m);T(T(m,I(5)),b);Bu(l,X(m));Q(l);}if(k>=e){l=new BK;b=Df(BW(b,c,d));m=new W;Y(m);T(T(BY(T(m,I(6)),e),I(7)),b);Bu(l,X(m));Q(l);}if(h>i){b=new BK;Bu(b,I(8));Q(b);}h=DF(e,h)+k|0;if(h<0){if(j==d&&h==(-2147483648)&&f)return (-2147483648);l=new BK;b=Df(BW(b,c,d));m=new W;Y(m);T(T(m,I(9)),b);Bu(l,X(m));Q(l);}g=j;}if(f)h= -h|0;return h;}b=new BK;l=new W;Y(l);BY(T(l,I(10)),e);Bu(b,X(l));Q(b);},RS=b=>
{BN();return DN(b,10);},Hp=b=>{let c,d;BN();if(!b)return 32;c=0;d=b>>>16|0;if(d)c=16;else d=b;b=d>>>8|0;if(!b)b=d;else c=c|8;d=b>>>4|0;if(!d)d=b;else c=c|4;b=d>>>2|0;if(!b)b=d;else c=c|2;if(b>>>1|0)c=c|1;return (32-c|0)-1|0;},DO=b=>{let c,d;BN();if(!b)return 32;c=0;d=b<<16;if(d)c=16;else d=b;b=d<<8;if(!b)b=d;else c=c|8;d=b<<4;if(!d)d=b;else c=c|4;b=d<<2;if(!b)b=d;else c=c|2;if(b<<1)c=c|1;return (32-c|0)-1|0;},KR=()=>{Lb=B9(Gs);};
function Cl(){let a=this;J.call(a);a.eD=null;a.er=0;}
let Y=a=>{CX(a,16);},Sk=()=>{let a=new Cl();Y(a);return a;},CX=(a,b)=>{a.eD=BJ(b);},OD=a=>{let b=new Cl();CX(b,a);return b;},B4=(a,b)=>{return a.x(a.er,b);},Eh=(a,b,c)=>{let d,e,f,g;if(b>=0&&b<=a.er){if(c===null)c=I(2);else if(c.eh.length?0:1)return a;a.y(a.er+c.eh.length|0);d=a.er-1|0;while(d>=b){a.eD.data[d+c.eh.length|0]=a.eD.data[d];d=d+(-1)|0;}a.er=a.er+c.eh.length|0;e=0;while(e<c.eh.length){f=a.eD.data;g=b+1|0;f[b]=P(c,e);e=e+1|0;b=g;}return a;}c=new CJ;Z(c);Q(c);},G7=(a,b,c)=>{return OZ(a,a.er,b,c);},OZ
=(a,b,c,d)=>{let e,f,g,h,i,j,k;e=1;if(c<0){e=0;c= -c|0;}a:{if(Es(c,d)<0){if(e)Cy(a,b,b+1|0);else{Cy(a,b,b+2|0);f=a.eD.data;g=b+1|0;f[b]=45;b=g;}a.eD.data[b]=Fm(c,d);}else{h=1;i=1;j=EP((-1),d);b:{while(true){k=DF(h,d);if(Es(k,c)>0){k=h;break b;}i=i+1|0;if(Es(k,j)>0)break;h=k;}}if(!e)i=i+1|0;Cy(a,b,b+i|0);if(e)e=b;else{f=a.eD.data;e=b+1|0;f[b]=45;}while(true){if(!k)break a;f=a.eD.data;b=e+1|0;f[e]=Fm(EP(c,k),d);c=RG(c,k);k=EP(k,d);e=b;}}}return a;},Bh=(a,b)=>{return a.B(a.er,b);},In=(a,b,c)=>{Cy(a,b,b+1|0);a.eD.data[b]
=c;return a;},EQ=(a,b)=>{let c,d,e,f,g;c=a.eD.data.length;if(c>=b)return;d=c>=1073741823?2147483647:BR(b,BR(c*2|0,5));e=a.eD.data;f=BJ(d);g=f.data;b=Bw(d,e.length);c=0;while(c<b){g[c]=e[c];c=c+1|0;}a.eD=f;},X=a=>{return D3(a.eD,0,a.er);},Er=(a,b)=>{let c;if(b>=0&&b<a.er)return a.eD.data[b];c=new Bo;Z(c);Q(c);},E3=(a,b,c,d)=>{return a.D(a.er,b,c,d);},Gr=(a,b,c,d,e)=>{let f,g;if(d<=e&&e<=c.E()&&d>=0){Cy(a,b,(b+e|0)-d|0);while(d<e){f=a.eD.data;g=b+1|0;f[b]=c.h(d);d=d+1|0;b=g;}return a;}c=new Bo;Z(c);Q(c);},Fo=
(a,b)=>{return a.F(b,0,b.E());},HQ=(a,b,c,d)=>{return a.G(a.er,b,c,d);},GA=(a,b,c,d,e)=>{let f,g,h,i;Cy(a,b,b+e|0);f=e+d|0;while(d<f){g=c.data;h=a.eD.data;e=b+1|0;i=d+1|0;h[b]=g[d];b=e;d=i;}return a;},DR=(a,b)=>{return a.H(b,0,b.data.length);},Cy=(a,b,c)=>{let d,e,f,g;d=a.er;e=d-b|0;a.y((d+c|0)-b|0);f=e-1|0;while(f>=0){g=a.eD.data;g[c+f|0]=g[b+f|0];f=f+(-1)|0;}a.er=a.er+(c-b|0)|0;},EL=L(0),W=L(Cl),PB=a=>{Y(a);},L7=()=>{let a=new W();PB(a);return a;},T=(a,b)=>{let c,d;c=a.er;d=a;b=b===null?I(2):b.g();Eh(d,c,
b);return a;},Lf=(a,b)=>{B4(a,b);return a;},BY=(a,b)=>{G7(a,b,10);return a;},F$=(a,b)=>{Bh(a,b);return a;},N6=(a,b,c)=>{let d,e,f,g,h,i;if(b>=0){d=B1(b,c);if(d<=0){e=a.er;if(b<=e){if(d){if(c>e)c=e;f=e-c|0;a.er=e-(c-b|0)|0;e=0;while(e<f){g=a.eD.data;d=b+1|0;h=c+1|0;g[b]=g[c];e=e+1|0;b=d;c=h;}}return a;}}}i=new CJ;Z(i);Q(i);},GB=(a,b)=>{let c,d,e,f;if(b>=0){c=a.er;if(b<c){c=c-1|0;a.er=c;while(b<c){d=a.eD.data;e=b+1|0;d[b]=d[e];b=e;}return a;}}f=new CJ;Z(f);Q(f);},Jb=(a,b,c)=>{let d;d=a;if(b<=c&&b>=0&&c<=d.er)return D3(d.eD,
b,c-b|0);d=new Bo;Z(d);Q(d);},QF=(a,b,c,d,e)=>{GA(a,b,c,d,e);return a;},JU=(a,b,c,d)=>{HQ(a,b,c,d);return a;},OI=(a,b,c,d,e)=>{Gr(a,b,c,d,e);return a;},RF=(a,b,c,d)=>{E3(a,b,c,d);return a;},MC=(a,b)=>{return Er(a,b);},C5=a=>{return a.er;},C9=a=>{return X(a);},Q6=(a,b)=>{EQ(a,b);},Kc=(a,b,c)=>{In(a,b,c);return a;},RA=(a,b,c)=>{Eh(a,b,c);return a;};
function Ff(){let a=this;J.call(a);a.gw=null;a.iq=null;a.ht=0;a.hI=0;}
let Si=a=>{return a;},G5=a=>{G0(a);},RV=a=>{return a.gw;},R8=a=>{let b;b=a.iq;if(b===a)b=null;return b;},Cr=L(Ff),Bi=L(Cr),Z=a=>{G5(a);a.ht=1;a.hI=1;},Sf=()=>{let a=new Bi();Z(a);return a;},Bu=(a,b)=>{G5(a);a.ht=1;a.hI=1;a.gw=b;},Rd=a=>{let b=new Bi();Bu(b,a);return b;},CT=L(),Me=L(CT),KP=(a,b)=>{switch(a.primitiveKind){default:return K(a,b);}},LO=L(CT),GE=L(0),Jl=L(0),FU=L(0);
function GX(){let a=this;J.call(a);a.fP=0;a.eJ=null;a.gE=null;a.gW=null;}
let K1=b=>{let c;c=new GX;c.eJ=b;return c;},I6=a=>{let b,c,d,e,f;b=a.fP;if(!(b&1)){a.fP=b|1;c=a.eJ[Bc].name;d=c===null?null:Bt(c);if(d===null){e=a.eJ[Bc].itemType;if(e!==null){f=I6(B9(e));if(f!==null){if(e[Bc].itemType!==null){c=new W;Y(c);Bh(c,91);T(c,f);d=X(c);}else{c=new W;Y(c);Bh(T(T(c,I(11)),f),59);d=X(c);}}}}a.gE=d;}return a.gE;},Fh=a=>{let b,c,d,e;b=a.fP;if(!(b&2)){a.fP=b|2;c=a.eJ[Bc].simpleName;d=c===null?null:Bt(c);if(d===null){if(a.eJ[Bc].itemType!==null){c=Fh(B9(a.eJ[Bc].itemType));d=new W;Y(d);T(T(d,
c),I(12));d=X(d);}else{c=a.eJ[Bc].enclosingClass;if((c===null?null:B9(c))!==null)d=I(13);else{d=I6(a);e=HW(d,36);if(e==(-1)){b=HW(d,46);if(b!=(-1))d=DV(d,b+1|0);}else{d=DV(d,e+1|0);if(P(d,0)>=48&&P(d,0)<=57)d=I(13);}}}}a.gW=d;}return a.gW;},DI=a=>{return !a.eJ[Bc].primitiveKind?0:1;},Ek=a=>{let b;b=a.eJ[Bc].itemType;return b===null?null:B9(b);},I_=L(),Bx=()=>{Bx=B3(I_);Mx();},O0=b=>{Bx();},H0=(b,c,d,e,f,g,h)=>{let i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,ba,bb,$$je;Bx();if(b!==null&&c!==null){if(f>=1&&g>=1){a:{b:
{try{i=FT(b,NZ(d));break a;}catch($$e){$$je=BS($$e);if($$je instanceof CB){j=$$je;break b;}else if($$je instanceof Bi){j=$$je;}else{throw $$e;}}return CR(I(14),Fh(Co(j)),Jr(j,I(15)),(-1));}k=j.gp;if(!(k!==null&&k.eh.length)){b=I(13);e=j.fv;if(e>=1){l=BJ(e);m=l.data;e=0;f=m.length;if(e>f){b=new Bs;Z(b);Q(b);}while(e<f){g=e+1|0;m[e]=32;e=g;}b=Dk(l);}c=j.gp;h=j.f3;if(h!==null&&h.eh.length){d=j.fv;h=j.f3;n=new W;Y(n);T(T(T(T(BY(n,d),I(16)),h),I(16)),b);b=X(n);}else b=I(13);h=new W;Y(h);T(T(h,c),b);k=X(h);}return CR(I(14),
I(17),k,j.fv);}o=Ha(i,c);p=o.eI.gz-1|0;if(p>1000)return CR(I(18),I(19),I(20),(-1));q=new W;Y(q);if(h===null)r=null;else{r=new DT;Y(r);}s=0;t=0;u=0;v=0;w=0;c:{try{d:{e:{while(G6(o)){x=BR(1,p);if(s>=f)break e;t=t+x|0;if(t>g)break e;Ec(q,C$(o));Ec(q,Dq(o));y=1;while(y<=p){Ec(q,Fq(o,y));Ec(q,Eu(o,y));y=y+1|0;}s=s+1|0;if(r!==null&&!w){z=Li(o,h);if(FW(Cv(HU(Cv(BM(I$(r)),BM(C$(o))),BM(u)),z),BM(67108864))){Gm(o,r,h);u=Dq(o);}else{Gy(r,c,u,C$(o),67108864);w=1;}}if(!e)break d;}break d;}v=1;}if(r!==null&&!w){if(FW(HU(Cv(BM(I$(r)),
BM(Dz(c))),BM(u)),BM(67108864)))GK(o,r);else{Gy(r,c,u,Dz(c),67108864);w=1;}}}catch($$e){$$je=BS($$e);if($$je instanceof Bi){j=$$je;break c;}else{throw $$e;}}ba=FX();B5(ba,I(21));B5(ba,I(22));ED(ba,p);FE(ba,v);ED(ba,s);Fo(ba.fO,q);if(r!==null){FE(ba,w);B5(ba,X(r));}return HX(ba);}n=h!==null?I(23):I(24);bb=h!==null?I(25):I(26);return CR(n,Fh(Co(j)),Jr(j,bb),(-1));}return CR(I(18),I(27),I(28),(-1));}return CR(I(29),I(30),I(31),(-1));},NZ=b=>{let c;Bx();c=0;if(b&1)c=2;if(b&2)c=c|8;if(b&4)c=c|32;if(!(!(b&8)&&!(b
&32)))c=c|64;if(b&16)c=c|4;if(b&64)c=c|1;return c;},Li=(b,c)=>{let d,e,f,g;Bx();d=RN;e=0;while(e<c.eh.length){f=P(c,e);if(f==92){e=e+1|0;if(e>=c.eh.length){b=new Bo;Z(b);Q(b);}d=Cv(d,BM(1));}else if(f!=36)d=Cv(d,BM(1));else{e=e+1|0;if(e>=c.eh.length){b=new Bs;Bu(b,I(13));Q(b);}g=Fc(P(c,e),10);if(g<0){b=new Bs;Bu(b,I(13));Q(b);}d=Cv(d,BM((Ep(b,g)).eh.length));}if(JK(d,BM(67108864)))return d;e=e+1|0;}return d;},Gy=(b,c,d,e,f)=>{let g,h;Bx();g=f-b.er|0;if(g>0&&d<e){h=Bw(e,d+g|0);if(h<e&&h>d&&BA(P(c,h-1|0))&&BI(P(c,
h)))h=h+(-1)|0;E3(b,c,d,h);return;}},CR=(b,c,d,e)=>{let f;Bx();f=FX();B5(f,I(21));B5(f,b);if(c===null)c=I(32);B5(f,c);if(d===null)d=I(13);B5(f,d);ED(f,e);return HX(f);},Jr=(b,c)=>{let d;Bx();d=b.gw;if(d!==null&&d.eh.length)c=d;return c;},Ec=(b,c)=>{let d;Bx();d=C7(c);b=BY(b,d.eh.length);Bh(b,58);B4(b,d);},D=()=>{Bx();return 1;},E=()=>{Bx();return "TeaVM java.util.regex 0.15.0";},F=()=>{let b,c,d,$$je;Bx();a:{try{b=Ha(RJ(I(33)),I(34));if(G6(b)&&C$(b)==1&&Dq(b)==3&&Fq(b,1)==1&&Eu(b,1)==2){c=QH();Gm(b,c,I(35));GK(b,
c);d=!KN(I(36),c)?2:0;break a;}d=1;break a;}catch($$e){$$je=BS($$e);if($$je instanceof Bi){d=3;break a;}else{throw $$e;}}}return d;},G=(b,c,d,e,f,g)=>{Bx();return D$(H0(Bt(b),Bt(c),d,e?1:0,f,g,null));},H=(b,c,d,e,f,g,h)=>{Bx();return D$(H0(Bt(b),Bt(c),e,f?1:0,g,h,Bt(d)));},Mx=()=>{return;},Pw=L(),Q1=L(),RT=L(),E7=L(0),Hw=L(),E9=L(),La=null,Eo=null,Fb=null,EB=null,EW=null,PL=null,Dt=null,Ds=null,Du=null,Dv=null,Bb=()=>{Bb=B3(E9);Mr();},HA=b=>{let c,d;Bb();c=new Br;d=BJ(1);d.data[0]=b;GJ(c,d);return c;},EM=b=>
{Bb();return b>=65536&&b<=1114111?1:0;},BA=b=>{Bb();return (b&64512)!=55296?0:1;},BI=b=>{Bb();return (b&64512)!=56320?0:1;},Dx=(b,c)=>{Bb();return BA(b)&&BI(c)?1:0;},Cm=(b,c)=>{Bb();return ((b&1023)<<10|c&1023)+65536|0;},Ft=b=>{Bb();return (55296|(b-65536|0)>>10&1023)&65535;},EH=b=>{Bb();return (56320|b&1023)&65535;},Cj=b=>{Bb();return CV(b)&65535;},CV=b=>{Bb();if(EB===null){if(Dt===null)Dt=Pq();EB=F6(Hx((Dt.value!==null?Bt(Dt.value):null)));}return Go(EB,b);},Ch=b=>{Bb();return Dh(b)&65535;},Dh=b=>{Bb();if
(Fb===null){if(Ds===null)Ds=KT();Fb=F6(Hx((Ds.value!==null?Bt(Ds.value):null)));}return Go(Fb,b);};
let Go=(b,c)=>{let d,e,f,g,h,i;Bb();d=b.gJ.data;if(c<d.length)return c+d[c]|0;d=b.hS.data;e=0;f=d.length;g=(f/2|0)-1|0;a:{while(true){h=(e+g|0)/2|0;i=B1(d[h*2|0],c);if(!i)break;if(i<=0){e=h+1|0;if(e>g)break a;}else{h=h-1|0;if(h<e)break a;g=h;}}}if(h>=0){h=h*2|0;if(h<f)return c+d[h+1|0]|0;}return 0;},Fc=(b,c)=>{let d,e,f,g,h,i,j,k,l,m;Bb();if(c>=2&&c<=36){if(Eo===null){if(Du===null)Du=Oa();d=(Du.value!==null?Bt(Du.value):null);e=JC(D4(d));f=DY(e);g=BC(f*2|0);h=g.data;i=0;j=0;k=0;l=0;while(l<f){j=j+Ev(e)|0;k=
k+Ev(e)|0;m=i+1|0;h[i]=j;i=m+1|0;h[m]=k;l=l+1|0;}Eo=g;}g=Eo.data;i=0;j=(g.length/2|0)-1|0;a:{while(j>=i){k=(i+j|0)/2|0;l=k*2|0;f=B1(b,g[l]);if(f>0)i=k+1|0;else{if(f>=0){b=g[l+1|0];break a;}j=k-1|0;}}b=(-1);}if(b>=c)b=(-1);}else b=(-1);return b;},Fm=(b,c)=>{Bb();if(c>=2&&c<=36&&b>=0&&b<c)return b<10?(48+b|0)&65535:((97+b|0)-10|0)&65535;return 0;},CI=b=>{let c,d,e;Bb();if(!(b>=0&&b<=1114111?1:0)){c=new Bs;Z(c);Q(c);}if(b<65536){d=BJ(1);d.data[0]=b&65535;return d;}d=BJ(2);e=d.data;e[0]=Ft(b);e[1]=EH(b);return d;},DD
=b=>{Bb();return BB(b);},BB=b=>{let c,d,e,f,g;Bb();if(b>0&&b<=65535?1:0){c=b&65535;if(!BA(c)&&!BI(c)?0:1)return 19;}if(EW===null){if(Dv===null)Dv=Re();EW=RR((Dv.value!==null?Bt(Dv.value):null));}d=EW.data;e=0;c=d.length-1|0;while(e<=c){f=(e+c|0)/2|0;g=d[f];if(b>=g.gF)e=f+1|0;else{c=g.gY;if(b>=c)return g.g7.data[b-c|0];c=f-1|0;}}return 0;},HY=b=>{Bb();return Ho(b);},Ho=b=>{Bb();a:{switch(BB(b)){case 1:case 2:case 3:case 4:case 5:case 9:break;case 6:case 7:case 8:break a;default:break a;}return 1;}return 0;},C4
=b=>{Bb();a:{if(!(b>=0&&b<=8)&&!(b>=14&&b<=27)){if(b<127)break a;if(b>159)break a;}return 1;}return BB(b)!=16?0:1;},JB=b=>{Bb();switch(BB(b)){case 12:case 13:case 14:break;default:return 0;}return 1;},HI=b=>{Bb();switch(b){case 9:case 10:case 11:case 12:case 13:case 28:case 29:case 30:case 31:break;case 160:case 8199:case 8239:return 0;default:return JB(b);}return 1;},Mr=()=>{La=B9(HC);PL=K(E9,128);},Pq=()=>{return {"value":"NY H#F#U 4%F#O #F#/ d%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #a1# #%# #%# #%# %%# #%# #%# #%# #%# #%# #%# #%# %%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #<+#%# #%# #%# \'.3#%# #%# #{1#%# #w1%%# %J\'#k1#o1#%# #w1#!3# #23#*3#%# \'23#:3# #>3#%# #%# #%# #N3#%# #N3# %%# #N3#%# #J3%%# #%# #R3#%# \'%# /)#%# #)#%# #)#%# #%# #%# #%# #%# #%# #%# #%# #%# %%# #%# #%# #%# #%# #%# #%# #%# #%# %)#%# #%# #8)#L%#%# #%# #%# #"
+"%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #a+# #%# #%# #%# #%# #%# #%# #%# #%# #%# /B45#%# #,/#645# %%# #P1#!\'#*\'#%# #%# #%# #%# #%# <-%# #%# \'%# 1&++ %_## #Z#)k%%g%% #F#W hA# 1%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# +]%# %%# #?#%# %a+\'N\'AF#b &#%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# 3%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #^#%# #%# #%# #%# #%# #%# #%# %%# #%# #%# #%# #%# #%# #%# #%"
+"# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# %*%p kB#oq-&# _?gejg#A1 a$#%# -mo%&# {-%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# 3,4/# #%# #%"
+"# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# 3C1 1C1 1C1 1C1 1C1 3C/ 1C1 QC1 1C1 1C1 1C%8\'%G# 7i\')G# 7C%D)\' 7C%u)%?# 7X+%P+%G# L-q*/# \'Pw/#8m/# -6## |bA G%# kC.#U !r*%&# &#%# #,05#qX\'#H.5# %%# #%# #%# #e25#D05#q25#m25# #%# %%# 1865%%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# "
+"#%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# 1%# #%# )%# (a=%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# G%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# y%%# #%# #%# #%# #%# #%# #%# \'%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #%# 5%# #%# #4Fd#%# #%# #%# #%# #%# )%# #<{p# %%# #%# \'%# #%# #%# #%# #%# #%# #%# #%# #%# #%# #P}p#}}p#m}p#D}p#P}p# #@yp#D{p#Lyp#Br#%# #%# #%"
+"# #%# #%# #%# #%# #%# #,%#L}p#LJd#%# #%# #$$r#%# #%# #%# #%# #%# #%# #%# #%# #P6r#}!rI )%# :GL#) i,5F#U TUg#r {%g#r >\'c#p Lnk%F# .\'F#S HB#F#b o@5F#b F#2#W 4Z;%# /%# #%# %%# \'%# M%# #%# #%# #%# \'%# #%# #%# #%# #%# #%# #%# u.#N#f "};},KT=()=>{return {"value":"L[ ,%H#U :#>b# vH#O #H#/:+# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #,5# #\'# #\'# #\'# %\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# %\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# %\'# #\'# #\'#(;#N1# %\'# #\'# %\'# \'\'# +\'# %6)# \'\'#*/#N6r# %_+# %\'# #\'# #\'# %\'# )\'# %\'# \'\'# #\'# %\'# \'\'# #J%# +\'#+# #\'#+# #\'#+# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'#L\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# %\'#+# #\'# \'\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'#"
+" #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# \'\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# 1\'# %665% #\'# )\'# #\'# #\'# #\'# #\'#o25#c25#k25#03#}1# #y1% #m1# #q1#{}p# \'y1#k}p# #$3#!$r#:{p#N}p# #,3#43#N}p#*05#B}p# %43# #B05#<3# %@3# #{!r# ){!r#F.5# %P3# #J}p#P3# \'B{p#P3#$\'#L3%,\'# +T3# 5Jyp#>yp# Z\'_\'# x\'# #\'# \'\'\' #_+\' !#a##]#\' #H#CD##H#3m%#i%% #e%#P%# \'(%#D%#C# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'#i\'#P\'#=#(+# #4)# %\'# %\'# .#H#bP\'A #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# 3\'# #\'# #\'# #\'# #\'# #\'# "
+"#\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# %\'# #\'# #\'# #\'# #\'# #\'# #\'#`# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'% &#,%n mB#ko%x %ko%\' RAC1 >$#yu+#uu+#Pu+#Hu+%Lu+#0u+#io+#>@d# #\'- (+2Fd# \'oX\'# AJJd# N%\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #"
+"\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# +X%# +\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'#A1 1A1 1A1 1A1 1A1 3A# #A# #A# #A% /A1 16\'%g\')B)%V+%s)%N+)A1 1A1 1A1 1A% #E# 5<m-# )E# 9A% =A% \'=# ;E# R/8## ddA )\'# @E0#U Nr,%&# #\'# \'D4"
+"5#845# #\'# #\'# #\'# -\'# %\'# 5\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# 1\'# #\'# )\'- /qq-&# i]=\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# G\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# y%\'# #\'# #\'# #\'# #\'# #\'# #\'# \'\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'#"
+" #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# 5\'# #\'# %\'# #\'# #\'# #\'# #\'# )\'# )\'# #\'#*%# %\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# 7\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# )\'# #\'# %\'# #\'# #\'# #\'# #\'# #\'# #\'# #\'# U\'# REJ#% -Dr# Yiejg# e*5H#U eUi#r {%i#r <\'e#t {nm%:# V%H#^ >B#H#b o@5H#b <#4#P# eV;\'# /\'# #\'# %\'# \'\'# M\'# #\'# #\'# #\'# \'\'# #\'# #\'# #\'# #\'# #\'# #\'# Z0#P#f "};},Oa=()=>{return {"value":"6G*% %%%%%%%%%%%%%%%%%%A%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=,#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%_H#T#%%%%%%%%%%%%%%%%%%s+G%%%%%%%%%%%%%%%%%%_1G%%%%%%%%%%%%%%%%%%{CG%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%6)G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%*\'G%%%%%%%%%%%%%%%%%%.9G%%%%%%%%%%%%%%%%%%*\'G%%%%%%%%%%%%%%%%%%!i#G"
+"%%%%%%%%%%%%%%%%%%c#G%%%%%%%%%%%%%%%%%%*;G%%%%%%%%%%%%%%%%%%Z+G%%%%%%%%%%%%%%%%%%:/G%%%%%%%%%%%%%%%%%%=G%%%%%%%%%%%%%%%%%%{/G%%%%%%%%%%%%%%%%%%k\'G%%%%%%%%%%%%%%%%%%s+G%%%%%%%%%%%%%%%%%%=G%%%%%%%%%%%%%%%%%%R@dG%%%%%%%%%%%%%%%%%%R[G%%%%%%%%%%%%%%%%%%c#G%%%%%%%%%%%%%%%%%%_1G%%%%%%%%%%%%%%%%%%!#G%%%%%%%%%%%%%%%%%%k\'G%%%%%%%%%%%%%%%%%%cCG%%%%%%%%%%%%%%%%%%o*IG%%%%%%%%%%%%%%%%%%A%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=,#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%c:#T#%%%%%%%%%%%%%%%%%%w&%G%%%%%"
+"%%%%%%%%%%%%%=G%%%%%%%%%%%%%%%%%%_fG%%%%%%%%%%%%%%%%%%Z+G%%%%%%%%%%%%%%%%%%_%G%%%%%%%%%%%%%%%%%%>-G%%%%%%%%%%%%%%%%%%.9G%%%%%%%%%%%%%%%%%%w=G%%%%%%%%%%%%%%%%%%2+G%%%%%%%%%%%%%%%%%%>AG%%%%%%%%%%%%%%%%%%N)G%%%%%%%%%%%%%%%%%%=G%%%%%%%%%%%%%%%%%%%G%%%%%%%%%%%%%%%%%%B\'G%%%%%%%%%%%%%%%%%%FEG%%%%%%%%%%%%%%%%%%N)G%%%%%%%%%%%%%%%%%%oYG%%%%%%%%%%%%%%%%%%k\'G%%%%%%%%%%%%%%%%%%g5G%%%%%%%%%%%%%%%%%%*\'G%%%%%%%%%%%%%%%%%%F%G%%%%%%%%%%%%%%%%%%Z?G%%%%%%%%%%%%%%%%%%ow?G%%%%%%%%%%%%%%%%%%s4%G%%%%%%%%%%%%%%%%%%k\'G%%%%%%%%%%%%%%"
+"%%%%s+G%%%%%%%%%%%%%%%%%%:OG%%%%%%%%%%%%%%%%%%c#G%%%%%%%%%%%%%%%%%%N&OG%%%%%%%%%%%%%%%%%%VZ%G%%%%%%%%%%%%%%%%%%%G%%%%%%%%%%%%%%%%%%%G%%%%%%%%%%%%%%%%%%%G%%%%%%%%%%%%%%%%%%%G%%%%%%%%%%%%%%%%%%!8%G%%%%%%%%%%%%%%%%%%FEG%%%%%%%%%%%%%%%%%%sKG%%%%%%%%%%%%%%%%%%k5G%%%%%%%%%%%%%%%%%%.lG%%%%%%%%%%%%%%%%%%wN)G%%%%%%%%%%%%%%%%%%"};},Re=()=>{return {"value":"PA-Y$;Y$679:95Y#J+Y#Z$Y#B;697<8<C;6:7:PB-9[%=9<=&>:1=<=:L#<#Y#<,&?L$9B8:B(C9:C)!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!C#!#!#!#!#!#!#!#!C#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#B##!#!C$B##!#B##B$C#B%#B##B$C$B##B##!#!#B##!C#!#B##B$#!#B#C#&!C$F%!$#!$#!$#!#!#!#!#!#!#!#!C#!#!#!#!#!#!#!#!#!C#!$#!#B$#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!C(B##B#C#!#B%#!#!#!#!CgF#C;E3]%E-]/E&](%<%]2b\'Q! !#!#%<!#A#%C$9!A%]#!9B$ ! B##B2 B*CD!C#B$C$!#!#!#!#!#!#!#!#!#!#!#!C&!#:!#B#C#BTCQ!#!#!#!"
+"#!#!#!#!#!#!#!#!#!#!#!#!#!#=G&H#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#B##!#!#!#!#!#!C#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!# BG E#Y\'CJ95E#^#; GN5\'9G#9G#9G$A\'F<A%F%Y#A,Q\'Z$Y#;Y#^#G,91Y$FA%F+G6J+Y%F#\'b&D! 9&G(1=G\'E#G#=G%F#J+F$^#&Y/ 1&\'F?G<A#b&:! G,&A/J+FBG*E#=Y$%A#\'[#F7G%%G*%G$%G&A#Y0 F:G$A#9 F,A&F9<F(Q#A&G*FJ%G91GA)FW\')\'&I$G)I%\'I#&G(F+G#Y#J+9%F0\'I#\'F)A#F#A#F7 F( &A$F%A#\'&I$G%A#I#A#I#\'&A))A%F# F$G#A#J+F#[#L\'=;&9\'& G#) F\'A%F#A#F7 F( F# F"
+"# F#A#\' I$G#A%G#A#G$A$\'A(F% &A(J+G#F$\'9A+G#) F* F$ F7 F( F# F&A#\'&I$G& G#) I#\'A#&A0F#G#A#J+9;A(&G\' \'I# F)A#F#A#F7 F( F# F&A#\'&)\')G%A#I#A#I#\'A&G%)A%F# F$G#A#J+=&L\'A+\'& F\'A$F$ F%A$F# & F#A$F#A$F$A$F-A%I#\'I#A$I$ I$\'A#&A\')A/J+L$^\';=A&\'I$\'F) F$ F8 F1A#\'&G$I% G$ G%A(G# F$ F#A#F#G#A#J+A(9L(=&\'I#9F) F$ F8 F+ F&A#\'&)\'I& \'I# I#G#A(I#A&F$ F#G#A#J+ F#)A-G#I#F* F$ FJG#&I$G% I$ I$\'&=A%F$)L(F$G#A#J+L*=F\' \'I# F3A$F9 F* &A#F(A$\'A%I$G$ \' I)A\'J+A#I#9A-FQ\'F#G(A%;F\'%G)9J+Y#AFF# & F& F9 & F+\'F#G*&A#F& % G( J+A#F%AA&^$Y0=9^$G#^\'J"
+"+L+=\'=\'=\'6767I#F) FEA%G/)G&9G#F&G, GE ^)\'^\' ^#Y&^%Y#AFFLI#G%)G\')G#I#G#&J+Y\'F\'I#G#F%G$&I$F#I(F$G%F.\'I#G#I\'\'&)J+I$\'^#BG !A&!A#CL9%C$b&*& F%A#F( & F%A#FJ F%A#FB F%A#F( & F%A#F0 FZ F%A#FeA#G$Y*L5A$F1^+A\'b!7! A#C\'A#5b&M* =9F2-F;67A$FmY$K$F)A(F3G$)A*F4G#)Y#A*F3G#A-F. F$ G#A-FUG#)G(I)\'I#G,Y$%Y$;&\'A#J+A\'L+A\'Y\'5Y%G$1\'J+A\'FD%FWA\'F&G#FC\'&A&FhA+F@ G$I%G#I$A%I#\'I\'G$A%=A$Y#J+F?A#F&A,FMA%F;A\'J+,A$^CF8G#I#\'A#Y#FV)\')G( \')\'I#G)I\'G+A#\'J+A\'J+A\'Y(%Y\'A#G/(GSA0G%)FP\')G&)\'I&\'I#F) Y#J+Y(^+G*^*Y$G#)F?)G%I#G#)G$F#J+FM\')G#I$\')G$I#A)Y"
+"%FEI)G)I#G#A$Y&J+A$F$J+F?E\'Y#C*!#A&BLA#B$Y)A)G$9G.)G(F%\'F\'\'F#)G#&A&CMEaC.%CCEFGb!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!C*!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!C*B)C\'A#B\'A#C)B)C)B)C\'A#B\'A#C) ! ! ! !C)B)C/A#C)D)C)D)C)D)C& C#B%$<#]$C$ C#B%$]$C%A#C#B% ]$C)B&]$A#C$ C#B%$]# M,Q&U\'Y#>?6_#?6>Y)./Q&-Y*>?Y%X#Y$:67Y,:98Y+-Q& Q+,%A#L\'Z$67%L+Z$67E2[FA,G."
+"H%\'H$G-A0^#!^%!^##B$C#B$#=!^#:B&^\'!=!=!=B%=#B%#F%#^#C#B#Z&!C%=:^##=L1KD!#K%,^#A%Z&^&Z#^%:^#:^#:^(:^@Z#^#:=:^@b:-% ^)6767^5Z#^(67b=2! :^?Z:^IZ\'^jA7^,A6L^^pL7b=X# :^*:^WZ)b=P! :b=Y$ 67676767676767L?^MZ&67Z@6767676767Z1b= % b:$# 6767676767676767676767Za6767ZA67b:#% ^QZ6^#Z\'^HA#b=+# BQCQ!#B$C#!#!#!#B%#!C#!C\'E#B$#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!C#^\'!#!#G$!#A&Y%,Y#CG #A&#A#FYA(%9A/\'F8A*F( F( F( F( F( F( F( F( GAY#>?>?Y$>?9>?Y*5Y#59>?Y#>?67676767Y&%Y"
+"+U#Y%596Y.^#Y$676767675A#Y#67A=^; b=:! A-b=7$ A;^1-Y$=%&+6767676767^#6767676756W#=K*G%I#5E&^#K$%&9^# b&7! A#G#]#E#&5b&;! 9E$&A&FL b&?! ^#L%^+FA^GA*=F1^@ L+^?L)=L0^AL+^HL0b= & b& H!^bb& %b&6)!%b&X2 A$^XA*FIE\'Y#b&-% %Y$F1J+F#A5!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#&\'H$9G+9%!#!#!#!#!#!#!#!#!#!#!#!#!#!#E#G#FhK+G#Y\'A)]8E*]#!#!#!#!#!#!#!C$!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#!#%C)!#!#B##!#!#!#!#%]#!#!#&!#!C$!#!#!#!#!#!#!#!#!#!#B&#B&#!#!#!#!#!#!#!#B%#!#B##!#!#!#!#!#!#!#B#A%!A/E%!#&"
+"E##F(\'F$\'F%\'F8I#G#)^%\'A$L\'^#;=A\'FUY%A)I#FSI1G#A)Y#J+A\'G3F\'Y$&9F#\'J+F=G)Y#F8G,I#A,9F>A$G$)FP\'I#G%I#G#I$Y. %J+A%Y#F&\'%F*J+F& FJG\'I#G#I#G#A*F$\'F)\')A#J+A#Y%F1%F\'^$&)\')FS\'&G$F#G#F&G#&\'&A9F#%Y#F,)G#I#Y#&E#)\'A+F\'A#F\'A#F\'A*F( F( CL<E%C*%]#B#A#b#1! FDI#\'I#\'I#9)\'A#J+A\'b&EO#A-F8A%FRA%b4 A b3 E!b&O& A#b&K! AGC(A-C&A&&\'F+:F. F& & F# F# b&M! ]2^1b&L& 76^1Fb^#FW^)AAF-;^$G1Y(679A\'G19U#X#6767676767676767Y#67Y%X$Y$ Y%5676767Y$:5Z$ 9;Y#A%F& b&(# A#1 Y$;Y$679:95Y#J+Y#Z$Y#B;697<8<C;6:7:67967Y#F+%FNE#F@A$F\'A#F\'A#F\'A#F$A$[#:<=[# "
+"=Z%^#A+Q$^#A#F- F; F4 F# F0A#F/ACb&]! A&Y$A%LNA$^*KVL%^2L#^$ ^.A$=AP^N\'b ## F>A$FRA0\'L<A%FAL%A*F5+F)+A&FGG&A&F? 9FEA%F)9K&AKBICIFpA#J+A\'BEA%CEA%FIA)FUA,9B, B0 B( B# C, C0 C( C#A$FUA-b&X% A*F7A+F)A9E\' EK E/AbF\'A#& FM F#A$&A#F8 9L)F8^#L(F@A)L*AQF4 F#A&L&F7L\'A$9F;A&9F;AGFYA%L#F#L1A#LO&G$ G#A&G%F% F$ F>A#G$A%\'L*A(Y*A(F>L#9F>L$AAF)=F=G#A%L&Y(A*FWA$Y(F7A#L)F4A&L)F3A(Y%A-L(b 1! FkAXBTA.CTA(L\'FEG%A)J+A\'J+F%%&B7A$G&5%C7A)Z#b 1$ L@ FK G#5A#F#A1F$%F# ]#G&9^)F7 G1F>L+&A)F7G,L%Y&A7F3G%Y%AGF6L(A5F8A*)\')FVG0Y(A%L5J+\'F#G#&"
+"A*G$)FNI$G%I#G#Y#1Y%\'A+1A#F:A(J+A\'G$FEG&)G) J+Y%&I#&A)FD\'Y#&A*G#)FQI$G*I#F%Y%G%9)\'J+&9&Y$ L5A,F3 F:I$G$I#\')G#Y\'\'F#\'A`F( & F% F0 F+9A\'FP\'I$G)A&J+A\'G#I# F)A#F#A#F7 F( F# F& G#&I#\'I%A#I#A#I$A#&A\')A&F&I#A#G(A$G&A,F+ &A#& FG &I$G\' )A#) I% I#\')\'&\'&Y# Y#A)G#A>FVI$G)I#G$)\'F%Y&J+Y# 9\'F$A?FQI$G\')\'I%G#)G#F#9&A)J+b G# FPI$G%A#I%G#)G#Y8F%G#ACFQI$G)I#\')G#Y$&A,J+A\'Y.A4FL\')\'I#G\')\'&9A\'J+A\'J5A=F<A#\')\'I#G%)G&A%J+L#Y$=F(b Z# FMI$G*)G#9b E! BACAJ+L*A-F)A#&A#F) F# F9I\' I#A#G#)\'&)&)\'Y$A*J+AhF)A#FHI$G%A#G#I%\'&9&)A<&G+FIG\')&G%Y)\'A)"
+"&G\'I#G$FOG.)G#Y$&Y&A.FkA(Y+&b 6! \')G$)\')b 9! FB9A/J+A\'F* FF)G( G\')\'&Y&A+J+L4A$Y#F?A#G7 )G()G#)G#AkF( F# FGG\'A$\' G# G(&\'A)J+A\'F\' F# FAI& G# I#\')\'&A(J+A\'FJ%F#A%J+b W$ F4G#I#Y#A(G#&)F. FCI#G&A$I#\')\'Y.J+\'b 6! &A0L6^)[%^2A.9b&;/ b G! b+Q! Y&K,b&%$ A-b+X% b *E b&B! Y#A.b&Q1 Q1\'F\'G0A+b&<` A&b&(* b ZK!F?G-I$G$J+b \'< b&Z) A(F@ J+A%Y#Fq J+A\'F?A#G&9A+FQG(Y&^%E%9=A+J+ L( F6A&F4b Q\' E$FIE#Y$J+A\'F9\'F%\'A#J+b 7# BACAL8Y%A&B:A#C:AMFmA%\'&IXA(G%E.AbE#9%\'A,I#E#K$A*b&<T!AEFCb @! b&T! A.b&3/ A/FTb >Y!E% E( E# b&J% A*&A>F$A#&A/F&"
+"A(b&-\' b %E b&L! A&F.A$F*A(F+A#=G#9Q%b =_ b=Q$ J+^$A$b=U\' A\'^8 ^$A)Z$^1Z/A#GOA#G8A*b=U! A^b=W$ A+^HG#^^I#G$^$I\'Q)G)^#G(^?G%b=5# G$=A+I$^)G#^#)^AI#A`L5A-L5A-b=8! A*L:b (# B;C;B;C( C3B;C;! B#A#!A#B#A#B% B)C% # C( C,B;C;B# B%A#B) B( C;B# B% B& !A$B( C;B;C;B;C;B;C;B;C;B;C;B;C> B::C::C\'B::C::C\'B::C::C\'B::C::C\'B::C::C\'!#A#JSb= ) GX^%GS^)\'^/\'^#Y&A0G& G0b 1! Z>b D0 C+&CV!C(!#!C#!C$!C7!#!#!#!C$!#!#!#!#!#!#!#F#A/C(AWETG( G2A#G( G# G&A&E`AB\'b Q! FNA$G(E(A#J+A%&=b & F?\'A2FMG%J+A&;b 1( F<%G%J+b 7$ F?G#&J+A%9b $ F@ F$\'"
+"F#\'F(G#F&\'A)&%b A$ F( F% F# F0 b&&$ A#L*G(AJBCCCG(%A%J+A%Y#b 2- L]=L$;L%AnLN=L0b #$ F% F< F# &A#& F+ F% & &A\'&A%& & & F$ F# &A#& & & & & F# &A#F% F( F% F% & F+ F2A&F$ F& F2AUZ#b /% ^MA%b=E! A-^0A#^0 ^0 ^FA+L.b=C# AX^>A.^MA%^*A(^#A/^\'b ;# b=]$ ]&b=;, A#^2A$^.A$b==$ A%^-A%^=A%^YA)^+A\'^IA)^?A#^-A%^#A/Z*AHb=9& A)^/A#^.A$^i =A$^3 ^.A$^-A&b=4# b==! J+=b &1 b& %b& %b&A<#AAb&@%! b&/;!A#b&RU!A0b&O* b CG b&?) b C8 b&,.!A&b&K%#b %b %b \'O!b& R#b %b %b %b %b %b %b %b %b %b %b %b %b %b "
+"%b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b %b !0 1A?b1A! b # b\'Q$ b %b %b %b 1Y$b3 %b3 %b3 %b3`a$A#b3 %b3 %b3 %b3`a$"};},LX=L(),Rf=L();
function HH(){let a=this;J.call(a);a.ei=null;a.eQ=0;a.gt=null;a.hb=0;a.fa=0;a.e1=0;a.ey=0;a.f2=null;a.fb=null;}
let Ha=(a,b)=>{let c,d,e,f,g,h,i,j,k;c=new FL;c.fI=(-1);c.f9=(-1);c.ir=a;c.gS=a.f2;c.fk=b;c.fI=0;d=b.eh.length;c.f9=d;e=new Jf;f=c.fI;g=a.fa;h=a.e1+1|0;i=a.ey+1|0;j=a.fb;e.fr=(-1);g=g+1|0;e.gz=g;e.eS=BC(g*2|0);e.ic=j;k=BC(i);e.fM=k;DS(k,(-1));if(h>0)e.gv=BC(h);DS(e.eS,(-1));IG(e,b,f,d);c.eI=e;e.e8=1;return c;},Fv=a=>{return a.ei.eE;},FT=(b,c)=>{let d,e,f,g,h,i,j;if(b===null){b=new Do;Bu(b,I(37));Q(b);}if(c&&(c|255)!=255){b=new Bs;Bu(b,I(13));Q(b);}Dc();CD=1;d=new HH;d.gt=K(By,10);d.fa=(-1);d.e1=(-1);d.ey=(-1);e
=new Hs;e.eY=1;e.eE=b;if((c&16)>0){f=new W;Y(f);B4(f,I(38));g=0;while(true){h=FJ(b,I(39),g);if(h<0)break;i=h+2|0;B4(f,BW(b,g,i));B4(f,I(40));g=i;}B4(f,DV(b,g));B4(f,I(39));b=X(f);}e.et=BJ(b.eh.length+2|0);G2(D4(b),0,e.et,0,b.eh.length);j=e.et.data;i=j.length;j[i-1|0]=0;j[i-2|0]=0;e.gK=i;e.fe=c;Cx(e);Cx(e);d.ei=e;d.eQ=c;d.f2=I3(d,(-1),c,null);if(!BQ(d.ei)){b=new CB;d=d.ei;DB(b,I(13),d.eE,d.eL);Q(b);}if(d.hb)d.f2.bP();b=d.fb;if(b===null){DA();d.fb=GL;}else{DA();e=new IF;e.ip=b;d.fb=e;}return d;},I3=(a,b,c,d)=>
{let e,f,g,h,i,j,k,l,m;e=HB();f=a.eQ;g=0;if(c!=f)a.eQ=c;a:{switch(b){case -1073741784:h=new IN;c=a.ey+1|0;a.ey=c;CE(h,c);break a;case -536870872:case -268435416:break;case -134217688:case -67108824:h=new FQ;c=a.ey+1|0;a.ey=c;CE(h,c);break a;case -33554392:h=new Gc;c=a.ey+1|0;a.ey=c;CE(h,c);break a;default:c=a.fa+1|0;a.fa=c;if(d===null){h=new Cz;CE(h,0);g=1;}else{h=RU(c);if(b==(-2130706392)){if(a.fb===null){i=new Hf;H7(i,16,0.75);i.g2=0;i.fN=null;a.fb=i;}i=a.fb;j=a.ei.hr;k=a.fa;BN();if(k>=(-128)&&k<=127){b:{if
(Ea===null){Ea=K(Dw,256);l=0;while(true){m=Ea.data;if(l>=m.length)break b;m[l]=GU(l-128|0);l=l+1|0;}}}d=Ea.data[k+128|0];}else d=GU(k);i.bR(j,d);}}k=a.fa;if(k<=(-1))break a;if(k>=10)break a;a.gt.data[k]=h;break a;}h=new IK;CE(h,(-1));}while(true){if(Cq(a.ei)&&a.ei.ej==(-536870788)){d=Hk(Bp(a,2),Bp(a,64));while(!BQ(a.ei)&&Cq(a.ei)){i=a.ei;k=i.ej;if(k&&k!=(-536870788)&&k!=(-536870871))break;Bv(d,V(i));i=a.ei;if(i.ev!=(-536870788))continue;V(i);}i=Fu(a,d);i.bY(h);}else if(a.ei.ev==(-536870788)){i=Cp(h);V(a.ei);}
else{i=IY(a,h);d=a.ei;if(d.ev==(-536870788))V(d);}if(i!==null)C2(e,i);if(BQ(a.ei))break;if(a.ei.ev==(-536870871))break;}if(a.ei.gb==(-536870788))C2(e,Cp(h));if(a.eQ!=f&&!g){a.eQ=f;d=a.ei;d.fe=f;d.ej=d.ev;d.e5=d.e0;g=d.eL;d.ek=g+1|0;d.fw=g;Cx(d);}switch(b){case -1073741784:break;case -536870872:d=new F4;Cn(d,e,h);return d;case -268435416:d=new Gx;Cn(d,e,h);return d;case -134217688:d=new HK;Cn(d,e,h);return d;case -67108824:d=new Hb;Cn(d,e,h);return d;case -33554392:d=new BZ;Cn(d,e,h);return d;default:switch(e.eC)
{case 0:break;case 1:return Ob(BT(e,0),h);default:return QX(e,h);}return Cp(h);}d=new D7;Cn(d,e,h);return d;},R4=a=>{let b,c,d,e,f,g,h;b=BC(4);c=(-1);d=(-1);if(!BQ(a.ei)&&Cq(a.ei)){e=b.data;c=V(a.ei);e[0]=c;d=c-4352|0;}if(d>=0&&d<19){e=BJ(3);b=e.data;b[0]=c&65535;f=a.ei;g=f.ev;h=g-4449|0;if(h>=0&&h<21){b[1]=g&65535;V(f);f=a.ei;g=f.ev;c=g-4519|0;if(c>=0&&c<28){b[2]=g&65535;V(f);return G$(e,3);}return G$(e,2);}if(!Bp(a,2))return Fx(b[0]);if(Bp(a,64))return IZ(b[0]);return Iz(b[0]);}e=b.data;c=1;while(c<4&&!BQ(a.ei)
&&Cq(a.ei)){h=c+1|0;e[c]=V(a.ei);c=h;}if(c==1){h=e[0];if(!(OH.b7(h)==J3?0:1))return IE(a,e[0]);}if(!Bp(a,2))return Ry(b,c);if(Bp(a,64)){f=new H9;ER(f,b,c);return f;}f=new Hy;ER(f,b,c);return f;},IY=(a,b)=>{let c,d,e,f,g,h,i;if(Cq(a.ei)&&!E1(a.ei)&&DM(a.ei.ej)){if(Bp(a,128)){c=R4(a);if(!BQ(a.ei)){d=a.ei;e=d.ev;if(!(e==(-536870871)&&!(b instanceof Cz))&&e!=(-536870788)&&!Cq(d))c=E8(a,b,c);}}else if(!HP(a.ei)&&!Ji(a.ei)){d=new DT;Y(d);while(!BQ(a.ei)&&Cq(a.ei)&&!HP(a.ei)&&!Ji(a.ei)){if(!(!E1(a.ei)&&!a.ei.ej)&&
!(!E1(a.ei)&&DM(a.ei.ej))){f=a.ei.ej;if(f!=(-536870871)&&(f&(-2147418113))!=(-2147483608)&&f!=(-536870788)&&f!=(-536870876))break;}e=V(a.ei);if(!EM(e))Bh(d,e&65535);else DR(d,CI(e));}if(!Bp(a,2)){c=new IA;BV(c);c.eG=X(d);e=d.er;c.eA=e;c.f6=Hc(e);c.f5=Hc(c.eA);g=0;while(g<(c.eA-1|0)){HR(c.f6,P(c.eG,g),(c.eA-g|0)-1|0);HR(c.f5,P(c.eG,(c.eA-g|0)-1|0),(c.eA-g|0)-1|0);g=g+1|0;}}else c=Bp(a,64)?Q8(d):Sc(d);}else c=E8(a,b,H8(a,b));}else{d=a.ei;if(d.ev!=(-536870871))c=E8(a,b,H8(a,b));else{if(b instanceof Cz)Q(Bl(I(13),
d.eE,d.eL));c=Cp(b);}}a:{if(!BQ(a.ei)){e=a.ei.ev;if(!(e==(-536870871)&&!(b instanceof Cz))&&e!=(-536870788)){h=IY(a,b);if(c instanceof BF&&!(c instanceof Ck)&&!(c instanceof BG)&&!(c instanceof Ci)){i=c;d=i;if(!h.ck(d.eo)){c=new GW;d=d.eo;i=i;Cc(c,d,i.eg,i.fC);c.eo.bY(c);}}if((h.cm()&65535)!=43)c.bY(h);else c.bY(h.eo);break a;}}if(c===null)return null;c.bY(b);}if((c.cm()&65535)!=43)return c;return c.eo;},E8=(a,b,c)=>{let d,e,f,g,h;d=a.ei;e=d.ev;if(c!==null&&!(c instanceof Bn)){switch(e){case -2147483606:V(d);d
=new JF;BP(d,c,b,e);Ct();c.bY(CW);return d;case -2147483605:V(d);d=new FG;BP(d,c,b,(-2147483606));Ct();c.bY(CW);return d;case -2147483585:V(d);d=new Jh;BP(d,c,b,(-536870849));Ct();c.bY(CW);return d;case -2147483525:f=new GG;d=CO(d);g=a.e1+1|0;a.e1=g;Dr(f,d,c,b,(-536870849),g);Ct();c.bY(CW);return f;case -1073741782:case -1073741781:V(d);d=new Iv;BP(d,c,b,e);c.bY(d);return d;case -1073741761:V(d);d=new G8;BP(d,c,b,(-536870849));c.bY(b);return d;case -1073741701:f=new IP;d=CO(d);e=a.e1+1|0;a.e1=e;Dr(f,d,c,b,(-536870849),
e);c.bY(f);return f;case -536870870:case -536870869:V(d);if(c.cm()!=(-2147483602)){d=new BG;BP(d,c,b,e);}else if(Bp(a,32)){d=new Iw;BP(d,c,b,e);}else{d=new H$;f=Fr(a.eQ);BP(d,c,b,e);d.ga=f;}c.bY(d);return d;case -536870849:V(d);d=new CN;BP(d,c,b,(-536870849));c.bY(b);return d;case -536870789:f=new Cf;d=CO(d);e=a.e1+1|0;a.e1=e;Dr(f,d,c,b,(-536870849),e);c.bY(f);return f;default:}return c;}h=null;if(c!==null)h=c;switch(e){case -2147483606:case -2147483605:V(d);d=new JH;Cc(d,h,b,e);h.eg=d;return d;case -2147483585:V(d);c
=new GN;Cc(c,h,b,(-2147483585));return c;case -2147483525:c=new IX;Fp(c,CO(d),h,b,(-2147483525));return c;case -1073741782:case -1073741781:V(d);d=new G4;Cc(d,h,b,e);h.eg=d;return d;case -1073741761:V(d);c=new HG;Cc(c,h,b,(-1073741761));return c;case -1073741701:c=new HL;Fp(c,CO(d),h,b,(-1073741701));return c;case -536870870:case -536870869:V(d);d=PI(h,b,e);h.eg=d;return d;case -536870849:V(d);c=new Ci;Cc(c,h,b,(-536870849));return c;case -536870789:return KA(CO(d),h,b,(-536870789));default:}return c;},H8=(a,
b)=>{let c,d,e,f,g,h,i,j;c=null;d=b instanceof Cz;while(true){a:{e=a.ei;f=e.ev;if((f&(-2147418113))==(-2147483608)){V(e);g=(f&16711680)>>16;f=f&(-16711681);if(f==(-16777176))a.eQ=g;else{if(f!=(-1073741784))g=a.eQ;c=I3(a,f,g,b);e=a.ei;if(e.ev!=(-536870871))Q(Bl(I(13),e.eE,e.eL));V(e);}}else{b:{c:{switch(f){case -2147483599:case -2147483598:case -2147483597:case -2147483596:case -2147483595:case -2147483594:case -2147483593:case -2147483592:case -2147483591:break c;case -2147483583:break;case -2147483582:V(e);c
=GR(0);break a;case -2147483577:V(e);c=new H1;Bm(c);break a;case -2147483558:V(e);c=new EO;h=a.ey+1|0;a.ey=h;Gu(c,h);break a;case -2147483550:V(e);c=GR(1);break a;case -2147483526:V(e);c=new Hi;Bm(c);break a;case -536870876:V(e);a.ey=a.ey+1|0;if(Bp(a,8)){if(Bp(a,1)){c=Oc(a.ey);break a;}c=Lw(a.ey);break a;}if(Bp(a,1)){c=JZ(a.ey);break a;}c=Kv(a.ey);break a;case -536870866:V(e);if(Bp(a,32)){c=Ov();break a;}c=Q0(Fr(a.eQ));break a;case -536870821:V(e);i=0;c=a.ei;if(c.ev==(-536870818)){i=1;V(c);}c=Fu(a,C3(a,i));c.bY(b);e
=a.ei;if(e.ev!=(-536870819))Q(Bl(I(13),e.eE,e.eL));IC(e,1);V(a.ei);break a;case -536870818:V(e);a.ey=a.ey+1|0;if(!Bp(a,8)){c=new EU;Bm(c);break a;}c=new GZ;e=Fr(a.eQ);Bm(c);c.gB=e;break a;case 0:j=e.e0;if(j!==null)c=Fu(a,j);else{if(BQ(e)){c=Cp(b);break a;}c=Fx(f&65535);}V(a.ei);break a;default:break b;}V(e);c=new EU;Bm(c);break a;}h=(f&2147483647)-48|0;if(a.fa<h)Q(Bl(I(13),Ca(e),OF(a.ei)));V(e);a.ey=a.ey+1|0;c=!Bp(a,2)?MI(h,a.ey):Bp(a,64)?Od(h,a.ey):Qq(h,a.ey);a.gt.data[h].gr=1;a.hb=1;break a;}if(f>=0&&!C_(e))
{c=IE(a,f);V(a.ei);}else if(f==(-536870788))c=Cp(b);else{if(f!=(-536870871)){b=new CB;c=!C_(a.ei)?HA(f&65535):a.ei.e0.g();e=a.ei;DB(b,c,e.eE,e.eL);Q(b);}if(d){b=new CB;e=a.ei;DB(b,I(13),e.eE,e.eL);Q(b);}c=Cp(b);}}}if(f!=(-16777176))break;}return c;},C3=(a,b)=>{let c,d,e,f,g,h,i,j,$$je;c=Hk(Bp(a,2),Bp(a,64));Cb(c,b);d=(-1);e=0;f=0;g=1;a:{b:{c:while(true){if(BQ(a.ei))break a;h=a.ei;b=h.ev;f=b==(-536870819)&&!g?0:1;if(!f)break a;d:{switch(b){case -536870874:if(d>=0)Bv(c,d);d=V(a.ei);h=a.ei;if(h.ev!=(-536870874))
{d=38;break d;}if(h.ej==(-536870821)){V(h);e=1;d=(-1);break d;}V(h);if(g){c=C3(a,0);break d;}if(a.ei.ev==(-536870819))break d;Hm(c,C3(a,0));break d;case -536870867:if(!g){b=h.ej;if(b!=(-536870819)&&b!=(-536870821)&&d>=0){V(h);h=a.ei;i=h.ev;if(C_(h))break c;if(i<0){j=a.ei.ej;if(j!=(-536870819)&&j!=(-536870821)&&d>=0)break c;}e:{try{if(DM(i))break e;i=i&65535;break e;}catch($$e){$$je=BS($$e);if($$je instanceof Cr){break b;}else{throw $$e;}}}try{Bk(c,d,i);}catch($$e){$$je=BS($$e);if($$je instanceof Cr){break b;}
else{throw $$e;}}V(a.ei);d=(-1);break d;}}if(d>=0)Bv(c,d);d=45;V(a.ei);break d;case -536870821:if(d>=0){Bv(c,d);d=(-1);}V(a.ei);j=0;h=a.ei;if(h.ev==(-536870818)){V(h);j=1;}if(!e)Kz(c,C3(a,j));else Hm(c,C3(a,j));e=0;V(a.ei);break d;case -536870819:if(d>=0)Bv(c,d);d=93;V(a.ei);break d;case -536870818:if(d>=0)Bv(c,d);d=94;V(a.ei);break d;case 0:if(d>=0)Bv(c,d);h=a.ei.e0;if(h===null)d=0;else{RC(c,h);d=(-1);}V(a.ei);break d;default:}if(d>=0)Bv(c,d);d=V(a.ei);}g=0;}Q(Bl(I(13),Fv(a),a.ei.eL));}Q(Bl(I(13),Fv(a),a.ei.eL));}if
(!f){if(d>=0)Bv(c,d);return c;}Q(Bl(I(13),Fv(a),a.ei.eL-1|0));},IE=(a,b)=>{let c,d,e;c=EM(b);if(Bp(a,2)){a:{if(!(b>=97&&b<=122)){if(b<65)break a;if(b>90)break a;}return Iz(b&65535);}if(Bp(a,64)&&b>128){if(c){d=new FR;BV(d);d.eA=2;d.gs=CV(Dh(b));return d;}if(Fj(b))return Hj(b&65535);if(!Ei(b))return IZ(b&65535);return H5(b&65535);}}if(!c){if(Fj(b))return Hj(b&65535);if(!Ei(b))return Fx(b&65535);return H5(b&65535);}d=new B7;BV(d);d.eA=2;d.fg=b;e=(CI(b)).data;d.fz=e[0];d.fp=e[1];return d;},Fu=(a,b)=>{let c,d,e;if
(!KM(b)){if(!b.eq){if(b.cJ())return Hu(b);return GS(b);}if(!b.cJ())return IS(b);c=new Em;E2(c,b);return c;}c=Nf(b);d=new Ga;Bm(d);d.gg=c;d.gH=c.ew;if(!b.eq){if(b.cJ())return EZ(Hu(DU(b)),d);return EZ(GS(DU(b)),d);}if(!b.cJ())return EZ(IS(DU(b)),d);c=new Ex;e=new Em;E2(e,DU(b));Iy(c,e,d);return c;},RJ=b=>{return FT(b,0);},Da=b=>{if(b>=97&&b<=122)b=(b-32|0)&65535;else if(b>=65&&b<=90)b=(b+32|0)&65535;return b;},Bp=(a,b)=>{return (a.eQ&b)!=b?0:1;},EC=L(0);
function FL(){let a=this;J.call(a);a.ir=null;a.gS=null;a.fk=null;a.eI=null;a.fI=0;a.f9=0;a.gl=0;a.ho=null;a.fV=null;a.e2=null;}
let Gm=(a,b,c)=>{let d,e;a.fV=OB(a,c);c=a.fk;d=a.gl;e=C$(a);Fo(b,BW(c,d,e));B4(b,a.fV);a.gl=Dq(a);return a;},OB=(a,b)=>{let c,d,e,f,g,h,i,j,k,l,$$je;c=a.ho;if(c!==null&&C1(c,b)){if(a.e2===null)return a.fV;d=new W;Y(d);e=0;while(true){b=a.e2;if(e>=b.eC)break;T(d,BT(b,e));e=e+1|0;}return X(d);}a.ho=b;f=D4(b);c=new W;Y(c);a.e2=null;g=0;h=0;i=0;a:{b:while(true){j=f.data;e=j.length;if(g>=e){b=a.e2;if(b!==null){k=c.er;if(h!=k)C2(b,Jb(c,h,k));}return X(c);}if(j[g]==92&&!i){i=1;g=g+1|0;}c:{if(i){if(g>=e)break b;Bh(c,
j[g]);i=0;}else if(j[g]!=36)Bh(c,j[g]);else{if(a.e2===null)a.e2=HB();d:{try{b=new Br;g=g+1|0;Jy(b,f,g,1);k=RS(b);if(h==C5(c))break d;C2(a.e2,Jb(c,h,C5(c)));h=C5(c);break d;}catch($$e){$$je=BS($$e);if($$je instanceof Cr){break a;}else{throw $$e;}}}try{C2(a.e2,J8(a,k));l=Ep(a,k);h=h+Dz(l)|0;Lf(c,l);break c;}catch($$e){$$je=BS($$e);if($$je instanceof Cr){break a;}else{throw $$e;}}}}g=g+1|0;}b=new Bo;Z(b);Q(b);}b=new Bs;Bu(b,I(13));Q(b);},GK=(a,b)=>{let c,d;c=a.fk;d=a.gl;c=c;Fo(b,BW(c,d,c.eh.length));return b;},Ep
=(a,b)=>{let c,d,e;c=a.eI;if(D9(c,b)<0)c=null;else{d=c.fS;e=D9(c,b);b=Fn(c,b);c=BW(d,e,b);}return c;},F5=(a,b)=>{let c,d,e;c=a.fk.eh.length;if(b>=0&&b<=c){IG(a.eI,null,(-1),(-1));d=a.eI;d.fE=1;d.eW=b;c=d.fr;if(c<0)c=b;d.fr=c;b=a.gS.cU(b,a.fk,d);if(b==(-1))a.eI.eP=1;if(b>=0){d=a.eI;if(d.fQ){e=d.eS.data;if(e[0]==(-1)){c=d.eW;e[0]=c;e[1]=c;}d.fr=Ej(d);return 1;}}a.eI.eW=(-1);return 0;}d=new Bo;Bu(d,Hg(b));Q(d);},G6=a=>{let b,c,d;b=a.fk.eh.length;c=a.eI;if(!c.fG)b=a.f9;if(c.eW>=0&&c.fE==1){c.eW=Ej(c);if(Ej(a.eI)
==D9(a.eI,0)){c=a.eI;c.eW=c.eW+1|0;}d=a.eI.eW;return d<=b&&F5(a,d)?1:0;}return F5(a,a.fI);},Fq=(a,b)=>{return D9(a.eI,b);},Eu=(a,b)=>{return Fn(a.eI,b);},C$=a=>{return Fq(a,0);},Dq=a=>{return Eu(a,0);},DT=L(Cl),NH=a=>{Y(a);},QH=()=>{let a=new DT();NH(a);return a;},LI=(a,b,c,d,e)=>{GA(a,b,c,d,e);return a;},OJ=(a,b,c,d)=>{HQ(a,b,c,d);return a;},Ms=(a,b,c,d,e)=>{Gr(a,b,c,d,e);return a;},Q7=(a,b,c,d)=>{E3(a,b,c,d);return a;},I$=a=>{return a.er;},PM=(a,b)=>{EQ(a,b);},Ly=(a,b,c)=>{In(a,b,c);return a;},K2=(a,b,c)=>
{Eh(a,b,c);return a;},Bo=L(Bi);
function Jf(){let a=this;J.call(a);a.eS=null;a.fM=null;a.gv=null;a.fS=null;a.gz=0;a.fQ=0;a.eK=0;a.el=0;a.eW=0;a.fG=0;a.e8=0;a.eP=0;a.hW=0;a.fr=0;a.fE=0;a.ic=null;}
let Bf=(a,b,c)=>{a.fM.data[b]=c;},BU=(a,b)=>{return a.fM.data[b];},Ej=a=>{return Fn(a,0);},Fn=(a,b)=>{IO(a,b);return a.eS.data[(b*2|0)+1|0];},B0=(a,b,c)=>{a.eS.data[b*2|0]=c;},ES=(a,b,c)=>{a.eS.data[(b*2|0)+1|0]=c;},CL=(a,b)=>{return a.eS.data[b*2|0];},D6=(a,b)=>{return a.eS.data[(b*2|0)+1|0];},D9=(a,b)=>{IO(a,b);return a.eS.data[b*2|0];},Gf=(a,b)=>{return a.gv.data[b];},Cd=(a,b,c)=>{a.gv.data[b]=c;},IO=(a,b)=>{let c;if(!a.fQ){c=new GI;Z(c);Q(c);}if(b>=0&&b<a.gz)return;c=new Bo;Bu(c,Hg(b));Q(c);},IG=(a,b,c,
d)=>{a.fQ=0;a.fE=2;DS(a.eS,(-1));DS(a.fM,(-1));if(b!==null)a.fS=b;if(c>=0){a.eK=c;a.el=d;}a.eW=a.eK;},Bs=L(Bi);
function CB(){let a=this;Bs.call(a);a.gp=null;a.f3=null;a.fv=0;}
let DB=(a,b,c,d)=>{Z(a);a.fv=(-1);a.gp=b;a.f3=c;a.fv=d;},Bl=(a,b,c)=>{let d=new CB();DB(d,a,b,c);return d;},OC=L(),Bw=(b,c)=>{if(b<c)c=b;return c;},BR=(b,c)=>{if(b>c)c=b;return c;};
function H2(){J.call(this);this.fO=null;}
let LC=a=>{let b;b=new W;Y(b);a.fO=b;},FX=()=>{let a=new H2();LC(a);return a;},B5=(a,b)=>{let c;c=BY(a.fO,b.eh.length);Bh(c,58);B4(c,b);},ED=(a,b)=>{B5(a,C7(b));},FE=(a,b)=>{B5(a,!b?I(0):I(21));},HX=a=>{return X(a.fO);},Do=L(Bi);
function Bd(){let a=this;J.call(a);a.eg=null;a.eH=0;a.fW=null;a.fC=0;}
let CD=0,Dc=()=>{Dc=B3(Bd);QK();},Bm=a=>{let b;Dc();b=CD;CD=b+1|0;a.fW=C7(b);},Je=(a,b)=>{let c;Dc();c=CD;CD=c+1|0;a.fW=C7(c);a.eg=b;},Dg=(a,b,c,d)=>{let e;e=d.el;while(true){if(b>e)return (-1);if(a.cY(b,c,d)>=0)break;b=b+1|0;}return b;},CY=(a,b,c,d,e)=>{while(true){if(c<b)return (-1);if(a.cY(c,d,e)>=0)break;c=c+(-1)|0;}return c;},So=(a,b)=>{a.fC=b;},Pg=a=>{return a.fC;},Lu=a=>{let b,c,d;b=a.fW;c=a.L();d=new W;Y(d);Bh(d,60);b=T(d,b);Bh(b,58);Bh(T(b,c),62);return X(d);},Rl=a=>{return Lu(a);},Sg=a=>{return a.eg;},MU
=(a,b)=>{a.eg=b;},MT=(a,b)=>{return 1;},PA=a=>{return null;},Dm=a=>{let b;a.eH=1;b=a.eg;if(b!==null){if(!b.eH){b=b.c0();if(b!==null){a.eg.eH=1;a.eg=b;}a.eg.bP();}else if(b instanceof CM&&b.eX.gr)a.eg=b.eg;}},QK=()=>{CD=1;},GF=L(0),DQ=L(0),CU=L(),I4=L(0),Ge=L(0);
function C6(){CU.call(this);this.eZ=0;}
let CS=L(0),EV=L(0);
function HD(){let a=this;C6.call(a);a.eU=null;a.eC=0;}
let L5=a=>{a.eU=K(J,10);},HB=()=>{let a=new HD();L5(a);return a;},G3=(a,b)=>{let c,d;c=a.eU.data.length;if(c<b){d=c>=1073741823?2147483647:BR(b,BR(c*2|0,5));a.eU=H3(a.eU,d);}},BT=(a,b)=>{Gb(a,b);return a.eU.data[b];},C2=(a,b)=>{let c,d;G3(a,a.eC+1|0);c=a.eU.data;d=a.eC;a.eC=d+1|0;c[d]=b;a.eZ=a.eZ+1|0;return 1;},Gb=(a,b)=>{let c;if(b>=0&&b<a.eC)return;c=new Bo;Z(c);Q(c);};
function Gv(){let a=this;J.call(a);a.hy=0;a.ib=0;a.hJ=null;}
let Sa=(a,b,c)=>{a.hJ=b;a.ib=c;a.hy=c;},J8=(a,b)=>{let c=new Gv();Sa(c,a,b);return c;},R$=a=>{return Ep(a.hJ,a.hy);},PZ=L(Bi);
function By(){let a=this;Bd.call(a);a.gr=0;a.eT=0;}
let CW=null,Ct=()=>{Ct=B3(By);K7();},CE=(a,b)=>{Ct();Bm(a);a.eT=b;},RU=a=>{let b=new By();CE(b,a);return b;},Nn=(a,b,c,d)=>{let e,f;e=D6(d,a.eT);ES(d,a.eT,b);f=a.eg.cY(b,c,d);if(f<0)ES(d,a.eT,e);return f;},Sh=a=>{return a.eT;},NS=a=>{return I(41);},OM=(a,b)=>{return 0;},K7=()=>{let b;b=new HT;Bm(b);CW=b;};
function Hs(){let a=this;J.call(a);a.et=null;a.fe=0;a.eY=0;a.g1=0;a.gb=0;a.ev=0;a.ej=0;a.hr=null;a.gK=0;a.e0=null;a.e5=null;a.ek=0;a.fx=0;a.eL=0;a.fw=0;a.eE=null;}
let K8=null,OH=null,J3=0,IC=(a,b)=>{if(b>0&&b<3)a.eY=b;if(b==1){a.ej=a.ev;a.e5=a.e0;a.ek=a.fw;a.fw=a.eL;Cx(a);}},C_=a=>{return a.e0===null?0:1;},E1=a=>{return a.e5===null?0:1;},V=a=>{Cx(a);return a.gb;},CO=a=>{let b;b=a.e0;Cx(a);return b;},Cx=a=>{let b,c,d,e,f,g,h,i,$$je;a.gb=a.ev;a.ev=a.ej;a.e0=a.e5;a.eL=a.fw;a.fw=a.ek;a:{while(true){b=0;c=a.ek>=a.et.data.length?0:Fs(a);a.ej=c;a.e5=null;if(a.eY==4){if(c!=92)return;c=a.ek;d=a.et.data;c=c>=d.length?0:d[Bg(a)];a.ej=c;switch(c){case 69:break;default:a.ej=92;a.ek
=a.fx;return;}a.eY=a.g1;a.ej=a.ek>(a.et.data.length-2|0)?0:Fs(a);}b:{c=a.ej;if(c==92){c=a.ek>=(a.et.data.length-2|0)?(-1):Fs(a);c:{a.ej=c;switch(c){case -1:Q(Bl(I(13),Ca(a),a.ek));case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:case 8:case 9:case 10:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:case 23:case 24:case 25:case 26:case 27:case 28:case 29:case 30:case 31:case 32:case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 118:break;case 48:a.ej
=Na(a);break b;case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:if(a.eY!=1)break b;a.ej=(-2147483648)|c;break b;case 65:a.ej=(-2147483583);break b;case 66:a.ej=(-2147483582);break b;case 67:case 69:case 70:case 72:case 73:case 74:case 75:case 76:case 77:case 78:case 79:case 82:case 84:case 85:case 86:case 88:case 89:case 103:case 104:case 105:case 106:case 107:case 108:case 109:case 111:case 113:case 121:Q(Bl(I(13),Ca(a),a.ek));case 68:case 83:case 87:case 100:case 115:case 119:a.e5=Ja(D3(a.et,
a.fx,1),0);a.ej=0;break b;case 71:a.ej=(-2147483577);break b;case 80:case 112:break c;case 81:a.g1=a.eY;a.eY=4;b=1;break b;case 90:a.ej=(-2147483558);break b;case 97:a.ej=7;break b;case 98:a.ej=(-2147483550);break b;case 99:c=a.ek;d=a.et.data;if(c>=(d.length-2|0))Q(Bl(I(13),Ca(a),a.ek));a.ej=d[Bg(a)]&31;break b;case 101:a.ej=27;break b;case 102:a.ej=12;break b;case 110:a.ej=10;break b;case 114:a.ej=13;break b;case 116:a.ej=9;break b;case 117:a.ej=FK(a,4);break b;case 120:a.ej=FK(a,2);break b;case 122:a.ej=(-2147483526);break b;default:}break b;}e
=L8(a);f=0;if(a.ej==80)f=1;try{a.e5=Ja(e,f);}catch($$e){$$je=BS($$e);if($$je instanceof Et){Q(Bl(I(13),Ca(a),a.ek));}else{throw $$e;}}a.ej=0;}else{g=a.eY;if(g==1)switch(c){case 36:a.ej=(-536870876);break b;case 40:if(a.et.data[a.ek]!=63){a.ej=(-2147483608);break b;}Bg(a);c=a.et.data[a.ek];g=0;h=null;while(true){d:{if(!g){switch(c){case 33:break;case 60:Bg(a);c=a.et.data[a.ek];g=1;break d;case 61:a.ej=(-536870872);Bg(a);break d;case 62:a.ej=(-33554392);Bg(a);break d;default:i=QQ(a);a.ej=i;if(i<256){a.fe=i;i=
i<<16;a.ej=i;a.ej=(-1073741784)|i;break d;}i=i&255;a.ej=i;a.fe=i;i=i<<16;a.ej=i;a.ej=(-16777176)|i;break d;}a.ej=(-268435416);Bg(a);}else{e:{switch(c){case 33:break;case 61:g=0;a.ej=(-134217688);Bg(a);break d;case 62:if(h===null)Q(Bl(I(13),Ca(a),a.ek));a.hr=C9(h);Bg(a);h=null;g=0;a.ej=(-2130706392);break d;default:break e;}g=0;a.ej=(-67108824);Bg(a);break d;}f:{if(!(c>=65&&c<=90)){if(c<97)break f;if(c>122)break f;}if(h===null)h=L7();F$(h,c);Bg(a);c=a.et.data[a.ek];break d;}if(c<48)break a;if(c>57)break a;if
(h===null)Q(Bl(I(42),Ca(a),a.ek));F$(h,c);Bg(a);c=a.et.data[a.ek];}}if(!g)break;}break b;case 41:a.ej=(-536870871);break b;case 42:case 43:case 63:g=a.ek;d=a.et.data;switch(g>=d.length?42:d[g]){case 43:a.ej=c|(-2147483648);Bg(a);break b;case 63:a.ej=c|(-1073741824);Bg(a);break b;default:}a.ej=c|(-536870912);break b;case 46:a.ej=(-536870866);break b;case 91:a.ej=(-536870821);IC(a,2);break b;case 93:if(g!=2)break b;a.ej=(-536870819);break b;case 94:a.ej=(-536870818);break b;case 123:a.e5=Ne(a,c);break b;case 124:a.ej
=(-536870788);break b;default:}else if(g==2)switch(c){case 38:a.ej=(-536870874);break b;case 45:a.ej=(-536870867);break b;case 91:a.ej=(-536870821);break b;case 93:a.ej=(-536870819);break b;case 94:a.ej=(-536870818);break b;default:}}}if(b)continue;else break;}return;}Q(Bl(I(13),Ca(a),a.ek));},L8=a=>{let b,c,d,e,f,g;b=new W;CX(b,10);c=a.ek;d=a.et;e=d.data;if(c<(e.length-2|0)){if(e[c]!=123){b=D3(d,Bg(a),1);f=new W;Y(f);T(T(f,I(43)),b);return X(f);}Bg(a);c=0;a:{while(true){g=a.ek;d=a.et.data;if(g>=(d.length-2
|0))break;c=d[Bg(a)];if(c==125)break a;Bh(b,c);}}if(c!=125)Q(Bl(I(13),a.eE,a.ek));}if(!b.er)Q(Bl(I(13),a.eE,a.ek));f=X(b);if(f.eh.length==1){b=new W;Y(b);T(T(b,I(43)),f);return X(b);}b:{c:{if(f.eh.length>3){if(II(f,I(43)))break c;if(II(f,I(44)))break c;}break b;}f=DV(f,2);}return f;},Ne=(a,b)=>{let c,d,e,f,g,$$je;c=new W;CX(c,4);d=(-1);e=2147483647;a:{while(true){f=a.ek;g=a.et.data;if(f>=g.length)break a;b=g[Bg(a)];if(b==125)break a;if(b==44&&d<0)try{d=DN(C9(c),10);N6(c,0,C5(c));continue;}catch($$e){$$je=BS($$e);if
($$je instanceof BK){break;}else{throw $$e;}}Bh(c,b&65535);}Q(Bl(I(13),a.eE,a.ek));}if(b!=125)Q(Bl(I(13),a.eE,a.ek));if(c.er>0)b:{try{e=DN(C9(c),10);if(d>=0)break b;d=e;break b;}catch($$e){$$je=BS($$e);if($$je instanceof BK){Q(Bl(I(13),a.eE,a.ek));}else{throw $$e;}}}else if(d<0)Q(Bl(I(13),a.eE,a.ek));if((d|e|(e-d|0))<0)Q(Bl(I(13),a.eE,a.ek));b=a.ek;g=a.et.data;f=b>=g.length?42:g[b];c:{switch(f){case 43:a.ej=(-2147483525);Bg(a);break c;case 63:a.ej=(-1073741701);Bg(a);break c;default:}a.ej=(-536870789);}c=new Gt;c.e7
=d;c.e4=e;return c;},Ca=a=>{return a.eE;},BQ=a=>{return !a.ev&&!a.ej&&a.ek==a.gK&&!C_(a)?1:0;},DM=b=>{return b<0?0:1;},Cq=a=>{return !BQ(a)&&!C_(a)&&DM(a.ev)?1:0;},HP=a=>{let b;b=a.ev;return b<=56319&&b>=55296?1:0;},Ji=a=>{let b;b=a.ev;return b<=57343&&b>=56320?1:0;},Ei=b=>{return b<=56319&&b>=55296?1:0;},Fj=b=>{return b<=57343&&b>=56320?1:0;},FK=(a,b)=>{let c,d,e,f,$$je;c=new W;CX(c,b);d=a.et.data.length-2|0;e=0;while(true){f=B1(e,b);if(f>=0)break;if(a.ek>=d)break;Bh(c,a.et.data[Bg(a)]);e=e+1|0;}if(!f)a:{try
{b=DN(C9(c),16);}catch($$e){$$je=BS($$e);if($$je instanceof BK){break a;}else{throw $$e;}}return b;}Q(Bl(I(13),a.eE,a.ek));},Na=a=>{let b,c,d,e,f,g;b=3;c=1;d=a.et.data;e=d.length-2|0;f=Fc(d[a.ek],8);switch(f){case -1:break;default:if(f>3)b=2;Bg(a);a:{while(true){if(c>=b)break a;g=a.ek;if(g>=e)break a;g=Fc(a.et.data[g],8);if(g<0)break;f=(f*8|0)+g|0;Bg(a);c=c+1|0;}}return f;}Q(Bl(I(13),a.eE,a.ek));},QQ=a=>{let b,c,d,e;b=1;c=a.fe;a:while(true){d=a.ek;e=a.et.data;if(d>=e.length)Q(Bl(I(13),a.eE,d));b:{c:{switch(e[d])
{case 41:Bg(a);return c|256;case 45:if(!b)Q(Bl(I(13),a.eE,d));b=0;break b;case 58:break a;case 100:break c;case 105:c=b?c|2:(c^2)&c;break b;case 109:c=b?c|8:(c^8)&c;break b;case 115:c=b?c|32:(c^32)&c;break b;case 117:c=b?c|64:(c^64)&c;break b;case 120:c=b?c|4:(c^4)&c;break b;default:}break b;}c=b?c|1:(c^1)&c;}Bg(a);}Bg(a);return c;},Bg=a=>{let b,c,d,e,f;b=a.ek;a.fx=b;if(!(a.fe&4))a.ek=b+1|0;else{c=a.et.data.length-2|0;a.ek=b+1|0;a:while(true){d=a.ek;if(d<c){d=a.et.data[d];Bb();if(HI(d)){a.ek=a.ek+1|0;continue;}}d
=a.ek;if(d>=c)break;e=a.et.data;if(e[d]!=35)break;a.ek=d+1|0;while(true){f=a.ek;if(f>=c)continue a;b=e[f];if(b!=10&&b!=13&&b!=133&&(b|1)!=8233?0:1)continue a;a.ek=f+1|0;}}}return a.fx;},GV=b=>{return K8.df(b);},Fs=a=>{let b,c,d,e;b=a.et.data[Bg(a)];if(BA(b)){c=a.fx+1|0;d=a.et.data;if(c<d.length){e=d[c];if(BI(e)){Bg(a);return Cm(b,e);}}}return b;},OF=a=>{return a.eL;},ND=L(),EK=(b,c)=>{let d,e,f,g;b=b.data;d=I0(c);e=d.data;f=Bw(c,b.length);g=0;while(g<f){e[g]=b[g];g=g+1|0;}return d;},H3=(b,c)=>{let d,e,f,g;d
=Ek(Co(b));if(d===null){d=new Do;Z(d);Q(d);}if(d===B9(N$)){d=new Bs;Z(d);Q(d);}if(c<0){d=new JD;Z(d);Q(d);}b=b.data;e=KP(d.eJ,c);f=Bw(c,b.length);g=0;while(g<f){e.data[g]=b[g];g=g+1|0;}return e;},Gi=(b,c,d,e)=>{let f,g,h;if(c>d){f=new Bs;Z(f);Q(f);}while(c<d){g=b.data;h=c+1|0;g[c]=e;c=h;}},DS=(b,c)=>{Gi(b,0,b.data.length,c);},CJ=L(Bo),OL=L(),Se=(b,c,d,e,f)=>{let g,h,i,j,k,l,m,n;if(b!==null&&d!==null){if(c>=0&&e>=0&&f>=0&&(c+f|0)<=DZ(b)&&(e+f|0)<=DZ(d)){a:{b:{if(b!==d){g=Ek(Co(b));h=Ek(Co(d));if(g!==null&&h!==
null){if(g===h)break b;if(!DI(g)&&!DI(h)){i=b;j=0;k=c;while(j<f){c:{l=i.data;m=k+1|0;n=l[k];if(n!==null){n=Co(n);if(Di(n.eJ,h.eJ)){k=1;break c;}}k=0;}if(!k){D8(b,c,d,e,j);b=new DJ;Z(b);Q(b);}j=j+1|0;k=m;}D8(b,c,d,e,f);return;}if(!DI(g))break a;if(DI(h))break b;else break a;}b=new DJ;Z(b);Q(b);}}D8(b,c,d,e,f);return;}b=new DJ;Z(b);Q(b);}b=new Bo;Z(b);Q(b);}d=new Do;Bu(d,I(45));Q(d);},G2=(b,c,d,e,f)=>{if(c>=0&&e>=0&&f>=0&&(c+f|0)<=DZ(b)&&(e+f|0)<=DZ(d)){D8(b,c,d,e,f);return;}b=new Bo;Z(b);Q(b);},D8=(b,c,d,e,f)=>
{if(f!==0){if(typeof b.data.buffer!=='undefined'){d.data.set(b.data.subarray(c,c+f),e);}else if(b!==d||e<c){for(let i=0;i<f;i=i+1|0){d.data[e++]=b.data[c++];}}else {c=c+f|0;e=e+f|0;for(let i=0;i<f;i=i+1|0){d.data[ --e]=b.data[ --c];}}}},IN=L(By),LP=(a,b,c,d)=>{let e;e=a.eT;Bf(d,e,b-BU(d,e)|0);return a.eg.cY(b,c,d);},Ra=a=>{return I(46);},R5=(a,b)=>{return 0;},IK=L(By),QI=(a,b,c,d)=>{return b;},P0=a=>{return I(47);},FQ=L(By),O$=(a,b,c,d)=>{if(BU(d,a.eT)!=b)b=(-1);return b;},MD=a=>{return I(48);};
function Gc(){By.call(this);this.g4=0;}
let L_=(a,b,c,d)=>{let e;e=a.eT;Bf(d,e,b-BU(d,e)|0);a.g4=b;return b;},KG=a=>{return I(49);},Or=(a,b)=>{return 0;},Cz=L(By),Nu=(a,b,c,d)=>{if(d.fE!=1&&b!=d.el)return (-1);d.fQ=1;ES(d,0,b);return b;},PN=a=>{return I(50);};
function Bn(){Bd.call(this);this.eA=0;}
let BV=a=>{Bm(a);a.eA=1;},QV=(a,b,c,d)=>{let e;if((b+a.dq()|0)>d.el){d.eP=1;return (-1);}e=a.dr(b,c);if(e<0)return (-1);return a.eg.cY(b+e|0,c,d);},LB=a=>{return a.eA;},PS=(a,b)=>{return 1;},Gp=L(Bn),Lg=(a,b)=>{Je(a,b);a.eA=1;a.fC=1;a.eA=0;},Cp=a=>{let b=new Gp();Lg(b,a);return b;},Kl=(a,b,c)=>{return 0;},Ls=(a,b,c,d)=>{let e,f,g,h;e=d.el;f=d.eK;while(true){g=B1(b,e);if(g>0)return (-1);if(g<0){h=c;if(BI(P(h,b))&&b>f&&BA(P(h,b-1|0))){b=b+1|0;continue;}}if(a.eg.cY(b,c,d)>=0)break;b=b+1|0;}return b;},Qp=(a,b,c,
d,e)=>{let f,g,h;f=e.el;g=e.eK;while(true){if(c<b)return (-1);if(c<f){h=d;if(BI(P(h,c))&&c>g&&BA(P(h,c-1|0))){c=c+(-1)|0;continue;}}if(a.eg.cY(c,d,e)>=0)break;c=c+(-1)|0;}return c;},NU=a=>{return I(51);},L3=(a,b)=>{return 0;};
function Bj(){let a=this;Bd.call(a);a.eB=null;a.eX=null;a.es=0;}
let Cn=(a,b,c)=>{Bm(a);a.eB=b;a.eX=c;a.es=c.eT;},QX=(a,b)=>{let c=new Bj();Cn(c,a,b);return c;},MY=(a,b,c,d)=>{let e,f,g,h;if(a.eB===null)return (-1);e=CL(d,a.es);B0(d,a.es,b);f=a.eB.eC;g=0;while(true){if(g>=f){B0(d,a.es,e);return (-1);}h=(BT(a.eB,g)).cY(b,c,d);if(h>=0)break;g=g+1|0;}return h;},Oh=(a,b)=>{a.eX.eg=b;},P9=a=>{return I(52);},RY=(a,b)=>{let c,d,e,f;a:{c=a.eB;if(c!==null){c=c;d=new He;d.hp=c;d.hf=c.eZ;d.hk=c.eC;d.gO=(-1);d=d;while(true){e=d.hd;if(!(e>=d.hk?0:1))break a;f=d.hf;c=d.hp;if(f!=c.eZ){b
=new GY;Z(b);Q(b);}d.gO=e;d.hd=e+1|0;if(!(BT(c,e)).ck(b))continue;else break;}return 1;}}return 0;},Qe=(a,b)=>{return D6(b,a.es)>=0&&CL(b,a.es)==D6(b,a.es)?0:1;},Qk=a=>{let b,c,d,e,f,g,h,i,j;a.eH=1;b=a.eX;if(b!==null&&!b.eH)Dm(b);a:{b=a.eB;if(b!==null){c=b.eC;d=0;while(true){if(d>=c)break a;b=BT(a.eB,d);e=b.c0();if(e===null)e=b;else{b.eH=1;f=a.eB;Gb(f,d);g=f.eC-1|0;f.eC=g;h=d;while(h<g){i=f.eU.data;j=h+1|0;i[h]=i[j];h=j;}f.eU.data[g]=null;f.eZ=f.eZ+1|0;f=a.eB;if(d<0)break;j=f.eC;if(d>j)break;G3(f,j+1|0);h=f.eC;g
=h;while(g>d){i=f.eU.data;i[g]=i[g-1|0];g=g+(-1)|0;}f.eU.data[d]=e;f.eC=h+1|0;f.eZ=f.eZ+1|0;}if(!e.eH)e.bP();d=d+1|0;}b=new Bo;Z(b);Q(b);}}if(a.eg!==null)Dm(a);},D7=L(Bj),NF=(a,b,c,d)=>{let e,f,g,h;e=BU(d,a.es);Bf(d,a.es,b);f=a.eB.eC;g=0;while(true){if(g>=f){Bf(d,a.es,e);return (-1);}h=(BT(a.eB,g)).cY(b,c,d);if(h>=0)break;g=g+1|0;}return h;},J6=a=>{return I(53);},Q9=(a,b)=>{return !BU(b,a.es)?0:1;},BZ=L(D7),RX=(a,b,c,d)=>{let e,f,g;e=BU(d,a.es);Bf(d,a.es,b);f=a.eB.eC;g=0;while(g<f){if((BT(a.eB,g)).cY(b,c,d)
>=0)return a.eg.cY(a.eX.g4,c,d);g=g+1|0;}Bf(d,a.es,e);return (-1);},Qm=(a,b)=>{a.eg=b;},LY=a=>{return I(53);},F4=L(BZ),N3=(a,b,c,d)=>{let e,f;e=a.eB.eC;f=0;while(f<e){if((BT(a.eB,f)).cY(b,c,d)>=0)return a.eg.cY(b,c,d);f=f+1|0;}return (-1);},M2=(a,b)=>{return 0;},Ps=a=>{return I(54);},Gx=L(BZ),Oz=(a,b,c,d)=>{let e,f;e=a.eB.eC;f=0;while(true){if(f>=e)return a.eg.cY(b,c,d);if((BT(a.eB,f)).cY(b,c,d)>=0)break;f=f+1|0;}return (-1);},LS=(a,b)=>{return 0;},MH=a=>{return I(55);},HK=L(BZ),Qb=(a,b,c,d)=>{let e,f,g,h;e
=a.eB.eC;f=d.fG?0:d.eK;a:{g=a.eg.cY(b,c,d);if(g>=0){Bf(d,a.es,b);h=0;while(true){if(h>=e)break a;if((BT(a.eB,h)).du(f,b,c,d)>=0){Bf(d,a.es,(-1));return g;}h=h+1|0;}}}return (-1);},RL=(a,b)=>{return 0;},M9=a=>{return I(56);},Hb=L(BZ),KH=(a,b,c,d)=>{let e,f;e=a.eB.eC;Bf(d,a.es,b);f=0;while(true){if(f>=e)return a.eg.cY(b,c,d);if((BT(a.eB,f)).du(0,b,c,d)>=0)break;f=f+1|0;}return (-1);},RD=(a,b)=>{return 0;},Pb=a=>{return I(57);};
function CM(){Bj.call(this);this.eM=null;}
let Jd=(a,b,c)=>{Bm(a);a.eM=b;a.eX=c;a.es=c.eT;},Ob=(a,b)=>{let c=new CM();Jd(c,a,b);return c;},Lm=(a,b,c,d)=>{let e,f;e=CL(d,a.es);B0(d,a.es,b);f=a.eM.cY(b,c,d);if(f>=0)return f;B0(d,a.es,e);return (-1);},Kh=(a,b,c,d)=>{let e;e=a.eM.cU(b,c,d);if(e>=0)B0(d,a.es,e);return e;},Sd=(a,b,c,d,e)=>{let f;f=a.eM.du(b,c,d,e);if(f>=0)B0(e,a.es,f);return f;},Rz=(a,b)=>{return a.eM.ck(b);},Om=a=>{let b;b=new GD;Jd(b,a.eM,a.eX);a.eg=b;return b;},PG=a=>{let b;a.eH=1;b=a.eX;if(b!==null&&!b.eH)Dm(b);b=a.eM;if(b!==null&&!b.eH)
{b=b.c0();if(b!==null){a.eM.eH=1;a.eM=b;}a.eM.bP();}},En=L(0),Cs=L(),NI=(a,b,c)=>{b=new EA;Z(b);Q(b);};
function D2(){let a=this;Cs.call(a);a.fu=0;a.e3=null;a.hA=0;a.hK=0.0;a.hu=0;}
let HO=b=>{let c;if(b>=1073741824)return 1073741824;if(!b)return 16;c=b-1|0;b=c|c>>1;b=b|b>>2;b=b|b>>4;b=b|b>>8;return (b|b>>16)+1|0;},H7=(a,b,c)=>{let d;if(b>=0&&c>0.0){b=HO(b);a.fu=0;a.e3=K(DG,b);a.hK=c;IJ(a);return;}d=new Bs;Z(d);Q(d);},Sr=(a,b)=>{let c=new D2();H7(c,a,b);return c;},IJ=a=>{a.hu=a.e3.data.length*a.hK|0;},G_=L(0);
function Hf(){let a=this;D2.call(a);a.g2=0;a.fN=null;a.fD=null;}
let NW=(a,b,c)=>{let d,e,f,g,h,i,j,k,l,m,n,o;d=a;e=d.fu;f=a.g2;if(!a.fu){a.fN=null;a.fD=null;}if(b===null)g=0;else{a:{h=b;if(!h.fF){i=0;while(true){if(i>=h.eh.length)break a;h.fF=(31*h.fF|0)+h.eh.charCodeAt(i)|0;i=i+1|0;}}}g=h.fF;}j=g&2147483647;k=a.e3.data;i=j%k.length|0;if(b===null){l=k[0];while(l!==null&&l.gx!==null){l=l.fB;}}else{l=k[i];while(l!==null){if(l.gc==g){h=l.gx;if(b!==h&&!C1(b,h)?0:1)break;}l=l.fB;}}l=l;if(l===null){a.hA=a.hA+1|0;f=a.fu+1|0;a.fu=f;if(f>a.hu){f=d.e3.data.length;f=HO(!f?1:f<<1);m
=K(DG,f);n=m.data;i=0;o=f-1|0;while(true){k=d.e3.data;if(i>=k.length)break;l=k[i];k[i]=null;while(l!==null){f=l.gc&o;h=l.fB;l.fB=n[f];n[f]=l;l=h;}i=i+1|0;}d.e3=m;IJ(d);i=j%a.e3.data.length|0;}h=new DG;l=null;h.gx=b;h.gf=l;h.gc=g;h.ff=null;h.fn=null;k=a.e3.data;h.fB=k[i];k[i]=h;b=a.fD;if(b===null)a.fN=h;else b.ff=h;h.fn=b;a.fD=h;l=h;}else if(f){b=l.ff;if(b!==null){h=l.fn;if(h===null)a.fN=b;else h.ff=b;b.fn=h;b=a.fD;if(b!==null)b.ff=l;l.fn=b;l.ff=null;a.fD=l;}}h=l.gf;l.gf=c;return h;},HF=L(),M5=null,GL=null,Kb
=null,JQ=null,Pi=null,Pc=null,DA=()=>{DA=B3(HF);LA();},LA=()=>{M5=new FF;GL=new FC;Kb=new FD;JQ=new FA;Pi=new FB;Pc=new Jj;},BK=L(Bs),L$=L(),DZ=b=>{let c;c=(Co(b)).eJ;if(c[Bc].itemType!==null)return Qn(b);b=new Bs;Z(b);Q(b);},DJ=L(Bi),C8=L();
function R(){let a=this;C8.call(a);a.ew=0;a.eF=0;a.ep=null;a.fK=null;a.gj=null;a.eq=0;}
let J$=null,CK=()=>{CK=B3(R);Rg();},Ba=a=>{let b;CK();b=new EN;b.em=BC(64);a.ep=b;},OG=a=>{return null;},MJ=a=>{return a.ep;},KM=a=>{let b,c,d,e,f;if(!a.eF)b=Db(a.ep,0)>=2048?0:1;else{a:{c=a.ep;b=0;d=c.ex;if(b<d){e=c.em.data;f=(e[0]^(-1))>>>0|0;if(f)b=DO(f)+b|0;else{b=(d+31|0)/32|0;f=1;while(f<b){if(e[f]!=(-1)){b=(f*32|0)+DO(e[f]^(-1))|0;break a;}f=f+1|0;}b=d;}}}b=b>=2048?0:1;}return b;},Sm=a=>{return a.eq;},Lo=a=>{return a;},Nf=a=>{let b,c;if(a.gj===null){b=a.dz();c=new GT;c.h5=a;c.hm=b;Ba(c);a.gj=c;Cb(c,a.eF);}return a.gj;},DU
=a=>{let b,c;if(a.fK===null){b=a.dz();c=new GP;c.il=a;c.gG=b;c.ha=a;Ba(c);a.fK=c;Cb(c,a.ew);a.fK.eq=a.eq;}return a.fK;},Pp=a=>{return 0;},Cb=(a,b)=>{let c;c=a.ew;if(c^b){a.ew=c?0:1;a.eF=a.eF?0:1;}if(!a.eq)a.eq=1;return a;},Sj=a=>{return a.ew;},Ez=(b,c)=>{CK();return b.dA(c);},DK=(b,c)=>{let d,e;CK();if(b.dB()!==null&&c.dB()!==null){b=b.dB();c=c.dB();d=Bw(b.em.data.length,c.em.data.length);e=0;a:{while(e<d){if(b.em.data[e]&c.em.data[e]){d=1;break a;}e=e+1|0;}d=0;}return d;}return 1;},Ja=(b,c)=>{let d,e,f;CK();d
=0;while(true){Eb();e=HZ.data;if(d>=e.length){f=new Et;Bu(f,I(13));f.ie=I(13);f.h7=b;Q(f);}e=e[d].data;if(C1(b,e[0]))break;d=d+1|0;}return Rm(e[1],c);},Rg=()=>{let b;b=new EG;Eb();J$=b;};
function Et(){let a=this;Bi.call(a);a.ie=null;a.h7=null;}
function Fg(){let a=this;R.call(a);a.f8=0;a.g3=0;a.fi=0;a.gq=0;a.eV=0;a.fc=0;a.en=null;a.ez=null;}
let Rc=a=>{Ba(a);a.en=Jw();},BE=()=>{let a=new Fg();Rc(a);return a;},QM=(a,b,c)=>{Ba(a);a.en=Jw();a.f8=b;a.g3=c;},Hk=(a,b)=>{let c=new Fg();QM(c,a,b);return c;},Bv=(a,b)=>{a:{if(a.f8){b:{if(!(b>=97&&b<=122)){if(b<65)break b;if(b>90)break b;}if(a.eV){Fw(a.en,Da(b&65535));break a;}Fe(a.en,Da(b&65535));break a;}if(a.g3&&b>128){a.fi=1;b=CV(Dh(b));}}}if(!(!Ei(b)&&!Fj(b))){if(a.gq)Fw(a.ep,b-55296|0);else Fe(a.ep,b-55296|0);}if(a.eV)Fw(a.en,b);else Fe(a.en,b);if(!a.eq&&EM(b))a.eq=1;return a;},RC=(a,b)=>{let c,d,e;if
(!a.eq&&b.eq)a.eq=1;if(a.gq){if(!b.eF)CP(a.ep,b.dz());else BL(a.ep,b.dz());}else if(!b.eF)CF(a.ep,b.dz());else{CQ(a.ep,b.dz());BL(a.ep,b.dz());a.eF=a.eF?0:1;a.gq=1;}if(!a.fc&&b.dB()!==null){if(a.eV){if(!b.ew)CP(a.en,b.dB());else BL(a.en,b.dB());}else if(!b.ew)CF(a.en,b.dB());else{CQ(a.en,b.dB());BL(a.en,b.dB());a.ew=a.ew?0:1;a.eV=1;}}else{c=a.ew;d=a.ez;if(d!==null){if(!c){e=new Im;e.im=a;e.hq=c;e.gU=d;e.gN=b;Ba(e);a.ez=e;}else{e=new Io;e.io=a;e.hO=c;e.hB=d;e.hl=b;Ba(e);a.ez=e;}}else{if(c&&!a.eV&&EX(a.en)){d
=new Ij;d.hY=a;d.hG=b;Ba(d);a.ez=d;}else if(!c){d=new Ih;d.gi=a;d.f0=c;d.hM=b;Ba(d);a.ez=d;}else{d=new Ii;d.gk=a;d.f_=c;d.gR=b;Ba(d);a.ez=d;}a.fc=1;}}return a;},Bk=(a,b,c)=>{let d,e,f,g,h;if(b>c){d=new Bs;Z(d);Q(d);}a:{b:{if(!a.f8){if(c<55296)break b;if(b>57343)break b;}c=c+1|0;while(true){if(b>=c)break a;Bv(a,b);b=b+1|0;}}if(!a.eV)DP(a.en,b,c+1|0);else{d=a.en;c=c+1|0;if(b>=0&&b<=c){e=d.ex;if(b<e){f=Bw(e,c);if(b!=f){g=b/32|0;c=f/32|0;if(g==c){h=d.em.data;h[g]=h[g]&(Dy(d,b)|Dp(d,f));}else{h=d.em.data;h[g]=h[g]
&Dy(d,b);e=g+1|0;while(e<c){d.em.data[e]=0;e=e+1|0;}if(f&31){h=d.em.data;h[c]=h[c]&Dp(d,f);}}CZ(d);}}}else{d=new Bo;Z(d);Q(d);}}}return a;},Kz=(a,b)=>{let c,d,e;if(!a.eq&&b.eq)a.eq=1;c=b;if(c.fi)a.fi=1;d=a.eF;if(!(d^b.eF)){if(!d)CF(a.ep,c.ep);else BL(a.ep,c.ep);}else if(d)CP(a.ep,c.ep);else{CQ(a.ep,c.ep);BL(a.ep,c.ep);a.eF=1;}if(!a.fc&&BD(c)!==null){d=a.ew;if(!(d^b.ew)){if(!d)CF(a.en,BD(c));else BL(a.en,BD(c));}else if(d)CP(a.en,BD(c));else{CQ(a.en,BD(c));BL(a.en,BD(c));a.ew=1;}}else{d=a.ew;e=a.ez;if(e!==null)
{if(!d){c=new Ic;c.h6=a;c.hF=d;c.gQ=e;c.hi=b;Ba(c);a.ez=c;}else{c=new I1;c.iu=a;c.hg=d;c.gL=e;c.g6=b;Ba(c);a.ez=c;}}else{if(!a.eV&&EX(a.en)){if(!d){e=new Ik;e.is=a;e.g_=b;Ba(e);a.ez=e;}else{e=new Il;e.hT=a;e.g9=b;Ba(e);a.ez=e;}}else if(!d){e=new Ip;e.hH=a;e.gD=b;e.hD=d;Ba(e);a.ez=e;}else{e=new Iq;e.gP=a;e.g8=b;e.hh=d;Ba(e);a.ez=e;}a.fc=1;}}},Hm=(a,b)=>{let c,d,e;if(!a.eq&&b.eq)a.eq=1;c=b;if(c.fi)a.fi=1;d=a.eF;if(!(d^b.eF)){if(!d)BL(a.ep,c.ep);else CF(a.ep,c.ep);}else if(!d)CP(a.ep,c.ep);else{CQ(a.ep,c.ep);BL(a.ep,
c.ep);a.eF=0;}if(!a.fc&&BD(c)!==null){d=a.ew;if(!(d^b.ew)){if(!d)BL(a.en,BD(c));else CF(a.en,BD(c));}else if(!d)CP(a.en,BD(c));else{CQ(a.en,BD(c));BL(a.en,BD(c));a.ew=0;}}else{d=a.ew;e=a.ez;if(e!==null){if(!d){c=new Ie;c.ik=a;c.hL=d;c.hj=e;c.hN=b;Ba(c);a.ez=c;}else{c=new If;c.hZ=a;c.hs=d;c.gI=e;c.hE=b;Ba(c);a.ez=c;}}else{if(!a.eV&&EX(a.en)){if(!d){e=new Ia;e.hV=a;e.gM=b;Ba(e);a.ez=e;}else{e=new Ib;e.ii=a;e.gT=b;Ba(e);a.ez=e;}}else if(!d){e=new Ig;e.hQ=a;e.hn=b;e.gZ=d;Ba(e);a.ez=e;}else{e=new H_;e.gV=a;e.hw=
b;e.hP=d;Ba(e);a.ez=e;}a.fc=1;}}},BH=(a,b)=>{let c;c=a.ez;if(c!==null)return a.ew^c.dA(b);return a.ew^BX(a.en,b);},BD=a=>{if(!a.fc)return a.en;return null;},K6=a=>{return a.ep;},Mp=a=>{let b,c;if(a.ez!==null)return a;b=BD(a);c=new Id;c.h2=a;c.fH=b;Ba(c);return Cb(c,a.ew);},Mi=a=>{let b,c,d;b=new W;Y(b);c=Db(a.en,0);while(c>=0){DR(b,CI(c));Bh(b,124);c=Db(a.en,c+1|0);}d=b.er;if(d>0)GB(b,d-1|0);return X(b);},LE=a=>{return a.fi;};
function B6(){Bd.call(this);this.eo=null;}
let BP=(a,b,c,d)=>{Je(a,c);a.eo=b;a.fC=d;},Sp=a=>{return a.eo;},JM=(a,b)=>{return !a.eo.ck(b)&&!a.eg.ck(b)?0:1;},Ng=(a,b)=>{return 1;},K5=a=>{let b;a.eH=1;b=a.eg;if(b!==null&&!b.eH){b=b.c0();if(b!==null){a.eg.eH=1;a.eg=b;}a.eg.bP();}b=a.eo;if(b!==null){if(!b.eH){b=b.c0();if(b!==null){a.eo.eH=1;a.eo=b;}a.eo.bP();}else if(b instanceof CM&&b.eX.gr)a.eo=b.eg;}};
function BF(){B6.call(this);this.eu=null;}
let Cc=(a,b,c,d)=>{BP(a,b,c,d);a.eu=b;},PI=(a,b,c)=>{let d=new BF();Cc(d,a,b,c);return d;},KJ=(a,b,c,d)=>{let e,f;e=0;a:{while((b+a.eu.dq()|0)<=d.el){f=a.eu.dr(b,c);if(f<=0)break a;b=b+f|0;e=e+1|0;}}while(true){if(e<0)return (-1);f=a.eg.cY(b,c,d);if(f>=0)break;b=b-a.eu.dq()|0;e=e+(-1)|0;}return f;},Qf=a=>{return I(58);};
function Ck(){BF.call(this);this.fo=null;}
let Fp=(a,b,c,d,e)=>{Cc(a,c,d,e);a.fo=b;},KA=(a,b,c,d)=>{let e=new Ck();Fp(e,a,b,c,d);return e;},Nv=(a,b,c,d)=>{let e,f,g,h,i;e=a.fo;f=e.e7;g=e.e4;h=0;while(true){if(h>=f){a:{while(h<g){if((b+a.eu.dq()|0)>d.el)break a;i=a.eu.dr(b,c);if(i<1)break a;b=b+i|0;h=h+1|0;}}while(true){if(h<f)return (-1);i=a.eg.cY(b,c,d);if(i>=0)break;b=b-a.eu.dq()|0;h=h+(-1)|0;}return i;}if((b+a.eu.dq()|0)>d.el){d.eP=1;return (-1);}i=a.eu.dr(b,c);if(i<1)break;b=b+i|0;h=h+1|0;}return (-1);},OK=a=>{return EE(a.fo);},BG=L(B6),Ll=(a,b,
c,d)=>{let e;if(!a.eo.dQ(d))return a.eg.cY(b,c,d);e=a.eo.cY(b,c,d);if(e>=0)return e;return a.eg.cY(b,c,d);},OP=a=>{return I(59);},Ci=L(BF),Kq=(a,b,c,d)=>{let e;e=a.eo.cY(b,c,d);if(e<0)e=a.eg.cY(b,c,d);return e;},R1=(a,b)=>{a.eg=b;a.eo.bY(b);},GW=L(BF),Qy=(a,b,c,d)=>{while((b+a.eu.dq()|0)<=d.el&&a.eu.dr(b,c)>0){b=b+a.eu.dq()|0;}return a.eg.cY(b,c,d);},L9=(a,b,c,d)=>{let e,f,g;e=a.eg.cU(b,c,d);if(e<0)return (-1);f=e-a.eu.dq()|0;while(f>=b&&a.eu.dr(f,c)>0){g=f-a.eu.dq()|0;e=f;f=g;}return e;};
function IF(){Cs.call(this);this.ip=null;}
let EG=L(),Ew=null,E6=null,HZ=null,Eb=()=>{Eb=B3(EG);J_();},J_=()=>{let b,c,d,e;Ew=Lp();E6=QG();b=K(CG(J),194);c=b.data;d=K(J,2);e=d.data;e[0]=I(60);e[1]=Qr();c[0]=d;d=K(J,2);e=d.data;e[0]=I(61);e[1]=KX();c[1]=d;d=K(J,2);e=d.data;e[0]=I(62);e[1]=KW();c[2]=d;d=K(J,2);e=d.data;e[0]=I(63);e[1]=Nl();c[3]=d;d=K(J,2);e=d.data;e[0]=I(64);e[1]=E6;c[4]=d;d=K(J,2);e=d.data;e[0]=I(65);e[1]=Kn();c[5]=d;d=K(J,2);e=d.data;e[0]=I(66);e[1]=P7();c[6]=d;d=K(J,2);e=d.data;e[0]=I(67);e[1]=Nx();c[7]=d;d=K(J,2);e=d.data;e[0]=I(68);e[1]
=Mo();c[8]=d;d=K(J,2);e=d.data;e[0]=I(69);e[1]=Op();c[9]=d;d=K(J,2);e=d.data;e[0]=I(70);e[1]=K0();c[10]=d;d=K(J,2);e=d.data;e[0]=I(71);e[1]=NO();c[11]=d;d=K(J,2);e=d.data;e[0]=I(72);e[1]=Pz();c[12]=d;d=K(J,2);e=d.data;e[0]=I(73);e[1]=Kr();c[13]=d;d=K(J,2);e=d.data;e[0]=I(74);e[1]=Mz();c[14]=d;d=K(J,2);e=d.data;e[0]=I(75);e[1]=KF();c[15]=d;d=K(J,2);e=d.data;e[0]=I(76);e[1]=Kd();c[16]=d;d=K(J,2);e=d.data;e[0]=I(77);e[1]=Ky();c[17]=d;d=K(J,2);e=d.data;e[0]=I(78);e[1]=Ki();c[18]=d;d=K(J,2);e=d.data;e[0]=I(79);e[1]
=O_();c[19]=d;d=K(J,2);e=d.data;e[0]=I(80);e[1]=Ot();c[20]=d;d=K(J,2);e=d.data;e[0]=I(81);e[1]=QL();c[21]=d;d=K(J,2);e=d.data;e[0]=I(82);e[1]=Lz();c[22]=d;d=K(J,2);e=d.data;e[0]=I(83);e[1]=KL();c[23]=d;d=K(J,2);e=d.data;e[0]=I(84);e[1]=Kw();c[24]=d;d=K(J,2);e=d.data;e[0]=I(85);e[1]=NY();c[25]=d;d=K(J,2);e=d.data;e[0]=I(86);e[1]=OS();c[26]=d;d=K(J,2);e=d.data;e[0]=I(87);e[1]=QB();c[27]=d;d=K(J,2);e=d.data;e[0]=I(88);e[1]=Ew;c[28]=d;d=K(J,2);e=d.data;e[0]=I(89);e[1]=MN();c[29]=d;d=K(J,2);e=d.data;e[0]=I(90);e[1]
=Nz();c[30]=d;d=K(J,2);e=d.data;e[0]=I(91);e[1]=Ew;c[31]=d;d=K(J,2);e=d.data;e[0]=I(92);e[1]=JP();c[32]=d;d=K(J,2);e=d.data;e[0]=I(93);e[1]=E6;c[33]=d;d=K(J,2);e=d.data;e[0]=I(94);e[1]=ME();c[34]=d;d=K(J,2);e=d.data;e[0]=I(95);e[1]=N(0,127);c[35]=d;d=K(J,2);e=d.data;e[0]=I(96);e[1]=N(128,255);c[36]=d;d=K(J,2);e=d.data;e[0]=I(97);e[1]=N(256,383);c[37]=d;d=K(J,2);e=d.data;e[0]=I(98);e[1]=N(384,591);c[38]=d;d=K(J,2);e=d.data;e[0]=I(99);e[1]=N(592,687);c[39]=d;d=K(J,2);e=d.data;e[0]=I(100);e[1]=N(688,767);c[40]
=d;d=K(J,2);e=d.data;e[0]=I(101);e[1]=N(768,879);c[41]=d;d=K(J,2);e=d.data;e[0]=I(102);e[1]=N(880,1023);c[42]=d;d=K(J,2);e=d.data;e[0]=I(103);e[1]=N(1024,1279);c[43]=d;d=K(J,2);e=d.data;e[0]=I(104);e[1]=N(1280,1327);c[44]=d;d=K(J,2);e=d.data;e[0]=I(105);e[1]=N(1328,1423);c[45]=d;d=K(J,2);e=d.data;e[0]=I(106);e[1]=N(1424,1535);c[46]=d;d=K(J,2);e=d.data;e[0]=I(107);e[1]=N(1536,1791);c[47]=d;d=K(J,2);e=d.data;e[0]=I(108);e[1]=N(1792,1871);c[48]=d;d=K(J,2);e=d.data;e[0]=I(109);e[1]=N(1872,1919);c[49]=d;d=K(J,2);e
=d.data;e[0]=I(110);e[1]=N(1920,1983);c[50]=d;d=K(J,2);e=d.data;e[0]=I(111);e[1]=N(2304,2431);c[51]=d;d=K(J,2);e=d.data;e[0]=I(112);e[1]=N(2432,2559);c[52]=d;d=K(J,2);e=d.data;e[0]=I(113);e[1]=N(2560,2687);c[53]=d;d=K(J,2);e=d.data;e[0]=I(114);e[1]=N(2688,2815);c[54]=d;d=K(J,2);e=d.data;e[0]=I(115);e[1]=N(2816,2943);c[55]=d;d=K(J,2);e=d.data;e[0]=I(116);e[1]=N(2944,3071);c[56]=d;d=K(J,2);e=d.data;e[0]=I(117);e[1]=N(3072,3199);c[57]=d;d=K(J,2);e=d.data;e[0]=I(118);e[1]=N(3200,3327);c[58]=d;d=K(J,2);e=d.data;e[0]
=I(119);e[1]=N(3328,3455);c[59]=d;d=K(J,2);e=d.data;e[0]=I(120);e[1]=N(3456,3583);c[60]=d;d=K(J,2);e=d.data;e[0]=I(121);e[1]=N(3584,3711);c[61]=d;d=K(J,2);e=d.data;e[0]=I(122);e[1]=N(3712,3839);c[62]=d;d=K(J,2);e=d.data;e[0]=I(123);e[1]=N(3840,4095);c[63]=d;d=K(J,2);e=d.data;e[0]=I(124);e[1]=N(4096,4255);c[64]=d;d=K(J,2);e=d.data;e[0]=I(125);e[1]=N(4256,4351);c[65]=d;d=K(J,2);e=d.data;e[0]=I(126);e[1]=N(4352,4607);c[66]=d;d=K(J,2);e=d.data;e[0]=I(127);e[1]=N(4608,4991);c[67]=d;d=K(J,2);e=d.data;e[0]=I(128);e[1]
=N(4992,5023);c[68]=d;d=K(J,2);e=d.data;e[0]=I(129);e[1]=N(5024,5119);c[69]=d;d=K(J,2);e=d.data;e[0]=I(130);e[1]=N(5120,5759);c[70]=d;d=K(J,2);e=d.data;e[0]=I(131);e[1]=N(5760,5791);c[71]=d;d=K(J,2);e=d.data;e[0]=I(132);e[1]=N(5792,5887);c[72]=d;d=K(J,2);e=d.data;e[0]=I(133);e[1]=N(5888,5919);c[73]=d;d=K(J,2);e=d.data;e[0]=I(134);e[1]=N(5920,5951);c[74]=d;d=K(J,2);e=d.data;e[0]=I(135);e[1]=N(5952,5983);c[75]=d;d=K(J,2);e=d.data;e[0]=I(136);e[1]=N(5984,6015);c[76]=d;d=K(J,2);e=d.data;e[0]=I(137);e[1]=N(6016,
6143);c[77]=d;d=K(J,2);e=d.data;e[0]=I(138);e[1]=N(6144,6319);c[78]=d;d=K(J,2);e=d.data;e[0]=I(139);e[1]=N(6400,6479);c[79]=d;d=K(J,2);e=d.data;e[0]=I(140);e[1]=N(6480,6527);c[80]=d;d=K(J,2);e=d.data;e[0]=I(141);e[1]=N(6528,6623);c[81]=d;d=K(J,2);e=d.data;e[0]=I(142);e[1]=N(6624,6655);c[82]=d;d=K(J,2);e=d.data;e[0]=I(143);e[1]=N(6656,6687);c[83]=d;d=K(J,2);e=d.data;e[0]=I(144);e[1]=N(7424,7551);c[84]=d;d=K(J,2);e=d.data;e[0]=I(145);e[1]=N(7552,7615);c[85]=d;d=K(J,2);e=d.data;e[0]=I(146);e[1]=N(7616,7679);c[86]
=d;d=K(J,2);e=d.data;e[0]=I(147);e[1]=N(7680,7935);c[87]=d;d=K(J,2);e=d.data;e[0]=I(148);e[1]=N(7936,8191);c[88]=d;d=K(J,2);e=d.data;e[0]=I(149);e[1]=N(8192,8303);c[89]=d;d=K(J,2);e=d.data;e[0]=I(150);e[1]=N(8304,8351);c[90]=d;d=K(J,2);e=d.data;e[0]=I(151);e[1]=N(8352,8399);c[91]=d;d=K(J,2);e=d.data;e[0]=I(152);e[1]=N(8400,8447);c[92]=d;d=K(J,2);e=d.data;e[0]=I(153);e[1]=N(8448,8527);c[93]=d;d=K(J,2);e=d.data;e[0]=I(154);e[1]=N(8528,8591);c[94]=d;d=K(J,2);e=d.data;e[0]=I(155);e[1]=N(8592,8703);c[95]=d;d=K(J,
2);e=d.data;e[0]=I(156);e[1]=N(8704,8959);c[96]=d;d=K(J,2);e=d.data;e[0]=I(157);e[1]=N(8960,9215);c[97]=d;d=K(J,2);e=d.data;e[0]=I(158);e[1]=N(9216,9279);c[98]=d;d=K(J,2);e=d.data;e[0]=I(159);e[1]=N(9280,9311);c[99]=d;d=K(J,2);e=d.data;e[0]=I(160);e[1]=N(9312,9471);c[100]=d;d=K(J,2);e=d.data;e[0]=I(161);e[1]=N(9472,9599);c[101]=d;d=K(J,2);e=d.data;e[0]=I(162);e[1]=N(9600,9631);c[102]=d;d=K(J,2);e=d.data;e[0]=I(163);e[1]=N(9632,9727);c[103]=d;d=K(J,2);e=d.data;e[0]=I(164);e[1]=N(9728,9983);c[104]=d;d=K(J,2);e
=d.data;e[0]=I(165);e[1]=N(9984,10175);c[105]=d;d=K(J,2);e=d.data;e[0]=I(166);e[1]=N(10176,10223);c[106]=d;d=K(J,2);e=d.data;e[0]=I(167);e[1]=N(10224,10239);c[107]=d;d=K(J,2);e=d.data;e[0]=I(168);e[1]=N(10240,10495);c[108]=d;d=K(J,2);e=d.data;e[0]=I(169);e[1]=N(10496,10623);c[109]=d;d=K(J,2);e=d.data;e[0]=I(170);e[1]=N(10624,10751);c[110]=d;d=K(J,2);e=d.data;e[0]=I(171);e[1]=N(10752,11007);c[111]=d;d=K(J,2);e=d.data;e[0]=I(172);e[1]=N(11008,11263);c[112]=d;d=K(J,2);e=d.data;e[0]=I(173);e[1]=N(11264,11359);c[113]
=d;d=K(J,2);e=d.data;e[0]=I(174);e[1]=N(11392,11519);c[114]=d;d=K(J,2);e=d.data;e[0]=I(175);e[1]=N(11520,11567);c[115]=d;d=K(J,2);e=d.data;e[0]=I(176);e[1]=N(11568,11647);c[116]=d;d=K(J,2);e=d.data;e[0]=I(177);e[1]=N(11648,11743);c[117]=d;d=K(J,2);e=d.data;e[0]=I(178);e[1]=N(11776,11903);c[118]=d;d=K(J,2);e=d.data;e[0]=I(179);e[1]=N(11904,12031);c[119]=d;d=K(J,2);e=d.data;e[0]=I(180);e[1]=N(12032,12255);c[120]=d;d=K(J,2);e=d.data;e[0]=I(181);e[1]=N(12272,12287);c[121]=d;d=K(J,2);e=d.data;e[0]=I(182);e[1]=N(12288,
12351);c[122]=d;d=K(J,2);e=d.data;e[0]=I(183);e[1]=N(12352,12447);c[123]=d;d=K(J,2);e=d.data;e[0]=I(184);e[1]=N(12448,12543);c[124]=d;d=K(J,2);e=d.data;e[0]=I(185);e[1]=N(12544,12591);c[125]=d;d=K(J,2);e=d.data;e[0]=I(186);e[1]=N(12592,12687);c[126]=d;d=K(J,2);e=d.data;e[0]=I(187);e[1]=N(12688,12703);c[127]=d;d=K(J,2);e=d.data;e[0]=I(188);e[1]=N(12704,12735);c[128]=d;d=K(J,2);e=d.data;e[0]=I(189);e[1]=N(12736,12783);c[129]=d;d=K(J,2);e=d.data;e[0]=I(190);e[1]=N(12784,12799);c[130]=d;d=K(J,2);e=d.data;e[0]=I(191);e[1]
=N(12800,13055);c[131]=d;d=K(J,2);e=d.data;e[0]=I(192);e[1]=N(13056,13311);c[132]=d;d=K(J,2);e=d.data;e[0]=I(193);e[1]=N(13312,19893);c[133]=d;d=K(J,2);e=d.data;e[0]=I(194);e[1]=N(19904,19967);c[134]=d;d=K(J,2);e=d.data;e[0]=I(195);e[1]=N(19968,40959);c[135]=d;d=K(J,2);e=d.data;e[0]=I(196);e[1]=N(40960,42127);c[136]=d;d=K(J,2);e=d.data;e[0]=I(197);e[1]=N(42128,42191);c[137]=d;d=K(J,2);e=d.data;e[0]=I(198);e[1]=N(42752,42783);c[138]=d;d=K(J,2);e=d.data;e[0]=I(199);e[1]=N(43008,43055);c[139]=d;d=K(J,2);e=d.data;e[0]
=I(200);e[1]=N(44032,55203);c[140]=d;d=K(J,2);e=d.data;e[0]=I(201);e[1]=N(55296,56191);c[141]=d;d=K(J,2);e=d.data;e[0]=I(202);e[1]=N(56192,56319);c[142]=d;d=K(J,2);e=d.data;e[0]=I(203);e[1]=N(56320,57343);c[143]=d;d=K(J,2);e=d.data;e[0]=I(204);e[1]=N(57344,63743);c[144]=d;d=K(J,2);e=d.data;e[0]=I(205);e[1]=N(63744,64255);c[145]=d;d=K(J,2);e=d.data;e[0]=I(206);e[1]=N(64256,64335);c[146]=d;d=K(J,2);e=d.data;e[0]=I(207);e[1]=N(64336,65023);c[147]=d;d=K(J,2);e=d.data;e[0]=I(208);e[1]=N(65024,65039);c[148]=d;d=K(J,
2);e=d.data;e[0]=I(209);e[1]=N(65040,65055);c[149]=d;d=K(J,2);e=d.data;e[0]=I(210);e[1]=N(65056,65071);c[150]=d;d=K(J,2);e=d.data;e[0]=I(211);e[1]=N(65072,65103);c[151]=d;d=K(J,2);e=d.data;e[0]=I(212);e[1]=N(65104,65135);c[152]=d;d=K(J,2);e=d.data;e[0]=I(213);e[1]=N(65136,65279);c[153]=d;d=K(J,2);e=d.data;e[0]=I(214);e[1]=N(65280,65519);c[154]=d;d=K(J,2);e=d.data;e[0]=I(215);e[1]=N(0,1114111);c[155]=d;d=K(J,2);e=d.data;e[0]=I(216);e[1]=NT();c[156]=d;d=K(J,2);e=d.data;e[0]=I(217);e[1]=Be(0,1);c[157]=d;d=K(J,
2);e=d.data;e[0]=I(218);e[1]=CC(62,1);c[158]=d;d=K(J,2);e=d.data;e[0]=I(219);e[1]=Be(1,1);c[159]=d;d=K(J,2);e=d.data;e[0]=I(220);e[1]=Be(2,1);c[160]=d;d=K(J,2);e=d.data;e[0]=I(221);e[1]=Be(3,0);c[161]=d;d=K(J,2);e=d.data;e[0]=I(222);e[1]=Be(4,0);c[162]=d;d=K(J,2);e=d.data;e[0]=I(223);e[1]=Be(5,1);c[163]=d;d=K(J,2);e=d.data;e[0]=I(224);e[1]=CC(448,1);c[164]=d;d=K(J,2);e=d.data;e[0]=I(225);e[1]=Be(6,1);c[165]=d;d=K(J,2);e=d.data;e[0]=I(226);e[1]=Be(7,0);c[166]=d;d=K(J,2);e=d.data;e[0]=I(227);e[1]=Be(8,1);c[167]
=d;d=K(J,2);e=d.data;e[0]=I(228);e[1]=CC(3584,1);c[168]=d;d=K(J,2);e=d.data;e[0]=I(229);e[1]=Be(9,1);c[169]=d;d=K(J,2);e=d.data;e[0]=I(230);e[1]=Be(10,1);c[170]=d;d=K(J,2);e=d.data;e[0]=I(231);e[1]=Be(11,1);c[171]=d;d=K(J,2);e=d.data;e[0]=I(232);e[1]=CC(28672,0);c[172]=d;d=K(J,2);e=d.data;e[0]=I(233);e[1]=Be(12,0);c[173]=d;d=K(J,2);e=d.data;e[0]=I(234);e[1]=Be(13,0);c[174]=d;d=K(J,2);e=d.data;e[0]=I(235);e[1]=Be(14,0);c[175]=d;d=K(J,2);e=d.data;e[0]=I(236);e[1]=O7(983040,1,1);c[176]=d;d=K(J,2);e=d.data;e[0]
=I(237);e[1]=Be(15,0);c[177]=d;d=K(J,2);e=d.data;e[0]=I(238);e[1]=Be(16,1);c[178]=d;d=K(J,2);e=d.data;e[0]=I(239);e[1]=Be(18,1);c[179]=d;d=K(J,2);e=d.data;e[0]=I(240);e[1]=RP(19,0,1);c[180]=d;d=K(J,2);e=d.data;e[0]=I(241);e[1]=CC(1643118592,1);c[181]=d;d=K(J,2);e=d.data;e[0]=I(242);e[1]=Be(20,0);c[182]=d;d=K(J,2);e=d.data;e[0]=I(243);e[1]=Be(21,0);c[183]=d;d=K(J,2);e=d.data;e[0]=I(244);e[1]=Be(22,0);c[184]=d;d=K(J,2);e=d.data;e[0]=I(245);e[1]=Be(23,0);c[185]=d;d=K(J,2);e=d.data;e[0]=I(246);e[1]=Be(24,1);c[186]
=d;d=K(J,2);e=d.data;e[0]=I(247);e[1]=CC(2113929216,1);c[187]=d;d=K(J,2);e=d.data;e[0]=I(248);e[1]=Be(25,1);c[188]=d;d=K(J,2);e=d.data;e[0]=I(249);e[1]=Be(26,0);c[189]=d;d=K(J,2);e=d.data;e[0]=I(250);e[1]=Be(27,0);c[190]=d;d=K(J,2);e=d.data;e[0]=I(251);e[1]=Be(28,1);c[191]=d;d=K(J,2);e=d.data;e[0]=I(252);e[1]=Be(29,0);c[192]=d;d=K(J,2);e=d.data;e[0]=I(253);e[1]=Be(30,0);c[193]=d;HZ=b;};
function U(){let a=this;J.call(a);a.gn=null;a.gh=null;}
let Rm=(a,b)=>{if(!b&&a.gn===null)a.gn=a.dT();else if(b&&a.gh===null)a.gh=Cb(a.dT(),1);if(b)return a.gh;return a.gn;};
function Gt(){let a=this;C8.call(a);a.e7=0;a.e4=0;}
let EE=a=>{let b,c,d,e,f;b=a.e7;c=a.e4;d=c!=2147483647?C7(c):I(13);e=new W;Y(e);Bh(e,123);f=BY(e,b);Bh(f,44);Bh(T(f,d),125);return X(e);},HT=L(Bd),Qu=(a,b,c,d)=>{return b;},N0=a=>{return I(254);},Oe=(a,b)=>{return 0;};
function EN(){let a=this;J.call(a);a.em=null;a.ex=0;}
let PR=a=>{a.em=BC(2);},Jw=()=>{let a=new EN();PR(a);return a;},Fe=(a,b)=>{let c,d,e;if(b<0){c=new Bo;Z(c);Q(c);}d=b/32|0;if(b>=a.ex){DW(a,d+1|0);a.ex=b+1|0;}e=a.em.data;e[d]=e[d]|1<<(b%32|0);},DP=(a,b,c)=>{let d,e,f,g,h;if(b>=0){d=B1(b,c);if(d<=0){if(!d)return;d=b/32|0;e=c/32|0;if(c>a.ex){DW(a,e+1|0);a.ex=c;}if(d==e){f=a.em.data;f[d]=f[d]|Dp(a,b)&Dy(a,c);}else{f=a.em.data;f[d]=f[d]|Dp(a,b);g=d+1|0;while(g<e){a.em.data[g]=(-1);g=g+1|0;}if(c&31){f=a.em.data;f[e]=f[e]|Dy(a,c);}}return;}}h=new Bo;Z(h);Q(h);},Dp
=(a,b)=>{return (-1)<<(b%32|0);},Dy=(a,b)=>{b=b%32|0;return !b?0:(-1)>>>(32-b|0)|0;},Fw=(a,b)=>{let c,d,e,f,g,h;if(b<0){c=new Bo;Z(c);Q(c);}d=b/32|0;e=a.em.data;if(d<e.length){f=e[d];g=b%32|0;BN();h=g&31;e[d]=f&((-2)<<h|((-2)>>>(32-h|0)|0));if(b==(a.ex-1|0))CZ(a);}},BX=(a,b)=>{let c,d,e;if(b<0){c=new Bo;Z(c);Q(c);}d=b/32|0;e=a.em.data;return d<e.length&&e[d]&1<<(b%32|0)?1:0;},Db=(a,b)=>{let c,d,e,f,g;if(b<0){c=new Bo;Z(c);Q(c);}d=a.ex;if(b>=d)return (-1);e=b/32|0;f=a.em.data;g=f[e]>>>(b%32|0)|0;if(g)return DO(g)
+b|0;d=(d+31|0)/32|0;g=e+1|0;while(g<d){if(f[g])return (g*32|0)+DO(f[g])|0;g=g+1|0;}return (-1);},DW=(a,b)=>{let c,d,e,f;c=a.em.data.length;if(c>=b)return;c=BR((b*3|0)/2|0,(c*2|0)+1|0);d=a.em.data;e=BC(c);f=e.data;b=Bw(c,d.length);c=0;while(c<b){f[c]=d[c];c=c+1|0;}a.em=e;},CZ=a=>{let b,c,d;b=(a.ex+31|0)/32|0;a.ex=b*32|0;c=b-1|0;a:{while(true){if(c<0)break a;d=Hp(a.em.data[c]);if(d<32)break;c=c+(-1)|0;a.ex=a.ex-32|0;}a.ex=a.ex-d|0;}},BL=(a,b)=>{let c,d,e,f;c=Bw(a.em.data.length,b.em.data.length);d=0;while(d<
c){e=a.em.data;e[d]=e[d]&b.em.data[d];d=d+1|0;}while(true){f=a.em.data;if(c>=f.length)break;f[c]=0;c=c+1|0;}a.ex=Bw(a.ex,b.ex);CZ(a);},CP=(a,b)=>{let c,d,e;c=Bw(a.em.data.length,b.em.data.length);d=0;while(d<c){e=a.em.data;e[d]=e[d]&(b.em.data[d]^(-1));d=d+1|0;}CZ(a);},CF=(a,b)=>{let c,d,e;c=BR(a.ex,b.ex);a.ex=c;DW(a,(c+31|0)/32|0);c=Bw(a.em.data.length,b.em.data.length);d=0;while(d<c){e=a.em.data;e[d]=e[d]|b.em.data[d];d=d+1|0;}},CQ=(a,b)=>{let c,d,e;c=BR(a.ex,b.ex);a.ex=c;DW(a,(c+31|0)/32|0);c=Bw(a.em.data.length,
b.em.data.length);d=0;while(d<c){e=a.em.data;e[d]=e[d]^b.em.data[d];d=d+1|0;}CZ(a);},EX=a=>{return a.ex?0:1;};
function Ga(){let a=this;Bj.call(a);a.gg=null;a.gH=0;}
let OA=a=>{let b,c,d;b=!a.gH?I(255):I(256);c=a.gg.g();d=new W;Y(d);T(T(T(d,I(257)),b),c);return X(d);};
function Ex(){let a=this;Bj.call(a);a.fJ=null;a.fL=null;}
let Iy=(a,b,c)=>{Bm(a);a.fJ=b;a.fL=c;},EZ=(a,b)=>{let c=new Ex();Iy(c,a,b);return c;},Nh=(a,b,c,d)=>{let e,f,g,h,i,j;e=a.fJ.cY(b,c,d);if(e<0)a:{f=a.fL;g=d.eK;e=d.el;h=b+1|0;e=B1(h,e);if(e>0){d.eP=1;e=(-1);}else{i=c;j=P(i,b);if(!f.gg.dA(j))e=(-1);else{if(BA(j)){if(e<0&&BI(P(i,h))){e=(-1);break a;}}else if(BI(j)&&b>g&&BA(P(i,b-1|0))){e=(-1);break a;}e=f.eg.cY(h,c,d);}}}if(e>=0)return e;return (-1);},NC=(a,b)=>{a.eg=b;a.fL.eg=b;a.fJ.bY(b);},Pl=a=>{let b,c,d;b=Df(a.fJ);c=Df(a.fL);d=new W;Y(d);T(T(T(T(d,I(258)),
b),I(259)),c);return X(d);},Pr=(a,b)=>{return 1;},OE=(a,b)=>{return 1;};
function BO(){let a=this;Bj.call(a);a.eN=null;a.gA=0;}
let E2=(a,b)=>{Bm(a);a.eN=b.dU();a.gA=b.ew;},IS=a=>{let b=new BO();E2(b,a);return b;},KK=(a,b,c,d)=>{let e,f,g,h,i;e=d.el;if(b<e){f=b+1|0;g=c;h=P(g,b);if(a.dA(h)){i=a.eg.cY(f,c,d);if(i>0)return i;}if(f<e){b=f+1|0;f=P(g,f);if(Dx(h,f)&&a.dA(Cm(h,f)))return a.eg.cY(b,c,d);}}return (-1);},O9=a=>{let b,c,d;b=!a.gA?I(255):I(256);c=a.eN.g();d=new W;Y(d);T(T(T(d,I(257)),b),c);return X(d);},L6=(a,b)=>{return a.eN.dA(b);},M1=(a,b)=>{if(b instanceof B7)return Ez(a.eN,b.fg);if(b instanceof B8)return Ez(a.eN,b.eO);if(b instanceof BO)return DK(a.eN,
b.eN);if(!(b instanceof B2))return 1;return DK(a.eN,b.e6);},Sn=a=>{return a.eN;},Lc=(a,b)=>{a.eg=b;},Lh=(a,b)=>{return 1;},Em=L(BO),PT=(a,b)=>{return a.eN.dA(CV(Dh(b)));},P6=a=>{let b,c,d;b=!a.gA?I(255):I(256);c=a.eN.g();d=new W;Y(d);T(T(T(d,I(260)),b),c);return X(d);};
function It(){let a=this;Bn.call(a);a.go=null;a.he=0;}
let MO=(a,b)=>{BV(a);a.go=b.dU();a.he=b.ew;},Hu=a=>{let b=new It();MO(b,a);return b;},Qz=(a,b,c)=>{return !a.go.dA(Cj(Ch(P(c,b))))?(-1):1;},OT=a=>{let b,c,d;b=!a.he?I(255):I(256);c=a.go.g();d=new W;Y(d);T(T(T(d,I(260)),b),c);return X(d);};
function B2(){let a=this;Bn.call(a);a.e6=null;a.g$=0;}
let Pv=(a,b)=>{BV(a);a.e6=b.dU();a.g$=b.ew;},GS=a=>{let b=new B2();Pv(b,a);return b;},Eg=(a,b,c)=>{return !a.e6.dA(P(c,b))?(-1):1;},QY=a=>{let b,c,d;b=!a.g$?I(255):I(256);c=a.e6.g();d=new W;Y(d);T(T(T(d,I(257)),b),c);return X(d);},Oj=(a,b)=>{if(b instanceof B8)return Ez(a.e6,b.eO);if(b instanceof B2)return DK(a.e6,b.e6);if(!(b instanceof BO)){if(!(b instanceof B7))return 1;return 0;}return DK(a.e6,b.eN);};
function Fa(){let a=this;Bj.call(a);a.fm=null;a.fX=null;a.fR=0;}
let Lq=(a,b,c)=>{Bm(a);a.fm=b;a.fR=c;},G$=(a,b)=>{let c=new Fa();Lq(c,a,b);return c;},Ko=(a,b)=>{a.eg=b;},EF=a=>{if(a.fX===null)a.fX=Dk(a.fm);return a.fX;},RH=a=>{let b,c;b=EF(a);c=new W;Y(c);T(T(c,I(261)),b);return X(c);},Kp=(a,b,c,d)=>{let e,f,g,h,i,j,k,l,m,n,o,p,q;e=d.el;f=BC(3);g=(-1);h=(-1);if(b>=e)return (-1);i=b+1|0;j=c;k=P(j,b);l=k-44032|0;if(l>=0&&l<11172){m=4352+(l/588|0)|0;n=4449+((l%588|0)/28|0)|0;b=l%28|0;o=!b?HV([m,n]):HV([m,n,4519+b|0]);}else o=null;if(o!==null){p=o.data;l=0;b=p.length;q=a.fR;if
(b!=q)return (-1);while(true){if(l>=q)return a.eg.cY(i,c,d);if(p[l]!=a.fm.data[l])break;l=l+1|0;}return (-1);}f=f.data;f[0]=k;n=k-4352|0;if(n>=0&&n<19){if(i<e){k=P(j,i);g=k-4449|0;}if(g>=0&&g<21){n=i+1|0;f[1]=k;if(n<e){k=P(j,n);h=k-4519|0;}if(h>=0&&h<28){a:{b=n+1|0;f[2]=k;if(a.fR==3){n=f[0];p=a.fm.data;if(n==p[0]&&f[1]==p[1]&&f[2]==p[2]){b=a.eg.cY(b,c,d);break a;}}b=(-1);}return b;}b:{if(a.fR==2){q=f[0];p=a.fm.data;if(q==p[0]&&f[1]==p[1]){b=a.eg.cY(n,c,d);break b;}}b=(-1);}return b;}return (-1);}return (-1);},O5
=(a,b)=>{return b instanceof Fa&&!C1(EF(b),EF(a))?0:1;},MG=(a,b)=>{return 1;};
function B8(){Bn.call(this);this.eO=0;}
let PO=(a,b)=>{BV(a);a.eO=b;},Fx=a=>{let b=new B8();PO(b,a);return b;},P$=a=>{return 1;},Nk=(a,b,c)=>{return a.eO!=P(c,b)?(-1):1;},KE=(a,b,c,d)=>{let e,f,g,h;if(!(c instanceof Br))return Dg(a,b,c,d);e=c;f=d.el;while(true){if(b>=f)return (-1);g=DH(e,a.eO,b);if(g<0)return (-1);h=a.eg;b=g+1|0;if(h.cY(b,c,d)>=0)break;}return g;},Ql=(a,b,c,d,e)=>{let f,g;if(!(d instanceof Br))return CY(a,b,c,d,e);f=d;a:{while(true){if(c<b)return (-1);g=C0(f,a.eO,c);if(g<0)break a;if(g<b)break a;if(a.eg.cY(g+1|0,d,e)>=0)break;c=g
+(-1)|0;}return g;}return (-1);},M8=a=>{let b,c;b=a.eO;c=new W;Y(c);Bh(c,b);return X(c);},Mg=(a,b)=>{if(b instanceof B8)return b.eO!=a.eO?0:1;if(!(b instanceof B2)){if(b instanceof BO)return b.dA(a.eO);if(!(b instanceof B7))return 1;return 0;}return Eg(b,0,HA(a.eO))<=0?0:1;};
function HJ(){Bn.call(this);this.fY=0;}
let L4=(a,b)=>{BV(a);a.fY=Cj(Ch(b));},IZ=a=>{let b=new HJ();L4(b,a);return b;},J1=(a,b,c)=>{return a.fY!=Cj(Ch(P(c,b)))?(-1):1;},NA=a=>{let b,c;b=a.fY;c=new W;Y(c);Bh(T(c,I(262)),b);return X(c);};
function F7(){let a=this;Bn.call(a);a.gm=0;a.hc=0;}
let Ro=(a,b)=>{BV(a);a.gm=b;a.hc=Da(b);},Iz=a=>{let b=new F7();Ro(b,a);return b;},K9=(a,b,c)=>{let d;d=a.gm;c=c;return d!=P(c,b)&&a.hc!=P(c,b)?(-1):1;},Of=a=>{let b,c;b=a.gm;c=new W;Y(c);Bh(T(c,I(263)),b);return X(c);};
function Cg(){let a=this;Bj.call(a);a.ft=0;a.fZ=null;a.f7=null;a.fU=0;}
let ER=(a,b,c)=>{Bm(a);a.ft=1;a.f7=b;a.fU=c;},Ry=(a,b)=>{let c=new Cg();ER(c,a,b);return c;},PF=(a,b)=>{a.eg=b;},NE=(a,b,c,d)=>{let e,f,g,h,i,j,k,l;e=BC(4);f=d.el;if(b>=f)return (-1);g=E5(a,b,c,f);h=b+a.ft|0;i=GV(g);if(i===null){i=e.data;b=1;i[0]=g;}else{b=i.data.length;G2(i,0,e,0,b);b=0+b|0;}a:{if(h<f){j=e.data;g=E5(a,h,c,f);while(b<4){if(!((g!=832?0:1)|(g!=833?0:1)|(g!=835?0:1)|(g!=836?0:1))){k=b+1|0;j[b]=g;}else{i=(GV(g)).data;if(i.length!=2){k=b+1|0;j[b]=i[0];}else{l=b+1|0;j[b]=i[0];k=l+1|0;j[l]=i[1];}}h
=h+a.ft|0;if(h>=f){b=k;break a;}g=E5(a,h,c,f);b=k;}}}if(b!=a.fU)return (-1);i=e.data;g=0;while(true){if(g>=b)return a.eg.cY(h,c,d);if(i[g]!=a.f7.data[g])break;g=g+1|0;}return (-1);},EJ=a=>{let b,c;if(a.fZ===null){b=new W;Y(b);c=0;while(c<a.fU){DR(b,CI(a.f7.data[c]));c=c+1|0;}a.fZ=X(b);}return a.fZ;},Nb=a=>{let b,c;b=EJ(a);c=new W;Y(c);T(T(c,I(264)),b);return X(c);},E5=(a,b,c,d)=>{let e,f,g;a.ft=1;if(b>=(d-1|0))e=P(c,b);else{d=b+1|0;c=c;e=P(c,b);f=P(c,d);if(Dx(e,f)){g=BJ(2).data;g[0]=e;g[1]=f;d=g.length;if(0
<d&&d<=d){e=0<(d-1|0)&&BA(g[0])&&BI(g[1])?Cm(g[0],g[1]):g[0];a.ft=2;}else{c=new Bo;Z(c);Q(c);}}}return e;},QA=(a,b)=>{return b instanceof Cg&&!C1(EJ(b),EJ(a))?0:1;},Qo=(a,b)=>{return 1;},H9=L(Cg),Hy=L(Cg),JF=L(BG),QU=(a,b,c,d)=>{let e;while(true){e=a.eo.cY(b,c,d);if(e<=0)break;b=e;}return a.eg.cY(b,c,d);},FG=L(BG),LD=(a,b,c,d)=>{let e;e=a.eo.cY(b,c,d);if(e<0)return (-1);if(e>b){while(true){b=a.eo.cY(e,c,d);if(b<=e)break;e=b;}b=e;}return a.eg.cY(b,c,d);},CN=L(BG),KU=(a,b,c,d)=>{let e;if(!a.eo.dQ(d))return a.eg.cY(b,
c,d);e=a.eo.cY(b,c,d);if(e>=0)return e;return a.eg.cY(b,c,d);},NM=(a,b)=>{a.eg=b;a.eo.bY(b);},Jh=L(CN),Qd=(a,b,c,d)=>{let e;e=a.eo.cY(b,c,d);if(e<=0)e=b;return a.eg.cY(e,c,d);},MB=(a,b)=>{a.eg=b;};
function Cf(){let a=this;BG.call(a);a.e9=null;a.eR=0;}
let Dr=(a,b,c,d,e,f)=>{BP(a,c,d,e);a.e9=b;a.eR=f;},Sq=(a,b,c,d,e)=>{let f=new Cf();Dr(f,a,b,c,d,e);return f;},Rw=(a,b,c,d)=>{let e,f;e=Gf(d,a.eR);if(!a.eo.dQ(d))return a.eg.cY(b,c,d);if(e>=a.e9.e4)return a.eg.cY(b,c,d);f=a.eR;e=e+1|0;Cd(d,f,e);f=a.eo.cY(b,c,d);if(f>=0){Cd(d,a.eR,0);return f;}f=a.eR;e=e+(-1)|0;Cd(d,f,e);if(e>=a.e9.e7)return a.eg.cY(b,c,d);Cd(d,a.eR,0);return (-1);},N4=a=>{return EE(a.e9);},GG=L(Cf),OQ=(a,b,c,d)=>{let e,f,g;e=0;f=a.e9.e4;a:{while(true){g=a.eo.cY(b,c,d);if(g<=b)break a;if(e>=f)break;e
=e+1|0;b=g;}}if(g<0&&e<a.e9.e7)return (-1);return a.eg.cY(b,c,d);},Iv=L(BG),P8=(a,b,c,d)=>{let e;if(!a.eo.dQ(d))return a.eg.cY(b,c,d);e=a.eg.cY(b,c,d);if(e>=0)return e;return a.eo.cY(b,c,d);},G8=L(CN),Pu=(a,b,c,d)=>{let e;if(!a.eo.dQ(d))return a.eg.cY(b,c,d);e=a.eg.cY(b,c,d);if(e<0)e=a.eo.cY(b,c,d);return e;},IP=L(Cf),Mk=(a,b,c,d)=>{let e,f,g;e=Gf(d,a.eR);if(!a.eo.dQ(d))return a.eg.cY(b,c,d);f=a.e9;if(e>=f.e4){Cd(d,a.eR,0);return a.eg.cY(b,c,d);}if(e<f.e7){Cd(d,a.eR,e+1|0);g=a.eo.cY(b,c,d);}else{g=a.eg.cY(b,
c,d);if(g>=0){Cd(d,a.eR,0);return g;}Cd(d,a.eR,e+1|0);g=a.eo.cY(b,c,d);}return g;},Iw=L(B6),Q5=(a,b,c,d)=>{let e;e=d.el;if(e>b)return a.eg.du(b,e,c,d);return a.eg.cY(b,c,d);},Lr=(a,b,c,d)=>{let e;e=d.el;if(a.eg.du(b,e,c,d)>=0)return b;return (-1);},PD=a=>{return I(265);};
function H$(){B6.call(this);this.ga=null;}
let Oo=(a,b,c,d)=>{let e,f;e=d.el;f=IV(a,b,e,c);if(f>=0)e=f;if(e>b)return a.eg.du(b,e,c,d);return a.eg.cY(b,c,d);},KC=(a,b,c,d)=>{let e,f,g,h;e=d.el;f=a.eg.cU(b,c,d);if(f<0)return (-1);g=IV(a,f,e,c);if(g>=0)e=g;g=BR(f,a.eg.du(f,e,c,d));if(g<=0)h=g?(-1):0;else{h=g-1|0;a:{while(true){if(h<b){h=(-1);break a;}if(a.ga.d7(P(c,h)))break;h=h+(-1)|0;}}}if(h>=b)b=h>=g?h:h+1|0;return b;},IV=(a,b,c,d)=>{while(true){if(b>=c)return (-1);if(a.ga.d7(P(d,b)))break;b=b+1|0;}return b;},R0=a=>{return I(266);},Dd=L(),IR=null,I2
=null,Fr=b=>{let c;if(!(b&1)){c=I2;if(c!==null)return c;c=new Jo;I2=c;return c;}c=IR;if(c!==null)return c;c=new Jn;IR=c;return c;},JH=L(BF),Mu=(a,b,c,d)=>{let e;a:{while(true){if((b+a.eu.dq()|0)>d.el)break a;e=a.eu.dr(b,c);if(e<1)break;b=b+e|0;}}return a.eg.cY(b,c,d);},GN=L(Ci),Lt=(a,b,c,d)=>{let e;if((b+a.eu.dq()|0)<=d.el){e=a.eu.dr(b,c);if(e>=1)b=b+e|0;}return a.eg.cY(b,c,d);},IX=L(Ck),JR=(a,b,c,d)=>{let e,f,g,h,i;e=a.fo;f=e.e7;g=e.e4;h=0;while(true){if(h>=f){a:{while(true){if(h>=g)break a;if((b+a.eu.dq()
|0)>d.el)break a;i=a.eu.dr(b,c);if(i<1)break;b=b+i|0;h=h+1|0;}}return a.eg.cY(b,c,d);}if((b+a.eu.dq()|0)>d.el){d.eP=1;return (-1);}i=a.eu.dr(b,c);if(i<1)break;b=b+i|0;h=h+1|0;}return (-1);},G4=L(BF),N8=(a,b,c,d)=>{let e;while(true){e=a.eg.cY(b,c,d);if(e>=0)break;if((b+a.eu.dq()|0)<=d.el){e=a.eu.dr(b,c);b=b+e|0;}if(e<1)return (-1);}return e;},HG=L(Ci),MR=(a,b,c,d)=>{let e;e=a.eg.cY(b,c,d);if(e>=0)return e;return a.eo.cY(b,c,d);},HL=L(Ck),Ks=(a,b,c,d)=>{let e,f,g,h,i,j;e=a.fo;f=e.e7;g=e.e4;h=0;while(true){if(h
>=f){a:{while(true){i=a.eg.cY(b,c,d);if(i>=0)break;if((b+a.eu.dq()|0)<=d.el){i=a.eu.dr(b,c);b=b+i|0;h=h+1|0;}if(i<1)break a;if(h>g)break a;}return i;}return (-1);}if((b+a.eu.dq()|0)>d.el){d.eP=1;return (-1);}j=a.eu.dr(b,c);if(j<1)break;b=b+j|0;h=h+1|0;}return (-1);},EU=L(Bd),RK=(a,b,c,d)=>{if(b&&!(d.e8&&b==d.eK))return (-1);return a.eg.cY(b,c,d);},Ph=(a,b)=>{return 0;},Lx=a=>{return I(267);};
function FM(){Bd.call(this);this.gX=0;}
let PX=(a,b)=>{Bm(a);a.gX=b;},GR=a=>{let b=new FM();PX(b,a);return b;},Ox=(a,b,c,d)=>{let e,f,g,h;e=b>=d.el?32:P(c,b);if(!b)f=32;else{g=b-1|0;f=P(c,g);}h=d.fG?0:d.eK;return (e!=32&&!Hd(a,e,b,h,c)?0:1)^(f!=32&&!Hd(a,f,b-1|0,h,c)?0:1)^a.gX?(-1):a.eg.cY(b,c,d);},OW=(a,b)=>{return 0;},Rp=a=>{return I(268);},Hd=(a,b,c,d,e)=>{let f;if(!HY(b)&&b!=95){a:{if(DD(b)==6)while(true){c=c+(-1)|0;if(c<d)break a;f=P(e,c);if(HY(f))return 0;if(DD(f)!=6)return 1;}}return 1;}return 0;},H1=L(Bd),PW=(a,b,c,d)=>{if(b!=d.fr)return (-1);return a.eg.cY(b,
c,d);},Ri=(a,b)=>{return 0;},NL=a=>{return I(269);};
function EO(){Bd.call(this);this.fh=0;}
let Gu=(a,b)=>{Bm(a);a.fh=b;},Kv=a=>{let b=new EO();Gu(b,a);return b;},P3=(a,b,c,d)=>{let e,f,g,h;e=d.e8?d.el:c.eh.length;if(b>=e){Bf(d,a.fh,0);return a.eg.cY(b,c,d);}f=e-b|0;if(f==2){g=c;if(P(g,b)==13&&P(g,b+1|0)==10){Bf(d,a.fh,0);return a.eg.cY(b,c,d);}}a:{if(f==1){h=P(c,b);if(h==10)break a;if(h==13)break a;if(h==133)break a;if((h|1)==8233)break a;}return (-1);}Bf(d,a.fh,0);return a.eg.cY(b,c,d);},Qw=(a,b)=>{let c;c=!BU(b,a.fh)?0:1;Bf(b,a.fh,(-1));return c;},KD=a=>{return I(270);},Hi=L(Bd),Nq=(a,b,c,d)=>{if
(b<(!d.fG?d.el:c.eh.length))return (-1);d.eP=1;d.hW=1;return a.eg.cY(b,c,d);},JX=(a,b)=>{return 0;},MX=a=>{return I(271);};
function GZ(){Bd.call(this);this.gB=null;}
let Qg=(a,b,c,d)=>{let e,f,g;a:{if(b!=d.el){if(!b)break a;if(d.e8&&b==d.eK)break a;e=a.gB;f=b-1|0;g=c;if(e.d_(P(g,f),P(g,b)))break a;}return (-1);}return a.eg.cY(b,c,d);},Mt=(a,b)=>{return 0;},M_=a=>{return I(272);},G9=L(Bj),M6=a=>{Bm(a);},Ov=()=>{let a=new G9();M6(a);return a;},Qi=(a,b,c,d)=>{let e,f,g,h;e=d.el;f=b+1|0;if(f>e){d.eP=1;return (-1);}g=c;h=P(g,b);if(BA(h)){b=b+2|0;if(b<=e&&Dx(h,P(g,f)))return a.eg.cY(b,c,d);}return a.eg.cY(f,c,d);},Sb=a=>{return I(273);},O8=(a,b)=>{a.eg=b;},MQ=a=>{return (-2147483602);},O6
=(a,b)=>{return 1;};
function IH(){Bj.call(this);this.gy=null;}
let Qs=(a,b)=>{Bm(a);a.gy=b;},Q0=a=>{let b=new IH();Qs(b,a);return b;},Nd=(a,b,c,d)=>{let e,f,g,h,i;e=d.el;f=b+1|0;if(f>e){d.eP=1;return (-1);}g=c;h=P(g,b);if(BA(h)){b=b+2|0;if(b<=e){i=P(g,f);if(Dx(h,i))return a.gy.d7(Cm(h,i))?(-1):a.eg.cY(b,c,d);}}return a.gy.d7(h)?(-1):a.eg.cY(f,c,d);},QR=a=>{return I(274);},RW=(a,b)=>{a.eg=b;},JI=a=>{return (-2147483602);},QT=(a,b)=>{return 1;};
function GO(){Bd.call(this);this.fA=0;}
let Mj=(a,b)=>{Bm(a);a.fA=b;},JZ=a=>{let b=new GO();Mj(b,a);return b;},QN=(a,b,c,d)=>{let e;e=d.e8?d.el:c.eh.length;if(b>=e){Bf(d,a.fA,0);return a.eg.cY(b,c,d);}if((e-b|0)==1&&P(c,b)==10){Bf(d,a.fA,1);return a.eg.cY(b+1|0,c,d);}return (-1);},Mf=(a,b)=>{let c;c=!BU(b,a.fA)?0:1;Bf(b,a.fA,(-1));return c;},JN=a=>{return I(270);};
function Gg(){Bd.call(this);this.fs=0;}
let Ns=(a,b)=>{Bm(a);a.fs=b;},Oc=a=>{let b=new Gg();Ns(b,a);return b;},Nm=(a,b,c,d)=>{if((d.e8?d.el-b|0:c.eh.length-b|0)<=0){Bf(d,a.fs,0);return a.eg.cY(b,c,d);}if(P(c,b)!=10)return (-1);Bf(d,a.fs,1);return a.eg.cY(b+1|0,c,d);},LU=(a,b)=>{let c;c=!BU(b,a.fs)?0:1;Bf(b,a.fs,(-1));return c;},LV=a=>{return I(275);};
function FZ(){Bd.call(this);this.e$=0;}
let RE=(a,b)=>{Bm(a);a.e$=b;},Lw=a=>{let b=new FZ();RE(b,a);return b;},O2=(a,b,c,d)=>{let e,f,g,h;e=d.e8?d.el-b|0:c.eh.length-b|0;if(!e){Bf(d,a.e$,0);return a.eg.cY(b,c,d);}if(e<2){f=P(c,b);g=97;}else{h=c;f=P(h,b);g=P(h,b+1|0);}switch(f){case 10:case 133:case 8232:case 8233:Bf(d,a.e$,0);return a.eg.cY(b,c,d);case 13:if(g!=10){Bf(d,a.e$,0);return a.eg.cY(b,c,d);}Bf(d,a.e$,0);return a.eg.cY(b,c,d);default:}return (-1);},QD=(a,b)=>{let c;c=!BU(b,a.e$)?0:1;Bf(b,a.e$,(-1));return c;},My=a=>{return I(276);};
function CA(){let a=this;Bj.call(a);a.g0=0;a.fj=0;}
let Fi=(a,b,c)=>{Bm(a);a.g0=b;a.fj=c;},Qq=(a,b)=>{let c=new CA();Fi(c,a,b);return c;},MF=(a,b,c,d)=>{let e,f,g,h,i;e=De(a,d);if(e!==null&&(b+e.eh.length|0)<=d.el){f=0;while(true){if(f>=e.eh.length){Bf(d,a.fj,e.eh.length);return a.eg.cY(b+e.eh.length|0,c,d);}g=P(e,f);h=b+f|0;i=c;if(g!=P(i,h)&&Da(P(e,f))!=P(i,h))break;f=f+1|0;}return (-1);}return (-1);},Qj=(a,b)=>{a.eg=b;},De=(a,b)=>{let c,d;c=a.g0;d=CL(b,c);c=D6(b,c);return (c|d|(c-d|0))>=0&&c<=b.fS.eh.length?BW(b.fS,d,c):null;},L0=a=>{let b,c;b=a.es;c=new W;Y(c);BY(T(c,
I(277)),b);return X(c);},Q_=(a,b)=>{let c;c=!BU(b,a.fj)?0:1;Bf(b,a.fj,(-1));return c;},G1=L(CA),Nj=(a,b,c)=>{Fi(a,b,c);},MI=(a,b)=>{let c=new G1();Nj(c,a,b);return c;},QS=(a,b,c,d)=>{let e,f;e=De(a,d);if(e!==null&&(b+e.eh.length|0)<=d.el){f=!H6(c,e,b)?(-1):e.eh.length;if(f<0)return (-1);Bf(d,a.fj,f);return a.eg.cY(b+f|0,c,d);}return (-1);},MW=(a,b,c,d)=>{let e,f,g;e=De(a,d);f=d.eK;if(e!==null&&(b+e.eh.length|0)<=f){g=c;while(true){if(b>f)return (-1);b=FJ(g,e,b);if(b<0)return (-1);if(a.eg.cY(b+e.eh.length|0,
c,d)>=0)break;b=b+1|0;}return b;}return (-1);},LW=(a,b,c,d,e)=>{let f,g,h;f=De(a,e);if(f===null)return (-1);g=d;a:{while(true){if(c<b)return (-1);c=Bw(c,g.eh.length-f.eh.length|0);b:{c:while(true){if(c<0){c=(-1);break b;}h=0;while(true){if(h>=f.eh.length)break c;if(P(g,c+h|0)!=P(f,h))break;h=h+1|0;}c=c+(-1)|0;}}if(c<0)break a;if(c<b)break a;if(a.eg.cY(c+f.eh.length|0,d,e)>=0)break;c=c+(-1)|0;}return c;}return (-1);},Lj=(a,b)=>{return 1;},NG=a=>{let b,c;b=a.es;c=new W;Y(c);BY(T(c,I(278)),b);return X(c);};
function HE(){CA.call(this);this.h_=0;}
let Md=(a,b,c)=>{Fi(a,b,c);},Od=(a,b)=>{let c=new HE();Md(c,a,b);return c;},Ka=(a,b,c,d)=>{let e,f,g,h;e=De(a,d);if(e!==null&&(b+e.eh.length|0)<=d.el){f=0;while(true){if(f>=e.eh.length){Bf(d,a.fj,e.eh.length);return a.eg.cY(b+e.eh.length|0,c,d);}g=Cj(Ch(P(e,f)));h=b+f|0;if(g!=Cj(Ch(P(c,h))))break;f=f+1|0;}return (-1);}return (-1);},OX=a=>{let b,c;b=a.h_;c=new W;Y(c);BY(T(c,I(279)),b);return X(c);};
function IA(){let a=this;Bn.call(a);a.eG=null;a.f6=null;a.f5=null;}
let RM=(a,b,c)=>{return !ET(a,c,b)?(-1):a.eA;},Ok=(a,b,c,d)=>{let e,f,g;e=d.el;while(true){if(b>e)return (-1);f=P(a.eG,a.eA-1|0);a:{while(true){g=a.eA;if(b>(e-g|0)){b=(-1);break a;}g=(b+g|0)-1|0;g=P(c,g);if(g==f&&ET(a,c,b))break;b=b+F1(a.f6,g)|0;}}if(b<0)return (-1);if(a.eg.cY(b+a.eA|0,c,d)>=0)break;b=b+1|0;}return b;},Mq=(a,b,c,d,e)=>{let f,g,h;while(true){if(c<b)return (-1);f=P(a.eG,0);g=d;h=(g.eh.length-c|0)-a.eA|0;if(h<=0)c=c+h|0;a:{while(true){if(c<b){c=(-1);break a;}h=P(g,c);if(h==f&&ET(a,d,c))break;c
=c-F1(a.f5,h)|0;}}if(c<0)return (-1);if(a.eg.cY(c+a.eA|0,d,e)>=0)break;c=c+(-1)|0;}return c;},NP=a=>{let b,c;b=a.eG;c=new W;Y(c);T(T(c,I(280)),b);return X(c);},NJ=(a,b)=>{let c;if(b instanceof B8)return b.eO!=P(a.eG,0)?0:1;if(b instanceof B2)return Eg(b,0,BW(a.eG,0,1))<=0?0:1;if(!(b instanceof BO)){if(!(b instanceof B7))return 1;return a.eG.eh.length>1&&b.fg==Cm(P(a.eG,0),P(a.eG,1))?1:0;}a:{b:{b=b;if(!b.dA(P(a.eG,0))){if(a.eG.eh.length<=1)break b;if(!b.dA(Cm(P(a.eG,0),P(a.eG,1))))break b;}c=1;break a;}c=0;}return c;},ET
=(a,b,c)=>{let d,e;d=0;while(d<a.eA){e=d+c|0;if(P(b,e)!=P(a.eG,d))return 0;d=d+1|0;}return 1;};
function FV(){Bn.call(this);this.fq=null;}
let Mh=(a,b)=>{let c,d;BV(a);c=new W;Y(c);d=0;while(d<b.er){Bh(c,Cj(Ch(Er(b,d))));d=d+1|0;}a.fq=X(c);a.eA=c.er;},Q8=a=>{let b=new FV();Mh(b,a);return b;},Kk=(a,b,c)=>{let d,e,f;d=0;while(true){if(d>=a.fq.eh.length)return a.fq.eh.length;e=P(a.fq,d);f=b+d|0;if(e!=Cj(Ch(P(c,f))))break;d=d+1|0;}return (-1);},Oi=a=>{let b,c;b=a.fq;c=new W;Y(c);T(T(c,I(281)),b);return X(c);};
function GQ(){Bn.call(this);this.fl=null;}
let No=(a,b)=>{BV(a);a.fl=X(b);a.eA=b.er;},Sc=a=>{let b=new GQ();No(b,a);return b;},JV=(a,b,c)=>{let d,e,f,g;d=0;while(true){if(d>=a.fl.eh.length)return a.fl.eh.length;e=P(a.fl,d);f=b+d|0;g=c;if(e!=P(g,f)&&Da(P(a.fl,d))!=P(g,f))break;d=d+1|0;}return (-1);},Mn=a=>{let b,c;b=a.fl;c=new W;Y(c);T(T(c,I(282)),b);return X(c);},IU=L(0),El=L(CU),Ey=L(El),FF=L(Ey),E$=L(Cs),KV=(a,b,c)=>{b=new EA;Z(b);Q(b);},FC=L(E$),EI=L(C6),FD=L(EI),D_=L(0),FA=L(),Ju=L(0),FB=L(),Jj=L(),RQ=L(),Pt=L(),Hx=b=>{let c,d,e,f,g,h,i;c=JC(D4(b));d
=DY(c);e=BC(d*2|0);f=e.data;g=0;h=0;while(h<d){g=g+DY(c)|0;i=h*2|0;f[i]=g;f[i+1|0]=Ev(c);h=h+1|0;}return e;},F6=b=>{let c,d,e,f,g,h,i,j,k,l;c=BC(65536);d=c.data;e=0;f=0;g=0;a:{while(true){h=b.data;if(g>=h.length)break a;i=h[g];j=h[g+1|0];k=d.length;if(i<k)k=i;else if(i==e)break;Gi(c,e,k,f);g=g+2|0;e=k;f=j;}}l=new F0;l.hS=b;l.gJ=c;return l;},D5=b=>{if(b>92)return ((b-32|0)-2|0)<<24>>24;if(b<=34)return (b-32|0)<<24>>24;return ((b-32|0)-1|0)<<24>>24;},RR=b=>{let c,d,e,f,g,h,i,j,k,l,m,n,o,p,q;c=K(DC,16384);d=c.data;e
=I0(16384);f=e.data;g=0;h=0;i=0;j=0;a:while(true){if(j>=b.eh.length){if(g<=0)k=h;else{k=h+1|0;d[h]=GC(i,i+g|0,EK(e,g));}return H3(c,k);}l=D5(P(b,j));if(l==64){j=j+1|0;l=D5(P(b,j));m=0;k=1;n=0;while(n<3){j=j+1|0;m=m|DF(k,D5(P(b,j)));k=k*64|0;n=n+1|0;}}else if(l<32)m=1;else{l=(l-32|0)<<24>>24;j=j+1|0;m=D5(P(b,j));}if(!l&&m>=128){if(g>0){k=h+1|0;d[h]=GC(i,i+g|0,EK(e,g));h=k;}i=i+(g+m|0)|0;g=0;}else while(m>0){k=f.length;if(g!=k){n=h;o=i;i=g;}else{n=h+1|0;p=new DC;o=i+g|0;Gl(p,i,o,EK(e,g));d[h]=p;i=0;}q=Bw(m,k-
i|0);g=i+q|0;if(i>g)break a;while(i<g){k=i+1|0;f[i]=l;i=k;}m=m-q|0;h=n;i=o;}j=j+1|0;}b=new Bs;Z(b);Q(b);};
function FR(){Bn.call(this);this.gs=0;}
let J2=(a,b,c)=>{let d,e;d=b+1|0;c=c;e=P(c,b);d=P(c,d);return a.gs!=CV(Dh(Cm(e,d)))?(-1):2;},Rk=a=>{let b,c;b=Dk(CI(a.gs));c=new W;Y(c);T(T(c,I(262)),b);return X(c);};
function Dn(){Bj.call(this);this.e_=0;}
let PC=(a,b)=>{Bm(a);a.e_=b;},Hj=a=>{let b=new Dn();PC(b,a);return b;},N2=(a,b)=>{a.eg=b;},Qx=(a,b,c,d)=>{let e,f,g;e=b+1|0;if(e>d.el){d.eP=1;return (-1);}f=c;g=P(f,b);if(b>d.eK&&BA(P(f,b-1|0)))return (-1);if(a.e_!=g)return (-1);return a.eg.cY(e,c,d);},NB=(a,b,c,d)=>{let e,f,g,h,i;if(!(c instanceof Br))return Dg(a,b,c,d);e=c;f=d.eK;g=d.el;while(true){if(b>=g)return (-1);h=DH(e,a.e_,b);if(h<0)return (-1);if(h>f&&BA(P(e,h-1|0))){b=h+1|0;continue;}i=a.eg;b=h+1|0;if(i.cY(b,c,d)>=0)break;}return h;},R9=(a,b,c,d,
e)=>{let f,g;if(!(d instanceof Br))return CY(a,b,c,d,e);f=e.eK;g=d;a:{while(true){if(c<b)return (-1);c=C0(g,a.e_,c);if(c<0)break a;if(c<b)break a;if(c>f&&BA(P(g,c-1|0))){c=c+(-2)|0;continue;}if(a.eg.cY(c+1|0,d,e)>=0)break;c=c+(-1)|0;}return c;}return (-1);},LF=a=>{let b,c;b=a.e_;c=new W;Y(c);Bh(c,b);return X(c);},LN=(a,b)=>{if(b instanceof B8)return 0;if(b instanceof B2)return 0;if(b instanceof BO)return 0;if(b instanceof B7)return 0;if(b instanceof DL)return 0;if(!(b instanceof Dn))return 1;return b.e_!=a.e_
?0:1;},LZ=(a,b)=>{return 1;};
function DL(){Bj.call(this);this.fd=0;}
let Ni=(a,b)=>{Bm(a);a.fd=b;},H5=a=>{let b=new DL();Ni(b,a);return b;},PK=(a,b)=>{a.eg=b;},KI=(a,b,c,d)=>{let e,f,g,h,i;e=d.el;f=b+1|0;g=B1(f,e);if(g>0){d.eP=1;return (-1);}h=c;i=P(h,b);if(g<0&&BI(P(h,f)))return (-1);if(a.fd!=i)return (-1);return a.eg.cY(f,c,d);},OU=(a,b,c,d)=>{let e,f,g;if(!(c instanceof Br))return Dg(a,b,c,d);e=c;f=d.el;while(true){if(b>=f)return (-1);g=DH(e,a.fd,b);if(g<0)return (-1);b=g+1|0;if(b<f&&BI(P(e,b))){b=g+2|0;continue;}if(a.eg.cY(b,c,d)>=0)break;}return g;},JS=(a,b,c,d,e)=>{let f,
g,h;if(!(d instanceof Br))return CY(a,b,c,d,e);f=d;g=e.el;a:{while(true){if(c<b)return (-1);c=C0(f,a.fd,c);if(c<0)break a;if(c<b)break a;h=c+1|0;if(h<g&&BI(P(f,h))){c=c+(-1)|0;continue;}if(a.eg.cY(h,d,e)>=0)break;c=c+(-1)|0;}return c;}return (-1);},Qc=a=>{let b,c;b=a.fd;c=new W;Y(c);Bh(c,b);return X(c);},RO=(a,b)=>{if(b instanceof B8)return 0;if(b instanceof B2)return 0;if(b instanceof BO)return 0;if(b instanceof B7)return 0;if(b instanceof Dn)return 0;if(!(b instanceof DL))return 1;return b.fd!=a.fd?0:1;},Pm
=(a,b)=>{return 1;};
function B7(){let a=this;Bn.call(a);a.fz=0;a.fp=0;a.fg=0;}
let Rb=(a,b,c)=>{let d,e;d=b+1|0;c=c;e=P(c,b);d=P(c,d);return a.fz==e&&a.fp==d?2:(-1);},Ml=(a,b,c,d)=>{let e,f,g;if(!(c instanceof Br))return Dg(a,b,c,d);e=c;f=d.el;while(b<f){b=DH(e,a.fz,b);if(b<0)return (-1);b=b+1|0;if(b>=f)continue;g=P(e,b);if(a.fp==g&&a.eg.cY(b+1|0,c,d)>=0)return b+(-1)|0;b=b+1|0;}return (-1);},PH=(a,b,c,d,e)=>{let f;if(!(d instanceof Br))return CY(a,b,c,d,e);f=d;a:{while(true){if(c<b)return (-1);c=C0(f,a.fp,c)+(-1)|0;if(c<0)break a;if(c<b)break a;if(a.fz==P(f,c)&&a.eg.cY(c+2|0,d,e)>=0)break;c
=c+(-1)|0;}return c;}return (-1);},NQ=a=>{let b,c,d;b=a.fz;c=a.fp;d=new W;Y(d);Bh(d,b);Bh(d,c);return X(d);},QE=(a,b)=>{if(b instanceof B7)return b.fg!=a.fg?0:1;if(b instanceof BO)return b.dA(a.fg);if(b instanceof B8)return 0;if(!(b instanceof B2))return 1;return 0;},Jn=L(Dd),P4=(a,b)=>{return b!=10?0:1;},QW=(a,b,c)=>{return b!=10?0:1;},Jo=L(Dd),KB=(a,b)=>{return b!=10&&b!=13&&b!=133&&(b|1)!=8233?0:1;},Pe=(a,b,c)=>{a:{b:{if(b!=10&&b!=133&&(b|1)!=8233){if(b!=13)break b;if(c==10)break b;}b=1;break a;}b=0;}return b;};
function IL(){let a=this;J.call(a);a.gu=null;a.f$=null;a.fy=0;a.hC=0;}
let PU=(a,b)=>{let c,d;while(true){c=a.fy;if(b<c)break;a.fy=c<<1|1;}d=c<<1|1;a.fy=d;d=d+1|0;a.gu=BC(d);a.f$=BC(d);a.hC=b;},Hc=a=>{let b=new IL();PU(b,a);return b;},HR=(a,b,c)=>{let d,e,f,g;d=0;e=a.fy;f=b&e;while(true){g=a.gu.data;if(!g[f])break;if(g[f]==b)break;d=(d+1|0)&e;f=(f+d|0)&e;}g[f]=b;a.f$.data[f]=c;},F1=(a,b)=>{let c,d,e,f;c=a.fy;d=b&c;e=0;while(true){f=a.gu.data[d];if(!f)break;if(f==b)return a.f$.data[d];e=(e+1|0)&c;d=(d+e|0)&c;}return a.hC;};
function Jg(){let a=this;J.call(a);a.hx=null;a.hz=0;}
let RZ=(a,b)=>{a.hx=b;},JC=a=>{let b=new Jg();RZ(b,a);return b;},Np=L(),DY=b=>{let c,d,e,f,g;c=0;d=1;while(true){e=b.hx.data;f=b.hz;b.hz=f+1|0;g=e[f];g=g<34?g-32|0:g>=92?(g-32|0)-2|0:(g-32|0)-1|0;f=(g%2|0)!=1?0:1;c=c+DF(d,g/2|0)|0;d=d*46|0;if(!f)break;}return c;},Ev=b=>{let c,d;c=DY(b);d=c/2|0;if(c%2|0)d= -d|0;return d;},Dj=L(U),OR=a=>{return;},Lp=()=>{let a=new Dj();OR(a);return a;},HM=a=>{return Bv(Bk(BE(),9,13),32);},Ed=L(U),LH=a=>{return;},QG=()=>{let a=new Ed();LH(a);return a;},FP=a=>{return Bk(BE(),48,
57);},IB=L(U),MZ=a=>{return;},Qr=()=>{let a=new IB();MZ(a);return a;},R3=a=>{return Bk(BE(),97,122);},Fy=L(U),P5=a=>{return;},KX=()=>{let a=new Fy();P5(a);return a;},L1=a=>{return Bk(BE(),65,90);},FI=L(U),Ol=a=>{return;},KW=()=>{let a=new FI();Ol(a);return a;},KO=a=>{return Bk(BE(),0,127);},Ee=L(U),QZ=a=>{return;},Nl=()=>{let a=new Ee();QZ(a);return a;},IW=a=>{return Bk(Bk(BE(),97,122),65,90);},D1=L(Ee),Pd=a=>{return;},Kn=()=>{let a=new D1();Pd(a);return a;},GM=a=>{return Bk(IW(a),48,57);},Jq=L(U),LK=a=>{return;},P7
=()=>{let a=new Jq();LK(a);return a;},Ny=a=>{return Bk(Bk(Bk(BE(),33,64),91,96),123,126);},Ef=L(D1),Qa=a=>{return;},Nx=()=>{let a=new Ef();Qa(a);return a;},FN=a=>{return Bk(Bk(Bk(GM(a),33,64),91,96),123,126);},Ix=L(Ef),Ln=a=>{return;},Mo=()=>{let a=new Ix();Ln(a);return a;},K_=a=>{return Bv(FN(a),32);},Jv=L(U),JW=a=>{return;},Op=()=>{let a=new Jv();JW(a);return a;},Ru=a=>{return Bv(Bv(BE(),32),9);},JE=L(U);
let ON=a=>{return;},K0=()=>{let a=new JE();ON(a);return a;},KZ=a=>{return Bv(Bk(BE(),0,31),127);},Jc=L(U),OV=a=>{return;},NO=()=>{let a=new Jc();OV(a);return a;},Po=a=>{return Bk(Bk(Bk(BE(),48,57),97,102),65,70);},FS=L(U),Nt=a=>{return;},Pz=()=>{let a=new FS();Nt(a);return a;},MK=a=>{let b;b=new Gq;b.iv=a;Ba(b);b.eq=1;return b;},JG=L(U),QO=a=>{return;},Kr=()=>{let a=new JG();QO(a);return a;},Kt=a=>{let b;b=new F9;b.h1=a;Ba(b);b.eq=1;return b;},IM=L(U),Ou=a=>{return;},Mz=()=>{let a=new IM();Ou(a);return a;},Pa
=a=>{let b;b=new I9;b.h8=a;Ba(b);return b;},H4=L(U),K3=a=>{return;},KF=()=>{let a=new H4();K3(a);return a;},Rj=a=>{let b;b=new I8;b.hX=a;Ba(b);return b;},Gw=L(U),QP=a=>{return;},Kd=()=>{let a=new Gw();QP(a);return a;},Rr=a=>{let b;b=new Hn;b.h4=a;Ba(b);DP(b.ep,0,2048);b.eq=1;return b;},Ht=L(U),PV=a=>{return;},Ky=()=>{let a=new Ht();PV(a);return a;},JY=a=>{let b;b=new IQ;b.ia=a;Ba(b);b.eq=1;return b;},Gz=L(U),Kg=a=>{return;},Ki=()=>{let a=new Gz();Kg(a);return a;},O4=a=>{let b;b=new HN;b.ih=a;Ba(b);b.eq=1;return b;},I5
=L(U),LM=a=>{return;},O_=()=>{let a=new I5();LM(a);return a;},J4=a=>{let b;b=new Hq;b.iw=a;Ba(b);return b;},Jt=L(U),Og=a=>{return;},Ot=()=>{let a=new Jt();Og(a);return a;},Q3=a=>{let b;b=new F3;b.hU=a;Ba(b);b.eq=1;return b;},Gk=L(U),L2=a=>{return;},QL=()=>{let a=new Gk();L2(a);return a;},Ke=a=>{let b;b=new F8;b.id=a;Ba(b);b.eq=1;return b;},Fz=L(U),P_=a=>{return;},Lz=()=>{let a=new Fz();P_(a);return a;};
let MA=a=>{let b;b=new Hv;b.h3=a;Ba(b);b.eq=1;return b;},Ir=L(U),Rq=a=>{return;},KL=()=>{let a=new Ir();Rq(a);return a;},Q$=a=>{let b;b=new Jp;b.h9=a;Ba(b);b.eq=1;return b;},Jm=L(U),Mv=a=>{return;},Kw=()=>{let a=new Jm();Mv(a);return a;},MM=a=>{let b;b=new JA;b.h$=a;Ba(b);return b;},Hz=L(U),PY=a=>{return;},NY=()=>{let a=new Hz();PY(a);return a;},PP=a=>{let b;b=new Hr;b.ig=a;Ba(b);return b;},Gj=L(U),Rn=a=>{return;},OS=()=>{let a=new Gj();Rn(a);return a;},Mm=a=>{let b;b=new FO;b.h0=a;Ba(b);b.eq=1;return b;},Jz
=L(U),N1=a=>{return;},QB=()=>{let a=new Jz();N1(a);return a;},RI=a=>{let b;b=new Gn;b.it=a;Ba(b);b.eq=1;return b;},DE=L(U),Ku=a=>{return;},MN=()=>{let a=new DE();Ku(a);return a;},Jx=a=>{return Bv(Bk(Bk(Bk(BE(),97,122),65,90),48,57),95);},GH=L(DE),Oq=a=>{return;},Nz=()=>{let a=new GH();Oq(a);return a;},LR=a=>{let b;b=Cb(Jx(a),1);b.eq=1;return b;},ID=L(Dj),NV=a=>{return;},JP=()=>{let a=new ID();NV(a);return a;},N7=a=>{let b;b=Cb(HM(a),1);b.eq=1;return b;},F_=L(Ed),Qt=a=>{return;},ME=()=>{let a=new F_();Qt(a);return a;},Nc
=a=>{let b;b=Cb(FP(a),1);b.eq=1;return b;};
function Jk(){let a=this;U.call(a);a.hv=0;a.hR=0;}
let O1=(a,b,c)=>{a.hv=b;a.hR=c;},N=(a,b)=>{let c=new Jk();O1(c,a,b);return c;},R6=a=>{return Bk(BE(),a.hv,a.hR);},F2=L(U),PJ=a=>{return;},NT=()=>{let a=new F2();PJ(a);return a;},Ow=a=>{return Bk(Bk(BE(),65279,65279),65520,65533);};
function E0(){let a=this;U.call(a);a.f4=0;a.fT=0;a.gC=0;}
let QC=(a,b,c)=>{a.fT=c;a.f4=b;},Be=(a,b)=>{let c=new E0();QC(c,a,b);return c;},O3=(a,b,c,d)=>{a.gC=d;a.fT=c;a.f4=b;},RP=(a,b,c)=>{let d=new E0();O3(d,a,b,c);return d;},LL=a=>{let b;b=PE(a.f4);if(a.gC)DP(b.ep,0,2048);b.eq=a.fT;return b;};
function E_(){let a=this;U.call(a);a.f1=0;a.ge=0;a.g5=0;}
let Kf=(a,b,c)=>{a.ge=c;a.f1=b;},CC=(a,b)=>{let c=new E_();Kf(c,a,b);return c;},J9=(a,b,c,d)=>{a.g5=d;a.ge=c;a.f1=b;},O7=(a,b,c)=>{let d=new E_();J9(d,a,b,c);return d;},J7=a=>{let b;b=new IT;Hl(b,a.f1);if(a.g5)DP(b.ep,0,2048);b.eq=a.ge;return b;},JD=L(Bi),Ma=L();
function F0(){let a=this;J.call(a);a.hS=null;a.gJ=null;}
function DC(){let a=this;J.call(a);a.gY=0;a.gF=0;a.g7=null;}
let Gl=(a,b,c,d)=>{a.gY=b;a.gF=c;a.g7=d;},GC=(a,b,c)=>{let d=new DC();Gl(d,a,b,c);return d;},EA=L(Bi);
function GT(){let a=this;R.call(a);a.hm=null;a.h5=null;}
let K4=(a,b)=>{let c;c=b-55296|0;return c>=0&&c<2048?a.eF^BX(a.hm,c):0;};
function GP(){let a=this;R.call(a);a.gG=null;a.ha=null;a.il=null;}
let Lk=(a,b)=>{let c,d;c=b-55296|0;d=c>=0&&c<2048?a.eF^BX(a.gG,c):0;return a.ha.dA(b)&&!d?1:0;};
function Id(){let a=this;R.call(a);a.fH=null;a.h2=null;}
let Rv=(a,b)=>{return a.ew^BX(a.fH,b);},MS=a=>{let b,c,d;b=new W;Y(b);c=Db(a.fH,0);while(c>=0){DR(b,CI(c));Bh(b,124);c=Db(a.fH,c+1|0);}d=b.er;if(d>0)GB(b,d-1|0);return X(b);};
function Ij(){let a=this;R.call(a);a.hG=null;a.hY=null;}
let Rh=(a,b)=>{return a.hG.dA(b);};
function Ih(){let a=this;R.call(a);a.f0=0;a.hM=null;a.gi=null;}
let J0=(a,b)=>{return !(a.f0^BX(a.gi.en,b))&&!(a.f0^a.gi.eV^a.hM.dA(b))?0:1;};
function Ii(){let a=this;R.call(a);a.f_=0;a.gR=null;a.gk=null;}
let Kj=(a,b)=>{return !(a.f_^BX(a.gk.en,b))&&!(a.f_^a.gk.eV^a.gR.dA(b))?1:0;};
function Im(){let a=this;R.call(a);a.hq=0;a.gU=null;a.gN=null;a.im=null;}
let J5=(a,b)=>{return a.hq^(!a.gU.dA(b)&&!a.gN.dA(b)?0:1);};
function Io(){let a=this;R.call(a);a.hO=0;a.hB=null;a.hl=null;a.io=null;}
let JJ=(a,b)=>{return a.hO^(!a.hB.dA(b)&&!a.hl.dA(b)?0:1)?0:1;};
function Ik(){let a=this;R.call(a);a.g_=null;a.is=null;}
let M0=(a,b)=>{return BH(a.g_,b);};
function Il(){let a=this;R.call(a);a.g9=null;a.hT=null;}
let Km=(a,b)=>{return BH(a.g9,b)?0:1;};
function Ip(){let a=this;R.call(a);a.gD=null;a.hD=0;a.hH=null;}
let M3=(a,b)=>{return !BH(a.gD,b)&&!(a.hD^BX(a.hH.en,b))?0:1;};
function Iq(){let a=this;R.call(a);a.g8=null;a.hh=0;a.gP=null;}
let Q4=(a,b)=>{return !BH(a.g8,b)&&!(a.hh^BX(a.gP.en,b))?1:0;};
function Ic(){let a=this;R.call(a);a.hF=0;a.gQ=null;a.hi=null;a.h6=null;}
let R_=(a,b)=>{return !(a.hF^a.gQ.dA(b))&&!BH(a.hi,b)?0:1;};
function I1(){let a=this;R.call(a);a.hg=0;a.gL=null;a.g6=null;a.iu=null;}
let M$=(a,b)=>{return !(a.hg^a.gL.dA(b))&&!BH(a.g6,b)?1:0;};
function Ia(){let a=this;R.call(a);a.gM=null;a.hV=null;}
let Q2=(a,b)=>{return BH(a.gM,b);};
function Ib(){let a=this;R.call(a);a.gT=null;a.ii=null;}
let Mc=(a,b)=>{return BH(a.gT,b)?0:1;};
function Ig(){let a=this;R.call(a);a.hn=null;a.gZ=0;a.hQ=null;}
let P1=(a,b)=>{return BH(a.hn,b)&&a.gZ^BX(a.hQ.en,b)?1:0;};
function H_(){let a=this;R.call(a);a.hw=null;a.hP=0;a.gV=null;}
let Mb=(a,b)=>{return BH(a.hw,b)&&a.hP^BX(a.gV.en,b)?0:1;};
function Ie(){let a=this;R.call(a);a.hL=0;a.hj=null;a.hN=null;a.ik=null;}
let OO=(a,b)=>{return a.hL^a.hj.dA(b)&&BH(a.hN,b)?1:0;};
function If(){let a=this;R.call(a);a.hs=0;a.gI=null;a.hE=null;a.hZ=null;}
let Pj=(a,b)=>{return a.hs^a.gI.dA(b)&&BH(a.hE,b)?0:1;},FH=L(0);
function Fd(){let a=this;J.call(a);a.gx=null;a.gf=null;}
function Eq(){let a=this;Fd.call(a);a.gc=0;a.fB=null;}
function DG(){let a=this;Eq.call(a);a.ff=null;a.fn=null;}
let GI=L(Bi),GD=L(CM),LT=(a,b,c,d)=>{let e,f,g;e=0;f=d.el;a:{while(true){if(b>f){b=e;break a;}g=CL(d,a.es);B0(d,a.es,b);e=a.eM.cY(b,c,d);if(e>=0)break;B0(d,a.es,g);b=b+1|0;}}return b;},Rs=(a,b,c,d,e)=>{let f,g;f=0;a:{while(true){if(c<b){c=f;break a;}g=CL(e,a.es);B0(e,a.es,c);f=a.eM.cY(c,d,e);if(f>=0)break;B0(e,a.es,g);c=c+(-1)|0;}}return c;},Qv=a=>{return null;};
function Gq(){R.call(this);this.iv=null;}
let Nr=(a,b)=>{Bb();return BB(b)!=2?0:1;};
function F9(){R.call(this);this.h1=null;}
let NR=(a,b)=>{Bb();return BB(b)!=1?0:1;};
function I9(){R.call(this);this.h8=null;}
let MP=(a,b)=>{return HI(b);};
function I8(){R.call(this);this.hX=null;}
let LJ=(a,b)=>{return 0;};
function Hn(){R.call(this);this.h4=null;}
let Py=(a,b)=>{Bb();return !BB(b)?0:1;};
function IQ(){R.call(this);this.ia=null;}
let Nw=(a,b)=>{Bb();return BB(b)!=9?0:1;};
function HN(){R.call(this);this.ih=null;}
let NN=(a,b)=>{return C4(b);};
function Hq(){R.call(this);this.iw=null;}
let Rx=(a,b)=>{a:{b:{Bb();if(!(b>=0&&b<=31)){if(b<127)break b;if(b>159)break b;}b=1;break a;}b=0;}return b;};
function F3(){R.call(this);this.hU=null;}
let QJ=(a,b)=>{a:{b:{Bb();switch(BB(b)){case 1:case 2:case 3:case 4:case 5:case 6:case 8:case 9:case 10:case 23:case 26:break;case 7:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:case 24:case 25:break b;default:break b;}b=1;break a;}b=C4(b);}return b;};
function F8(){R.call(this);this.id=null;}
let R2=(a,b)=>{a:{b:{Bb();switch(BB(b)){case 1:case 2:case 3:case 4:case 5:case 10:case 23:case 26:break;case 6:case 7:case 8:case 9:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:case 24:case 25:break b;default:break b;}b=1;break a;}b=C4(b);}return b;};
function Hv(){R.call(this);this.h3=null;}
let On=(a,b)=>{a:{Bb();switch(BB(b)){case 1:case 2:case 3:case 4:case 5:break;default:b=0;break a;}b=1;}return b;};
function Jp(){R.call(this);this.h9=null;}
let KQ=(a,b)=>{return Ho(b);};
function JA(){R.call(this);this.h$=null;}
let Qh=(a,b)=>{return JB(b);};
function Hr(){R.call(this);this.ig=null;}
let M4=(a,b)=>{Bb();return BB(b)!=3?0:1;};
function FO(){R.call(this);this.h0=null;}
let PQ=(a,b)=>{a:{b:{Bb();switch(BB(b)){case 1:case 2:case 3:case 4:case 5:case 6:case 8:case 9:case 10:case 23:break;case 7:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:break b;default:break b;}b=1;break a;}b=C4(b);}return b;};
function Gn(){R.call(this);this.it=null;}
let Rt=(a,b)=>{a:{b:{Bb();switch(BB(b)){case 1:case 2:case 3:case 4:case 5:case 10:break;case 6:case 7:case 8:case 9:break b;default:break b;}b=1;break a;}b=C4(b);}return b;};
function D0(){R.call(this);this.gd=0;}
let Hl=(a,b)=>{Ba(a);a.gd=b;},PE=a=>{let b=new D0();Hl(b,a);return b;},NX=(a,b)=>{return a.ew^(a.gd!=DD(b&65535)?0:1);},IT=L(D0),Le=(a,b)=>{return a.ew^(!(a.gd>>DD(b&65535)&1)?0:1);};
function He(){let a=this;J.call(a);a.hd=0;a.hf=0;a.hk=0;a.gO=0;a.hp=null;}
let Os=L(),GY=L(Bi);
LQ([-1,"java",0,"util",0,"lang"]);
Cu([J,0,0,[],1,0,0,["g",M(Is)],B$,0,J,[],1537,0,0,0,DX,0,J,[],1537,0,0,0,EY,0,J,[],1537,0,0,0,Br,0,J,[B$,DX,EY],17,0,()=>Ce(),["h",O(P),"E",M(Dz),"g",M(Px)],Fk,0,J,[B$],1025,0,0,0,Dw,0,Fk,[DX],1,0,()=>BN(),0,Cl,0,J,[B$,EY],0,0,0,["y",O(EQ),"g",M(X)],EL,0,J,[],1537,0,0,0,W,0,Cl,[EL],1,0,0,["G",Bz(QF),"H",S(JU),"D",Bz(OI),"F",S(RF),"h",O(MC),"E",M(C5),"g",M(C9),"y",O(Q6),"B",Bq(Kc),"x",Bq(RA)],Ff,0,J,[],1,0,0,0,Cr,0,Ff,[],1,0,0,0,Bi,0,Cr,[],1,0,0,0,CT,0,J,[],1025,0,0,0,Me,0,CT,[],17,0,0,0,LO,0,CT,[],17,0,0,0,GE,
0,J,[],1537,0,0,0,Jl,0,J,[GE],1537,0,0,0,FU,0,J,[],1537,0,0,0,GX,0,J,[Jl,FU],17,0,0,0,I_,0,J,[],17,0,()=>Bx(),0,Pw,0,J,[],17,0,0,0,Q1,0,J,[],17,0,0,0,RT,0,J,[],17,0,0,0,E7,0,J,[],1537,0,0,0,Hw,0,J,[E7],1,0,0,0,E9,0,J,[DX],1,0,()=>Bb(),0,LX,0,J,[],17,0,0,0,Rf,0,J,[],17,0,0,0,HH,0,J,[B$],17,0,0,0,EC,0,J,[],1537,0,0,0,FL,0,J,[EC],17,0,0,0,DT,0,Cl,[EL],1,0,0,["G",Bz(LI),"H",S(OJ),"D",Bz(Ms),"F",S(Q7),"y",O(PM),"B",Bq(Ly),"x",Bq(K2)],Bo,"IndexOutOfBoundsException",2,Bi,[],1,[0,0,0],0,0,Jf,0,J,[EC],0,0,0,0,Bs,"IllegalArgumentException",
2,Bi,[],1,[0,0,0],0,0,CB,0,Bs,[],1,0,0,0,OC,0,J,[],17,0,0,0,H2,0,J,[],16,0,0,0,Do,"NullPointerException",2,Bi,[],1,[0,0,0],0,0,Bd,0,J,[],1024,0,()=>Dc(),["cU",S(Dg),"du",Bz(CY),"cm",M(Pg),"g",M(Rl),"bY",O(MU),"ck",O(MT),"c0",M(PA),"bP",M(Dm)],GF,0,J,[],1537,0,0,0,DQ,0,J,[GF],1537,0,0,0,CU,0,J,[DQ],1025,0,0,0,I4,0,J,[DQ],1537,0,0,0,Ge,0,J,[I4],1537,0,0,0,C6,0,CU,[Ge],1025,0,0,0,CS,0,J,[],1537,0,0,0,EV,0,J,[],1537,0,0,0,HD,0,C6,[CS,B$,EV],1,0,0,0]);
Cu([Gv,0,J,[],0,0,0,["g",M(R$)],PZ,"ClassCastException",2,Bi,[],1,[0,0,0],0,0,By,0,Bd,[],0,0,()=>Ct(),["cY",S(Nn),"L",M(NS),"dQ",O(OM)],Hs,0,J,[],0,0,0,0,ND,0,J,[],1,0,0,0,CJ,"StringIndexOutOfBoundsException",2,Bo,[],1,[0,0,0],0,0,OL,0,J,[],17,0,0,0,IN,0,By,[],0,0,0,["cY",S(LP),"L",M(Ra),"dQ",O(R5)],IK,0,By,[],0,0,0,["cY",S(QI),"L",M(P0)],FQ,0,By,[],0,0,0,["cY",S(O$),"L",M(MD)],Gc,0,By,[],0,0,0,["cY",S(L_),"L",M(KG),"dQ",O(Or)],Cz,0,By,[],0,0,0,["cY",S(Nu),"L",M(PN)],Bn,0,Bd,[],1024,0,0,["cY",S(QV),"dq",M(LB),
"dQ",O(PS)],Gp,0,Bn,[],0,0,0,["dr",Bq(Kl),"cU",S(Ls),"du",Bz(Qp),"L",M(NU),"dQ",O(L3)],Bj,0,Bd,[],0,0,0,["cY",S(MY),"bY",O(Oh),"L",M(P9),"ck",O(RY),"dQ",O(Qe),"bP",M(Qk)],D7,0,Bj,[],0,0,0,["cY",S(NF),"L",M(J6),"dQ",O(Q9)],BZ,0,D7,[],0,0,0,["cY",S(RX),"bY",O(Qm),"L",M(LY)],F4,0,BZ,[],0,0,0,["cY",S(N3),"dQ",O(M2),"L",M(Ps)],Gx,0,BZ,[],0,0,0,["cY",S(Oz),"dQ",O(LS),"L",M(MH)],HK,0,BZ,[],0,0,0,["cY",S(Qb),"dQ",O(RL),"L",M(M9)],Hb,0,BZ,[],0,0,0,["cY",S(KH),"dQ",O(RD),"L",M(Pb)],CM,0,Bj,[],0,0,0,["cY",S(Lm),"cU",S(Kh),
"du",Bz(Sd),"ck",O(Rz),"c0",M(Om),"bP",M(PG)],En,0,J,[],1537,0,0,0,Cs,0,J,[En],1025,0,0,["bR",Bq(NI)],D2,0,Cs,[CS,B$],1,0,0,0,G_,0,J,[En],1537,0,0,0,Hf,0,D2,[G_],1,0,0,["bR",Bq(NW)],HF,0,J,[],17,0,()=>DA(),0,BK,0,Bs,[],1,0,0,0,L$,0,J,[],17,0,0,0,DJ,"ArrayStoreException",2,Bi,[],1,[0,0,0],0,0,C8,0,J,[],1024,0,0,0,R,0,C8,[],1024,0,()=>CK(),["dB",M(OG),"dz",M(MJ),"dU",M(Lo),"cJ",M(Pp)],Et,"MissingResourceException",1,Bi,[],1,[0,0,0],0,0,Fg,0,R,[],0,0,0,["dA",O(BH),"dB",M(BD),"dz",M(K6),"dU",M(Mp),"g",M(Mi),"cJ",
M(LE)],B6,0,Bd,[],1024,0,0,["ck",O(JM),"dQ",O(Ng),"bP",M(K5)],BF,0,B6,[],0,0,0,["cY",S(KJ),"L",M(Qf)],Ck,0,BF,[],0,0,0,["cY",S(Nv),"L",M(OK)],BG,0,B6,[],0,0,0,["cY",S(Ll),"L",M(OP)],Ci,0,BF,[],0,0,0,["cY",S(Kq),"bY",O(R1)],GW,0,BF,[],0,0,0,["cY",S(Qy),"cU",S(L9)],IF,0,Cs,[],0,0,0,0,EG,0,J,[],16,0,()=>Eb(),0,U,0,J,[],1024,0,0,0,Gt,0,C8,[CS],0,0,0,["g",M(EE)],HT,0,Bd,[],0,0,0,["cY",S(Qu),"L",M(N0),"dQ",O(Oe)],EN,0,J,[CS,B$],1,0,0,0,Ga,0,Bj,[],0,0,0,["L",M(OA)],Ex,0,Bj,[],0,0,0,["cY",S(Nh),"bY",O(NC),"L",M(Pl),
"dQ",O(Pr),"ck",O(OE)],BO,0,Bj,[],0,0,0,["cY",S(KK),"L",M(O9),"dA",O(L6),"ck",O(M1),"bY",O(Lc),"dQ",O(Lh)]]);
Cu([Em,0,BO,[],0,0,0,["dA",O(PT),"L",M(P6)],It,0,Bn,[],0,0,0,["dr",Bq(Qz),"L",M(OT)],B2,0,Bn,[],0,0,0,["dr",Bq(Eg),"L",M(QY),"ck",O(Oj)],Fa,0,Bj,[],0,0,0,["bY",O(Ko),"L",M(RH),"cY",S(Kp),"ck",O(O5),"dQ",O(MG)],B8,0,Bn,[],0,0,0,["dq",M(P$),"dr",Bq(Nk),"cU",S(KE),"du",Bz(Ql),"L",M(M8),"ck",O(Mg)],HJ,0,Bn,[],0,0,0,["dr",Bq(J1),"L",M(NA)],F7,0,Bn,[],0,0,0,["dr",Bq(K9),"L",M(Of)],Cg,0,Bj,[],0,0,0,["bY",O(PF),"cY",S(NE),"L",M(Nb),"ck",O(QA),"dQ",O(Qo)],H9,0,Cg,[],0,0,0,0,Hy,0,Cg,[],0,0,0,0,JF,0,BG,[],0,0,0,["cY",
S(QU)],FG,0,BG,[],0,0,0,["cY",S(LD)],CN,0,BG,[],0,0,0,["cY",S(KU),"bY",O(NM)],Jh,0,CN,[],0,0,0,["cY",S(Qd),"bY",O(MB)],Cf,0,BG,[],0,0,0,["cY",S(Rw),"L",M(N4)],GG,0,Cf,[],0,0,0,["cY",S(OQ)],Iv,0,BG,[],0,0,0,["cY",S(P8)],G8,0,CN,[],0,0,0,["cY",S(Pu)],IP,0,Cf,[],0,0,0,["cY",S(Mk)],Iw,0,B6,[],0,0,0,["cY",S(Q5),"cU",S(Lr),"L",M(PD)],H$,0,B6,[],0,0,0,["cY",S(Oo),"cU",S(KC),"L",M(R0)],Dd,0,J,[],1024,0,0,0,JH,0,BF,[],0,0,0,["cY",S(Mu)],GN,0,Ci,[],0,0,0,["cY",S(Lt)],IX,0,Ck,[],0,0,0,["cY",S(JR)],G4,0,BF,[],0,0,0,["cY",
S(N8)],HG,0,Ci,[],0,0,0,["cY",S(MR)],HL,0,Ck,[],0,0,0,["cY",S(Ks)],EU,0,Bd,[],16,0,0,["cY",S(RK),"dQ",O(Ph),"L",M(Lx)],FM,0,Bd,[],0,0,0,["cY",S(Ox),"dQ",O(OW),"L",M(Rp)],H1,0,Bd,[],0,0,0,["cY",S(PW),"dQ",O(Ri),"L",M(NL)],EO,0,Bd,[],16,0,0,["cY",S(P3),"dQ",O(Qw),"L",M(KD)],Hi,0,Bd,[],0,0,0,["cY",S(Nq),"dQ",O(JX),"L",M(MX)],GZ,0,Bd,[],0,0,0,["cY",S(Qg),"dQ",O(Mt),"L",M(M_)],G9,0,Bj,[],0,0,0,["cY",S(Qi),"L",M(Sb),"bY",O(O8),"cm",M(MQ),"dQ",O(O6)],IH,0,Bj,[],16,0,0,["cY",S(Nd),"L",M(QR),"bY",O(RW),"cm",M(JI),"dQ",
O(QT)],GO,0,Bd,[],16,0,0,["cY",S(QN),"dQ",O(Mf),"L",M(JN)],Gg,0,Bd,[],0,0,0,["cY",S(Nm),"dQ",O(LU),"L",M(LV)],FZ,0,Bd,[],0,0,0,["cY",S(O2),"dQ",O(QD),"L",M(My)],CA,0,Bj,[],0,0,0,["cY",S(MF),"bY",O(Qj),"L",M(L0),"dQ",O(Q_)],G1,0,CA,[],0,0,0,["cY",S(QS),"cU",S(MW),"du",Bz(LW),"ck",O(Lj),"L",M(NG)],HE,0,CA,[],0,0,0,["cY",S(Ka),"L",M(OX)],IA,0,Bn,[],0,0,0,["dr",Bq(RM),"cU",S(Ok),"du",Bz(Mq),"L",M(NP),"ck",O(NJ)],FV,0,Bn,[],0,0,0,["dr",Bq(Kk),"L",M(Oi)],GQ,0,Bn,[],0,0,0,["dr",Bq(JV),"L",M(Mn)],IU,0,J,[DQ],1537,0,
0,0,El,0,CU,[IU],1025,0,0,0,Ey,0,El,[],1024,0,0,0,FF,0,Ey,[],0,0,0,0,E$,0,Cs,[],1024,0,0,["bR",Bq(KV)]]);
Cu([FC,0,E$,[],0,0,0,0,EI,0,C6,[EV],1024,0,0,0,FD,0,EI,[],0,0,0,0,D_,0,J,[],1537,0,0,0,FA,0,J,[D_],0,0,0,0,Ju,0,J,[D_],1537,0,0,0,FB,0,J,[Ju],0,0,0,0,Jj,0,J,[E7],1,0,0,0,RQ,0,J,[],16,0,0,0,Pt,0,J,[],17,0,0,0,FR,0,Bn,[],0,0,0,["dr",Bq(J2),"L",M(Rk)],Dn,0,Bj,[],0,0,0,["bY",O(N2),"cY",S(Qx),"cU",S(NB),"du",Bz(R9),"L",M(LF),"ck",O(LN),"dQ",O(LZ)],DL,0,Bj,[],0,0,0,["bY",O(PK),"cY",S(KI),"cU",S(OU),"du",Bz(JS),"L",M(Qc),"ck",O(RO),"dQ",O(Pm)],B7,0,Bn,[],0,0,0,["dr",Bq(Rb),"cU",S(Ml),"du",Bz(PH),"L",M(NQ),"ck",O(QE)],Jn,
0,Dd,[],0,0,0,["d7",O(P4),"d_",Bq(QW)],Jo,0,Dd,[],0,0,0,["d7",O(KB),"d_",Bq(Pe)],IL,0,J,[],0,0,0,0,Jg,0,J,[],1,0,0,0,Np,0,J,[],17,0,0,0,Dj,0,U,[],0,0,0,["dT",M(HM)],Ed,0,U,[],0,0,0,["dT",M(FP)],IB,0,U,[],0,0,0,["dT",M(R3)],Fy,0,U,[],0,0,0,["dT",M(L1)],FI,0,U,[],0,0,0,["dT",M(KO)],Ee,0,U,[],0,0,0,["dT",M(IW)],D1,0,Ee,[],0,0,0,["dT",M(GM)],Jq,0,U,[],0,0,0,["dT",M(Ny)],Ef,0,D1,[],0,0,0,["dT",M(FN)],Ix,0,Ef,[],0,0,0,["dT",M(K_)],Jv,0,U,[],0,0,0,["dT",M(Ru)],JE,0,U,[],0,0,0,["dT",M(KZ)],Jc,0,U,[],0,0,0,["dT",M(Po)],FS,
0,U,[],0,0,0,["dT",M(MK)],JG,0,U,[],0,0,0,["dT",M(Kt)],IM,0,U,[],0,0,0,["dT",M(Pa)],H4,0,U,[],0,0,0,["dT",M(Rj)],Gw,0,U,[],0,0,0,["dT",M(Rr)],Ht,0,U,[],0,0,0,["dT",M(JY)],Gz,0,U,[],0,0,0,["dT",M(O4)],I5,0,U,[],0,0,0,["dT",M(J4)],Jt,0,U,[],0,0,0,["dT",M(Q3)],Gk,0,U,[],0,0,0,["dT",M(Ke)],Fz,0,U,[],0,0,0,["dT",M(MA)],Ir,0,U,[],0,0,0,["dT",M(Q$)],Jm,0,U,[],0,0,0,["dT",M(MM)],Hz,0,U,[],0,0,0,["dT",M(PP)],Gj,0,U,[],0,0,0,["dT",M(Mm)],Jz,0,U,[],0,0,0,["dT",M(RI)],DE,0,U,[],0,0,0,["dT",M(Jx)],GH,0,DE,[],0,0,0,["dT",
M(LR)]]);
Cu([ID,0,Dj,[],0,0,0,["dT",M(N7)],F_,0,Ed,[],0,0,0,["dT",M(Nc)],Jk,0,U,[],0,0,0,["dT",M(R6)],F2,0,U,[],0,0,0,["dT",M(Ow)],E0,0,U,[],0,0,0,["dT",M(LL)],E_,0,U,[],0,0,0,["dT",M(J7)],JD,"NegativeArraySizeException",2,Bi,[],1,[0,0,0],0,0,Ma,0,J,[],0,0,0,0,F0,0,J,[],1,0,0,0,DC,0,J,[],1,0,0,0,EA,"UnsupportedOperationException",2,Bi,[],1,[0,0,0],0,0,GT,0,R,[],0,0,0,["dA",O(K4)],GP,0,R,[],0,0,0,["dA",O(Lk)],Id,0,R,[],0,0,0,["dA",O(Rv),"g",M(MS)],Ij,0,R,[],0,0,0,["dA",O(Rh)],Ih,0,R,[],0,0,0,["dA",O(J0)],Ii,0,R,[],0,
0,0,["dA",O(Kj)],Im,0,R,[],0,0,0,["dA",O(J5)],Io,0,R,[],0,0,0,["dA",O(JJ)],Ik,0,R,[],0,0,0,["dA",O(M0)],Il,0,R,[],0,0,0,["dA",O(Km)],Ip,0,R,[],0,0,0,["dA",O(M3)],Iq,0,R,[],0,0,0,["dA",O(Q4)],Ic,0,R,[],0,0,0,["dA",O(R_)],I1,0,R,[],0,0,0,["dA",O(M$)],Ia,0,R,[],0,0,0,["dA",O(Q2)],Ib,0,R,[],0,0,0,["dA",O(Mc)],Ig,0,R,[],0,0,0,["dA",O(P1)],H_,0,R,[],0,0,0,["dA",O(Mb)],Ie,0,R,[],0,0,0,["dA",O(OO)],If,0,R,[],0,0,0,["dA",O(Pj)],FH,0,J,[],1537,0,0,0,Fd,0,J,[FH,CS],0,0,0,0,Eq,0,Fd,[],0,0,0,0,DG,0,Eq,[],16,0,0,0,GI,"IllegalStateException",
2,Bi,[],1,[0,0,0],0,0,GD,0,CM,[],0,0,0,["cU",S(LT),"du",Bz(Rs),"c0",M(Qv)],Gq,0,R,[],0,0,0,["dA",O(Nr)],F9,0,R,[],0,0,0,["dA",O(NR)],I9,0,R,[],0,0,0,["dA",O(MP)],I8,0,R,[],0,0,0,["dA",O(LJ)],Hn,0,R,[],0,0,0,["dA",O(Py)],IQ,0,R,[],0,0,0,["dA",O(Nw)],HN,0,R,[],0,0,0,["dA",O(NN)],Hq,0,R,[],0,0,0,["dA",O(Rx)],F3,0,R,[],0,0,0,["dA",O(QJ)],F8,0,R,[],0,0,0,["dA",O(R2)],Hv,0,R,[],0,0,0,["dA",O(On)],Jp,0,R,[],0,0,0,["dA",O(KQ)],JA,0,R,[],0,0,0,["dA",O(Qh)]]);
Cu([Hr,0,R,[],0,0,0,["dA",O(M4)],FO,0,R,[],0,0,0,["dA",O(PQ)],Gn,0,R,[],0,0,0,["dA",O(Rt)],D0,0,R,[],0,0,0,["dA",O(NX)],IT,0,D0,[],0,0,0,["dA",O(Le)],He,0,J,[D_],0,0,0,0,Os,0,J,[],0,0,0,0,GY,"ConcurrentModificationException",1,Bi,[],1,[0,0,0],0,0]);
let Pn=CG(HC),Pf=CG(JO),Iu=CG(Gs);
K$(["0","<java_object>@","null","String is null","String is empty","String contains invalid digits: ","String contains digits out of radix ",": ","The value is too big for integer type","The value is too big for int type: ","Illegal radix: ","[L","[]","","compile-error","TeaVM rejected the pattern.",", ","PatternSyntaxException","limit-error","CaptureGroupLimit","The pattern exceeds the 1,000 capture-group execution limit.","1","ok","replacement-error","execution-error","TeaVM Matcher rejected the replacement.",
"TeaVM failed while matching.","IllegalArgumentException","Result limits must be positive.","bridge-error","NullPointerException","Pattern and subject must not be null.","Error","(?<=a)(b)\\1","abb","<$1>","a<b>","Patter is null","\\Q","\\E","\\\\E\\Q","fSet","Name capturing group should start with letter","Is","In","Either src or dest is null","NonCapFSet","AheadFSet","BehindFSet","AtomicFSet","FinalSet","<Empty set>","JointSet","NonCapJointSet","PosLookaheadJointSet","NegLookaheadJointSet","PosBehindJointSet",
"NegBehindJointSet","<Quant>","<GroupQuant>","Lower","Upper","ASCII","Alpha","Digit","Alnum","Punct","Graph","Print","Blank","Cntrl","XDigit","javaLowerCase","javaUpperCase","javaWhitespace","javaMirrored","javaDefined","javaDigit","javaIdentifierIgnorable","javaISOControl","javaJavaIdentifierPart","javaJavaIdentifierStart","javaLetter","javaLetterOrDigit","javaSpaceChar","javaTitleCase","javaUnicodeIdentifierPart","javaUnicodeIdentifierStart","Space","w","W","s","S","d","D","BasicLatin","Latin-1Supplement",
"LatinExtended-A","LatinExtended-B","IPAExtensions","SpacingModifierLetters","CombiningDiacriticalMarks","Greek","Cyrillic","CyrillicSupplement","Armenian","Hebrew","Arabic","Syriac","ArabicSupplement","Thaana","Devanagari","Bengali","Gurmukhi","Gujarati","Oriya","Tamil","Telugu","Kannada","Malayalam","Sinhala","Thai","Lao","Tibetan","Myanmar","Georgian","HangulJamo","Ethiopic","EthiopicSupplement","Cherokee","UnifiedCanadianAboriginalSyllabics","Ogham","Runic","Tagalog","Hanunoo","Buhid","Tagbanwa","Khmer",
"Mongolian","Limbu","TaiLe","NewTaiLue","KhmerSymbols","Buginese","PhoneticExtensions","PhoneticExtensionsSupplement","CombiningDiacriticalMarksSupplement","LatinExtendedAdditional","GreekExtended","GeneralPunctuation","SuperscriptsandSubscripts","CurrencySymbols","CombiningMarksforSymbols","LetterlikeSymbols","NumberForms","Arrows","MathematicalOperators","MiscellaneousTechnical","ControlPictures","OpticalCharacterRecognition","EnclosedAlphanumerics","BoxDrawing","BlockElements","GeometricShapes","MiscellaneousSymbols",
"Dingbats","MiscellaneousMathematicalSymbols-A","SupplementalArrows-A","BraillePatterns","SupplementalArrows-B","MiscellaneousMathematicalSymbols-B","SupplementalMathematicalOperators","MiscellaneousSymbolsandArrows","Glagolitic","Coptic","GeorgianSupplement","Tifinagh","EthiopicExtended","SupplementalPunctuation","CJKRadicalsSupplement","KangxiRadicals","IdeographicDescriptionCharacters","CJKSymbolsandPunctuation","Hiragana","Katakana","Bopomofo","HangulCompatibilityJamo","Kanbun","BopomofoExtended","CJKStrokes",
"KatakanaPhoneticExtensions","EnclosedCJKLettersandMonths","CJKCompatibility","CJKUnifiedIdeographsExtensionA","YijingHexagramSymbols","CJKUnifiedIdeographs","YiSyllables","YiRadicals","ModifierToneLetters","SylotiNagri","HangulSyllables","HighSurrogates","HighPrivateUseSurrogates","LowSurrogates","PrivateUseArea","CJKCompatibilityIdeographs","AlphabeticPresentationForms","ArabicPresentationForms-A","VariationSelectors","VerticalForms","CombiningHalfMarks","CJKCompatibilityForms","SmallFormVariants","ArabicPresentationForms-B",
"HalfwidthandFullwidthForms","all","Specials","Cn","IsL","Lu","Ll","Lt","Lm","Lo","IsM","Mn","Me","Mc","N","Nd","Nl","No","IsZ","Zs","Zl","Zp","IsC","Cc","Cf","Co","Cs","IsP","Pd","Ps","Pe","Pc","Po","IsS","Sm","Sc","Sk","So","Pi","Pf","posFSet"," ","^ ","range:","CompositeRangeSet: <nonsurrogate> "," <surrogate> ","UCI range:","decomposed Hangul syllable:","UCI ","CI ","decomposed char:","<DotAllQuant>","<DotQuant>","<SOL>","WordBoundary","PreviousMatch","<EOL>","EOI","^","DotAll",".","<Unix MultiLine $>",
"<MultiLine $>","CI back reference: ","back reference: ","UCI back reference: ","sequence: ","UCI sequence: ","CI sequence: "]);
Br.prototype.toString=function(){return D$(this);};
Br.prototype.valueOf=Br.prototype.toString;J.prototype.toString=function(){return D$(Is(this));};
J.prototype.__teavm_class__=function(){return RB(this);};
let C=JT(O0);
C.javaException=N_;
let Sl=Symbol('jsoClass');
(()=>{let c;})();
export{C as main,D as bridgeAbiVersion,E as engineIdentity,F as selfTest,G as execute,H as replace};

View File

@@ -0,0 +1,104 @@
PCRE2 Licence
=============
| SPDX-License-Identifier: | BSD-3-Clause WITH PCRE2-exception |
|---------|-------|
PCRE2 is a library of functions to support regular expressions whose syntax
and semantics are as close as possible to those of the Perl 5 language.
Releases 10.00 and above of PCRE2 are distributed under the terms of the "BSD"
licence, as specified below, with one exemption for certain binary
redistributions. The documentation for PCRE2, supplied in the "doc" directory,
is distributed under the same terms as the software itself. The data in the
testdata directory is not copyrighted and is in the public domain.
The basic library functions are written in C and are freestanding. Also
included in the distribution is a just-in-time compiler that can be used to
optimize pattern matching. This is an optional feature that can be omitted when
the library is built. The just-in-time compiler is separately licensed under the
"2-clause BSD" licence.
COPYRIGHT
---------
### The basic library functions
Written by: Philip Hazel
Email local part: Philip.Hazel
Email domain: gmail.com
Retired from University of Cambridge Computing Service,
Cambridge, England.
Copyright (c) 1997-2007 University of Cambridge
Copyright (c) 2007-2024 Philip Hazel
All rights reserved.
### PCRE2 Just-In-Time compilation support
Written by: Zoltan Herczeg
Email local part: hzmester
Email domain: freemail.hu
Copyright (c) 2010-2024 Zoltan Herczeg
All rights reserved.
### Stack-less Just-In-Time compiler
Written by: Zoltan Herczeg
Email local part: hzmester
Email domain: freemail.hu
Copyright (c) 2009-2024 Zoltan Herczeg
All rights reserved.
The code in the `deps/sljit` directory has its own LICENSE file.
### All other contributions
Many other contributors have participated in the authorship of PCRE2. As PCRE2
has never required a Contributor Licensing Agreement, or other copyright
assignment agreement, all contributions have copyright retained by each
original contributor or their employer.
THE "BSD" LICENCE
-----------------
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notices,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notices, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the University of Cambridge nor the names of any
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
EXEMPTION FOR BINARY LIBRARY-LIKE PACKAGES
------------------------------------------
The second condition in the BSD licence (covering binary redistributions) does
not apply all the way down a chain of software. If binary package A includes
PCRE2, it must respect the condition, but if package B is software that
includes package A, the condition is not imposed on package B unless it uses
PCRE2 independently.

View File

@@ -0,0 +1,4 @@
197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811 LICENSE.txt
8c7f67ad85893fcc545c40d496abcc82d1009b76c9b7bcc7bd9ab92828852d76 engine-metadata.json
d31a5f1e0839955ae8321430eafe65da8cbdef832a1245bb71ccc5bb0d4b6b9c pcre2.mjs
cc7c4b2b0038e137c86d2037e73f2302e56432cb09bc082e4f35bba221dd1889 pcre2.wasm

View File

@@ -0,0 +1,90 @@
{
"schemaVersion": 1,
"engine": "pcre2",
"engineVersion": "10.47 2025-10-21",
"status": "production-runtime",
"bridge": {
"abiVersion": 3,
"maximumPatternBytes": 1048576,
"maximumSubjectBytes": 16777216,
"maximumReplacementBytes": 262144,
"maximumMatches": 10000,
"maximumCaptureRows": 100000,
"maximumCaptureGroups": 1000,
"maximumTraceEvents": 50000,
"maximumTraceBytes": 10485760,
"maximumTraceMarkBytes": 1024,
"sourceFiles": [
{
"path": "engines/pcre2/CMakeLists.txt",
"sha256": "670397b070a908b414d4d8ffa7c0a9ed2549b9ee27815a90de5fc9622c6df8fa",
"bytes": 2583
},
{
"path": "engines/pcre2/pcre2_bridge.c",
"sha256": "ab5929472d9981eb2d1cc635a4963a7243d5cbb9db6c5ba8460136d4c79b3246",
"bytes": 32530
},
{
"path": "engines/pcre2/pcre2_bridge.h",
"sha256": "b8767c0c59229a92d092915fc7b862a22d088a7f184485e4d67e14d6071d7d1c",
"bytes": 7388
}
],
"exports": [
"_free",
"_malloc",
"_regex_pcre2_bridge_abi_version",
"_regex_pcre2_compile_probe",
"_regex_pcre2_config_flags",
"_regex_pcre2_error_message",
"_regex_pcre2_execute",
"_regex_pcre2_self_test",
"_regex_pcre2_substitute",
"_regex_pcre2_trace",
"_regex_pcre2_version"
]
},
"source": {
"repository": "https://github.com/PCRE2Project/pcre2.git",
"tag": "pcre2-10.47",
"tagObjectSha1": "cd007b4466798f66d479d1442a407099e7c40050",
"commitSha1": "f454e231fe5006dd7ff8f4693fd2b8eb94333429",
"treeSha1": "81a83a3552bd68d0ea7b7004f8bb6e7892f583ba",
"license": "BSD-3-Clause WITH PCRE2-exception"
},
"toolchain": {
"emscriptenVersion": "6.0.4",
"emscriptenCompilerRevision": "fe5be6afdff43ad58860d821fcc8572a23f92d19",
"emsdkCommitSha1": "224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f",
"cmakeVersion": "4.3.4",
"ninjaVersion": "1.13.2"
},
"configuration": {
"codeUnitWidth": 8,
"unicode": true,
"jit": false,
"threads": false,
"filesystem": false,
"initialMemoryBytes": 16777216,
"maximumMemoryBytes": 268435456
},
"sourceDateEpoch": 1760997684,
"files": [
{
"path": "LICENSE.txt",
"sha256": "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811",
"bytes": 4011
},
{
"path": "pcre2.mjs",
"sha256": "d31a5f1e0839955ae8321430eafe65da8cbdef832a1245bb71ccc5bb0d4b6b9c",
"bytes": 7513
},
{
"path": "pcre2.wasm",
"sha256": "cc7c4b2b0038e137c86d2037e73f2302e56432cb09bc082e4f35bba221dd1889",
"bytes": 339494
}
]
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,277 @@
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see https://opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
Python software and documentation are licensed under the
Python Software Foundation License Version 2.
Starting with Python 3.8.6, examples, recipes, and other code in
the documentation are dual licensed under the PSF License Version 2
and the Zero-Clause BSD license.
Some software incorporated into Python is under different licenses.
The licenses are listed with code falling under that license.
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001 Python Software Foundation; All Rights Reserved"
are retained in Python alone or in any derivative version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
----------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -0,0 +1,8 @@
b0e25a78cffb43f4d92de8b61ccfa1f1f98ecbc22330b54b5251e7b6ba010231 LICENSE.cpython.txt
1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5 LICENSE.pyodide.txt
00267477b691c9f855375a6930f13a3a87fa6580d8fab949bc6cce0fb1d0a821 engine-metadata.json
c963d22858f6bcb8f41586a2142f03905ab370c88ea22a86a2736e95fac2a8f3 pyodide-lock.json
1a9775427ef6e8abaa7db88ece0515422d1886915ae5c9093776410c865dfd8d pyodide.asm.mjs
e7f8fac36f8bf11085309cbc5c829b3ec3057c18bf1d73b05a6741612d63cdbf pyodide.asm.wasm
640ac750bf7cf42ee58b524faa09f803732d2237753cd630d6878edda8a107bf pyodide.mjs
444c770dfd75a32097fc0a7d5c1413fd3140601f49c3a1f2e9af0376fcd124b4 python_stdlib.zip

View File

@@ -0,0 +1,99 @@
{
"schemaVersion": 1,
"engine": "python",
"engineVersion": "CPython 3.14.2 re",
"runtime": "Pyodide 314.0.3",
"status": "production-runtime",
"bridge": {
"abiVersion": 1,
"sourceFile": {
"path": "engines/python/bridge.py",
"sha256": "87cf2690368eab306eb6765d5e516f23c73d987711a2be704576b1c36cf8c6c0",
"bytes": 8509
},
"userValuesAreFunctionArguments": true,
"regexModule": "re",
"supportModules": {
"serialization": "json",
"runtimeIdentity": "sys"
},
"maximumPatternUtf16": 65536,
"maximumSubjectBytes": 16777216,
"maximumReplacementUtf16": 65536,
"maximumOutputBytes": 67108864,
"maximumMatches": 10000,
"maximumCaptureRows": 100000,
"maximumCaptureGroups": 1000,
"maximumNativeExpansionCodePoints": 16777216
},
"source": {
"pyodide": {
"repository": "https://github.com/pyodide/pyodide.git",
"tag": "314.0.3",
"commitSha1": "ac57031be7564f864d061cb37c5c152e59f83ad4",
"license": "MPL-2.0",
"npmPackage": "pyodide@314.0.3",
"npmResolved": "https://registry.npmjs.org/pyodide/-/pyodide-314.0.3.tgz",
"npmIntegrity": "sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ=="
},
"cpython": {
"repository": "https://github.com/python/cpython.git",
"tag": "v3.14.2",
"tagObjectSha1": "a1d0069daf8e85b25a0c3f96abc43182be6d429e",
"commitSha1": "df793163d5821791d4e7caf88885a2c11a107986",
"license": "Python-2.0"
}
},
"toolchain": {
"abiVersion": "2026_0",
"architecture": "wasm32",
"emscriptenVersion": "5.0.3",
"lockPythonVersion": "3.14.0",
"executablePythonVersion": "3.14.2",
"threads": false
},
"packaging": {
"loaderTransformation": "Removed only the upstream sourceMappingURL trailer; executable JavaScript is unchanged.",
"upstreamLoaderSha256": "5cfc46f5dcbaf2a16f26e2363f441873eb424762609cc03db00d6a2ace4d00e5",
"packedLoaderSha256": "640ac750bf7cf42ee58b524faa09f803732d2237753cd630d6878edda8a107bf",
"externalPackagesIncluded": false,
"lockPythonVersionNote": "The upstream lock's 3.14.0 field identifies its CPython 3.14 ABI baseline; the executable runtime self-identifies as 3.14.2."
},
"files": [
{
"path": "LICENSE.cpython.txt",
"sha256": "b0e25a78cffb43f4d92de8b61ccfa1f1f98ecbc22330b54b5251e7b6ba010231",
"bytes": 13804
},
{
"path": "LICENSE.pyodide.txt",
"sha256": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
"bytes": 16725
},
{
"path": "pyodide-lock.json",
"sha256": "c963d22858f6bcb8f41586a2142f03905ab370c88ea22a86a2736e95fac2a8f3",
"bytes": 113804
},
{
"path": "pyodide.asm.mjs",
"sha256": "1a9775427ef6e8abaa7db88ece0515422d1886915ae5c9093776410c865dfd8d",
"bytes": 1249447
},
{
"path": "pyodide.asm.wasm",
"sha256": "e7f8fac36f8bf11085309cbc5c829b3ec3057c18bf1d73b05a6741612d63cdbf",
"bytes": 9596462
},
{
"path": "pyodide.mjs",
"sha256": "640ac750bf7cf42ee58b524faa09f803732d2237753cd630d6878edda8a107bf",
"bytes": 17843
},
{
"path": "python_stdlib.zip",
"sha256": "444c770dfd75a32097fc0a7d5c1413fd3140601f49c3a1f2e9af0376fcd124b4",
"bytes": 2545106
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -3,7 +3,7 @@
"schemaVersion": 1, "schemaVersion": 1,
"id": "de.add-ideas.regex-tools", "id": "de.add-ideas.regex-tools",
"name": "Regex Tools", "name": "Regex Tools",
"version": "0.1.0", "version": "0.3.0",
"description": "Develop, explain, test and apply regular expressions locally in the browser.", "description": "Develop, explain, test and apply regular expressions locally in the browser.",
"entry": "./", "entry": "./",
"icon": "./favicon.svg", "icon": "./favicon.svg",
@@ -36,5 +36,15 @@
"label": "Source", "label": "Source",
"url": "https://git.add-ideas.de/zemion/regex-tools" "url": "https://git.add-ideas.de/zemion/regex-tools"
} }
],
"assets": [
"./engines/pcre2/pcre2.mjs",
"./engines/pcre2/pcre2.wasm",
"./engines/python/pyodide.mjs",
"./engines/python/pyodide.asm.mjs",
"./engines/python/pyodide.asm.wasm",
"./engines/python/pyodide-lock.json",
"./engines/python/python_stdlib.zip",
"./engines/java/java-regex.mjs"
] ]
} }

View File

@@ -1,8 +1,37 @@
import { readFile, readdir } from "node:fs/promises"; import { readFile, readdir } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import {
buildPcre2Pack,
parsePcre2BuildArguments,
} from "./pcre2-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const options = parsePcre2BuildArguments(process.argv.slice(2));
if (options.mode === "help") {
console.log(`Usage:
node scripts/build-engines.mjs
node scripts/build-engines.mjs --pcre2 --source-dir PATH --emcc PATH [--force]
The PCRE2 command is an explicit offline build gate. It verifies exact local
source and compiler identities, writes .engine-build/pcre2, and never
downloads or publishes an engine pack. Install the verified local pack with
npm run engines:pcre2:install.`);
process.exit(0);
}
if (options.mode === "pcre2") {
const result = await buildPcre2Pack(options, root);
console.log(
`Built and verified staged PCRE2 ${result.metadata.engineVersion} pack at ${result.output}.`,
);
console.log(
"The verified pack remains in private staging until the explicit local install command is run.",
);
process.exit(0);
}
const packageJson = JSON.parse( const packageJson = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"), await readFile(path.join(root, "package.json"), "utf8"),
); );
@@ -10,15 +39,18 @@ const engineDirectory = path.join(root, "public", "engines");
const entries = (await readdir(engineDirectory)).sort(); const entries = (await readdir(engineDirectory)).sort();
if ( if (
packageJson.version !== "0.1.0" || typeof packageJson.version !== "string" ||
entries.length !== 1 || entries.length !== 4 ||
entries[0] !== "README.md" entries[0] !== "README.md" ||
entries[1] !== "java" ||
entries[2] !== "pcre2" ||
entries[3] !== "python"
) { ) {
throw new Error( throw new Error(
"The ECMAScript release must contain only the documented native-browser engine placeholder.", "The production build requires the documented Java, PCRE2 and Python runtime packs.",
); );
} }
console.log( console.log(
"Regex Tools 0.1.0 uses the native browser RegExp engine; no external engine pack needs building.", "The checked-in Java, PCRE2 and Python runtime packs are present; no release-time download or engine compilation is needed.",
); );

View File

@@ -7,14 +7,23 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const sourcePath = join(root, "src", "toolbox", "manifest.source.json"); const sourcePath = join(root, "src", "toolbox", "manifest.source.json");
const outputPath = join(root, "public", "toolbox-app.json"); const outputPath = join(root, "public", "toolbox-app.json");
const packagePath = join(root, "package.json"); const packagePath = join(root, "package.json");
const applicationVersionPath = join(root, "src", "version.ts");
const checkOnly = process.argv.includes("--check"); const checkOnly = process.argv.includes("--check");
const source = JSON.parse(await readFile(sourcePath, "utf8")); const source = JSON.parse(await readFile(sourcePath, "utf8"));
const packageJson = JSON.parse(await readFile(packagePath, "utf8")); const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
const applicationVersionSource = await readFile(applicationVersionPath, "utf8");
const applicationVersion =
/^export const APPLICATION_VERSION = "([^"]+)";$/mu.exec(
applicationVersionSource,
)?.[1];
if (source.version !== packageJson.version) { if (
source.version !== packageJson.version ||
applicationVersion !== packageJson.version
) {
throw new Error( throw new Error(
`Manifest version ${source.version} differs from package version ${packageJson.version}`, `Version drift: manifest ${source.version}, application ${String(applicationVersion)}, package ${packageJson.version}`,
); );
} }

View File

@@ -0,0 +1,16 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { installJavaPack } from "./java-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 1) {
throw new Error(
"Usage: node scripts/install-java-engine.mjs [.engine-build/java]",
);
}
const source = path.resolve(root, arguments_[0] ?? ".engine-build/java");
const result = await installJavaPack(source, root);
console.log(
`Installed verified ${result.metadata.semanticIdentity} assets at ${result.output}.`,
);

View File

@@ -0,0 +1,16 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { installPcre2Pack } from "./pcre2-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 1) {
throw new Error(
"Usage: node scripts/install-pcre2-engine.mjs [.engine-build/pcre2]",
);
}
const source = path.resolve(root, arguments_[0] ?? ".engine-build/pcre2");
const result = await installPcre2Pack(source, root);
console.log(
`Installed verified PCRE2 ${result.metadata.engineVersion} runtime assets at ${result.output}.`,
);

View File

@@ -0,0 +1,16 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { installPythonPack } from "./python-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 1) {
throw new Error(
"Usage: node scripts/install-python-engine.mjs [.engine-build/python]",
);
}
const source = path.resolve(root, arguments_[0] ?? ".engine-build/python");
const result = await installPythonPack(source, root);
console.log(
`Installed verified ${result.metadata.runtime} / ${result.metadata.engineVersion} assets at ${result.output}.`,
);

View File

@@ -0,0 +1,603 @@
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import {
access,
constants as fsConstants,
copyFile,
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rename,
rm,
stat,
writeFile,
} from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
export const JAVA_LOCK = Object.freeze({
bridgeAbi: 1,
engineIdentity: "TeaVM java.util.regex 0.15.0",
engineVersion: "0.15.0",
sourceDateEpoch: 1_780_759_630,
maximumPatternUtf16: 65_536,
maximumSubjectBytes: 16_777_216,
maximumReplacementUtf16: 65_536,
maximumMatches: 10_000,
maximumCaptureRows: 100_000,
maximumCaptureGroups: 1_000,
maximumOutputBytes: 67_108_864,
source: Object.freeze({
repository: "https://github.com/konsoletyper/teavm.git",
tag: "0.15.0",
commit: "ee91b03e616c4b45401cd11fb0cd7eb0daf6649b",
tree: "1cdc5c332d5809828c8952f96d73f9e7504a0cec",
license: "Apache-2.0",
licenseSha256:
"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
noticeSha256:
"6c2b3372a6beee7fbaad482248ac0f5ad2b099d92ff2a528c04261cc9c0636f4",
}),
toolchain: Object.freeze({
mavenVersion: "3.9.15",
javaVersion: "25.0.3",
artifacts: Object.freeze([
Object.freeze({
coordinate: "org.teavm:teavm-classlib:0.15.0",
relativePath:
"org/teavm/teavm-classlib/0.15.0/teavm-classlib-0.15.0.jar",
sha256:
"7d805be4670a892a34c89d9d750a8afd65e2f8201b7b3a10343634ea19d1c410",
}),
Object.freeze({
coordinate: "org.teavm:teavm-jso:0.15.0",
relativePath: "org/teavm/teavm-jso/0.15.0/teavm-jso-0.15.0.jar",
sha256:
"58b337b93924106bf819c45890c7c231a0c740145afe77795f1e0f099f621d71",
}),
Object.freeze({
coordinate: "org.teavm:teavm-maven-plugin:0.15.0",
relativePath:
"org/teavm/teavm-maven-plugin/0.15.0/teavm-maven-plugin-0.15.0.jar",
sha256:
"1133e16e45f1fd3cb93129fd96d142e1f5ef3608891d83b2273442d5446a3879",
}),
]),
}),
});
const PACK_FILES = Object.freeze([
"LICENSE.txt",
"NOTICE.txt",
"SHA256SUMS",
"engine-metadata.json",
"java-regex.mjs",
]);
const CHECKSUM_FILES = Object.freeze([
"LICENSE.txt",
"NOTICE.txt",
"engine-metadata.json",
"java-regex.mjs",
]);
const BRIDGE_SOURCE_FILES = Object.freeze([
"engines/java/pom.xml",
"engines/java/README.md",
"engines/java/LICENSE-TeaVM.txt",
"engines/java/NOTICE-TeaVM.txt",
"engines/java/src/main/java/de/addideas/regextools/javaengine/RegexBridge.java",
]);
const EXPORTED_FUNCTIONS = Object.freeze([
"bridgeAbiVersion",
"engineIdentity",
"execute",
"replace",
"selfTest",
]);
function sha256(data) {
return createHash("sha256").update(data).digest("hex");
}
async function sha256File(file) {
return sha256(await readFile(file));
}
async function assertRealDirectory(candidate, label) {
const details = await lstat(candidate).catch(() => null);
if (!details?.isDirectory() || details.isSymbolicLink()) {
throw new Error(`${label} must be a real directory: ${candidate}`);
}
return realpath(candidate);
}
async function assertRegularFile(candidate, label) {
const details = await lstat(candidate).catch(() => null);
if (!details?.isFile() || details.isSymbolicLink()) {
throw new Error(
`${label} must be a regular, non-symlink file: ${candidate}`,
);
}
return realpath(candidate);
}
async function resolveExecutable(name, environment = process.env) {
for (const directory of (environment.PATH ?? "").split(path.delimiter)) {
if (!directory) continue;
const candidate = path.join(directory, name);
try {
await access(candidate, fsConstants.X_OK);
if ((await stat(candidate)).isFile()) return realpath(candidate);
} catch {
// Continue through PATH.
}
}
throw new Error(`${name} is required on PATH for the TeaVM Java build.`);
}
function commandLabel(command, arguments_) {
return [command, ...arguments_]
.map((part) =>
/^[A-Za-z0-9_./:=,+-]+$/u.test(part) ? part : JSON.stringify(part),
)
.join(" ");
}
async function run(command, arguments_, options = {}) {
const child = spawn(command, arguments_, {
cwd: options.cwd,
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stdout = [];
const stderr = [];
child.stdout.on("data", (chunk) => stdout.push(chunk));
child.stderr.on("data", (chunk) => stderr.push(chunk));
const result = await new Promise((resolve, reject) => {
child.once("error", reject);
child.once("close", (code, signal) => resolve({ code, signal }));
});
const output = Buffer.concat(stdout).toString("utf8");
const errorOutput = Buffer.concat(stderr).toString("utf8");
if (result.code !== 0) {
const detail = [output, errorOutput].filter(Boolean).join("\n").trim();
throw new Error(
`${commandLabel(command, arguments_)} failed with ${
result.signal ? `signal ${result.signal}` : `exit code ${result.code}`
}${detail ? `:\n${detail}` : "."}`,
);
}
return { stdout: output, stderr: errorOutput };
}
async function verifyToolchain() {
const maven = await resolveExecutable("mvn");
const java = await resolveExecutable("java");
const mavenVersion = await run(maven, ["--version"]);
const javaVersion = await run(java, ["-version"]);
if (
!mavenVersion.stdout.startsWith(
`Apache Maven ${JAVA_LOCK.toolchain.mavenVersion} `,
)
) {
throw new Error(
`The Java pack requires Maven ${JAVA_LOCK.toolchain.mavenVersion}.`,
);
}
const javaText = `${javaVersion.stdout}\n${javaVersion.stderr}`;
if (
!javaText.includes(`openjdk version "${JAVA_LOCK.toolchain.javaVersion}`)
) {
throw new Error(
`The Java pack requires OpenJDK ${JAVA_LOCK.toolchain.javaVersion}.`,
);
}
const repository = path.join(homedir(), ".m2", "repository");
for (const artifact of JAVA_LOCK.toolchain.artifacts) {
const file = await assertRegularFile(
path.join(repository, artifact.relativePath),
artifact.coordinate,
);
const actual = await sha256File(file);
if (actual !== artifact.sha256) {
throw new Error(
`${artifact.coordinate} SHA-256 mismatch: expected ${artifact.sha256}, got ${actual}.`,
);
}
}
return { maven };
}
function cleanBuildEnvironment() {
const environment = { ...process.env };
for (const variable of [
"CLASSPATH",
"JAVA_TOOL_OPTIONS",
"MAVEN_ARGS",
"MAVEN_OPTS",
"_JAVA_OPTIONS",
]) {
delete environment[variable];
}
environment.LANG = "C";
environment.LC_ALL = "C";
environment.SOURCE_DATE_EPOCH = String(JAVA_LOCK.sourceDateEpoch);
environment.TZ = "UTC";
return environment;
}
async function sourceFiles(root) {
return Promise.all(
BRIDGE_SOURCE_FILES.map(async (relativePath) => {
const file = await assertRegularFile(
path.join(root, relativePath),
`Java bridge source ${relativePath}`,
);
const contents = await readFile(file);
return {
path: relativePath,
sha256: sha256(contents),
bytes: contents.byteLength,
};
}),
);
}
async function fileMetadata(pack, name) {
const contents = await readFile(path.join(pack, name));
return { path: name, sha256: sha256(contents), bytes: contents.byteLength };
}
async function expectedMetadata(root, pack) {
return {
schemaVersion: 1,
engine: "java",
engineName: "TeaVM java.util.regex",
engineVersion: JAVA_LOCK.engineVersion,
semanticIdentity:
"TeaVM 0.15.0 classlib java.util.regex; Apache Harmony-derived; not OpenJDK",
status: "production-compatibility-runtime",
bridge: {
abiVersion: JAVA_LOCK.bridgeAbi,
maximumPatternUtf16: JAVA_LOCK.maximumPatternUtf16,
maximumSubjectBytes: JAVA_LOCK.maximumSubjectBytes,
maximumReplacementUtf16: JAVA_LOCK.maximumReplacementUtf16,
maximumMatches: JAVA_LOCK.maximumMatches,
maximumCaptureRows: JAVA_LOCK.maximumCaptureRows,
maximumCaptureGroups: JAVA_LOCK.maximumCaptureGroups,
maximumOutputBytes: JAVA_LOCK.maximumOutputBytes,
offsetUnit: "utf16",
exports: [...EXPORTED_FUNCTIONS],
sourceFiles: await sourceFiles(root),
},
source: {
repository: JAVA_LOCK.source.repository,
tag: JAVA_LOCK.source.tag,
commitSha1: JAVA_LOCK.source.commit,
treeSha1: JAVA_LOCK.source.tree,
license: JAVA_LOCK.source.license,
upstreamLicenseSha256: JAVA_LOCK.source.licenseSha256,
upstreamNoticeSha256: JAVA_LOCK.source.noticeSha256,
},
toolchain: {
mavenVersion: JAVA_LOCK.toolchain.mavenVersion,
javaVersion: JAVA_LOCK.toolchain.javaVersion,
teaVmArtifacts: JAVA_LOCK.toolchain.artifacts.map(
({ coordinate, sha256: digest }) => ({
coordinate,
sha256: digest,
}),
),
},
configuration: {
target: "JavaScript ES2015 module",
minified: true,
optimizationLevel: "ADVANCED",
sourceMaps: false,
filesystem: false,
supportedApplicationFlags: ["g", "i", "d", "m", "s", "u", "x", "U"],
unixLines: "d maps to TeaVM Pattern.UNIX_LINES.",
unicodeCharacterClassCompatibility:
"TeaVM 0.15.0 lacks OpenJDK UNICODE_CHARACTER_CLASS; U implies Unicode case folding only.",
replacement:
"TeaVM 0.15.0 Matcher appendReplacement/appendTail semantics: only single-digit $n references; ${name} is rejected; not OpenJDK Matcher parity.",
},
sourceDateEpoch: JAVA_LOCK.sourceDateEpoch,
files: await Promise.all(
["LICENSE.txt", "NOTICE.txt", "java-regex.mjs"].map((name) =>
fileMetadata(pack, name),
),
),
};
}
async function expectedChecksums(pack) {
const lines = await Promise.all(
CHECKSUM_FILES.map(
async (name) => `${await sha256File(path.join(pack, name))} ${name}`,
),
);
return `${lines.join("\n")}\n`;
}
function equalJson(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
function readField(payload, state) {
const colon = payload.indexOf(":", state.cursor);
if (colon < 0) throw new Error("Malformed Java smoke-test payload.");
const length = Number.parseInt(payload.slice(state.cursor, colon), 10);
if (!Number.isSafeInteger(length) || length < 0) {
throw new Error("Malformed Java smoke-test field length.");
}
const start = colon + 1;
const end = start + length;
if (end > payload.length) {
throw new Error("Truncated Java smoke-test payload.");
}
state.cursor = end;
return payload.slice(start, end);
}
async function smokeJavaPack(pack) {
const moduleFile = path.join(pack, "java-regex.mjs");
const imported = await import(
`${pathToFileURL(moduleFile).href}?verify=${await sha256File(moduleFile)}`
);
for (const name of EXPORTED_FUNCTIONS) {
if (typeof imported[name] !== "function") {
throw new Error(`The Java module is missing export ${name}.`);
}
}
if (
imported.bridgeAbiVersion() !== JAVA_LOCK.bridgeAbi ||
imported.engineIdentity() !== JAVA_LOCK.engineIdentity ||
imported.selfTest() !== 0
) {
throw new Error("The TeaVM Java bridge identity or self-test failed.");
}
const execution = imported.execute("(?<=a)(b)\\1", "abb", 0, true, 10, 10);
const executionState = { cursor: 0 };
const executionFields = Array.from({ length: 9 }, () =>
readField(execution, executionState),
);
if (
executionState.cursor !== execution.length ||
JSON.stringify(executionFields) !==
JSON.stringify(["1", "ok", "1", "0", "1", "1", "3", "1", "2"])
) {
throw new Error("The TeaVM Java matching smoke test failed.");
}
const replacement = imported.replace("(a)", "a a", "<$1>", 0, true, 10, 10);
const replacementState = { cursor: 0 };
const replacementFields = [];
while (replacementState.cursor < replacement.length) {
replacementFields.push(readField(replacement, replacementState));
}
if (
replacementFields.at(-2) !== "0" ||
replacementFields.at(-1) !== "<a> <a>"
) {
throw new Error("The TeaVM Java replacement smoke test failed.");
}
}
export async function verifyJavaPack(packDirectory, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const pack = await assertRealDirectory(packDirectory, "Java engine pack");
const entries = (await readdir(pack, { withFileTypes: true })).sort(
(left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
);
if (
entries.length !== PACK_FILES.length ||
entries.some(
(entry, index) =>
entry.name !== PACK_FILES[index] ||
!entry.isFile() ||
entry.isSymbolicLink(),
)
) {
throw new Error(
`The staged Java pack must contain only: ${PACK_FILES.join(", ")}.`,
);
}
if (
(await sha256File(path.join(pack, "LICENSE.txt"))) !==
JAVA_LOCK.source.licenseSha256
) {
throw new Error("The staged TeaVM licence does not match tag 0.15.0.");
}
const moduleText = await readFile(path.join(pack, "java-regex.mjs"), "utf8");
if (
moduleText.includes("sourceMappingURL") ||
moduleText.includes("/mnt/") ||
moduleText.includes("/home/") ||
!moduleText.includes("as bridgeAbiVersion") ||
!moduleText.includes("as engineIdentity") ||
!moduleText.includes("as execute") ||
!moduleText.includes("as replace") ||
!moduleText.includes("as selfTest")
) {
throw new Error(
"java-regex.mjs leaks a build path, has a source map, or lacks bridge exports.",
);
}
let metadata;
try {
metadata = JSON.parse(
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
);
} catch (error) {
throw new Error("Java engine-metadata.json is not valid JSON.", {
cause: error,
});
}
const expected = await expectedMetadata(repositoryRoot, pack);
if (!equalJson(metadata, expected)) {
throw new Error(
"The staged Java metadata does not match the pinned build contract.",
);
}
if (
(await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !==
(await expectedChecksums(pack))
) {
throw new Error("The staged Java SHA256SUMS file is incorrect.");
}
await smokeJavaPack(pack);
return metadata;
}
async function replaceDirectory(stage, target, label) {
const targetDetails = await lstat(target).catch(() => null);
if (
targetDetails &&
(!targetDetails.isDirectory() || targetDetails.isSymbolicLink())
) {
throw new Error(`${label} must be a real directory.`);
}
if (!targetDetails) {
await rename(stage, target);
return;
}
const backup = `${target}.replaced-${process.pid}`;
await rename(target, backup);
try {
await rename(stage, target);
} catch (error) {
await rename(backup, target);
throw error;
}
await rm(backup, { recursive: true });
}
export async function buildJavaPack(root, outputDirectory) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const engine = await assertRealDirectory(
path.join(repositoryRoot, "engines", "java"),
"Java engine source",
);
const { maven } = await verifyToolchain();
const environment = cleanBuildEnvironment();
const buildArguments = [
"--batch-mode",
"--no-transfer-progress",
"--offline",
"clean",
"package",
];
await run(maven, buildArguments, { cwd: engine, env: environment });
const generated = await assertRegularFile(
path.join(engine, "target", "generated", "java-regex.mjs"),
"generated TeaVM module",
);
const firstBuild = await readFile(generated);
await run(maven, buildArguments, { cwd: engine, env: environment });
const secondBuild = await readFile(generated);
if (sha256(firstBuild) !== sha256(secondBuild)) {
throw new Error(
"Two clean TeaVM builds produced different Java module hashes.",
);
}
const output = path.resolve(
repositoryRoot,
outputDirectory ?? path.join(".engine-build", "java"),
);
await mkdir(path.dirname(output), { recursive: true });
const stage = await mkdtemp(path.join(path.dirname(output), ".java-build-"));
try {
await copyFile(
path.join(engine, "LICENSE-TeaVM.txt"),
path.join(stage, "LICENSE.txt"),
fsConstants.COPYFILE_EXCL,
);
await copyFile(
path.join(engine, "NOTICE-TeaVM.txt"),
path.join(stage, "NOTICE.txt"),
fsConstants.COPYFILE_EXCL,
);
await writeFile(path.join(stage, "java-regex.mjs"), secondBuild, {
flag: "wx",
});
const metadata = await expectedMetadata(repositoryRoot, stage);
await writeFile(
path.join(stage, "engine-metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
{ encoding: "utf8", flag: "wx" },
);
await writeFile(
path.join(stage, "SHA256SUMS"),
await expectedChecksums(stage),
{ encoding: "utf8", flag: "wx" },
);
await verifyJavaPack(stage, repositoryRoot);
await replaceDirectory(stage, output, "Java build output");
return {
output,
metadata,
moduleSha256: sha256(secondBuild),
moduleBytes: secondBuild.byteLength,
};
} finally {
await rm(stage, { recursive: true, force: true });
}
}
export async function installJavaPack(packDirectory, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const source = await assertRealDirectory(packDirectory, "verified Java pack");
const metadata = await verifyJavaPack(source, repositoryRoot);
const engineRoot = await assertRealDirectory(
path.join(repositoryRoot, "public", "engines"),
"public engine directory",
);
const target = path.join(engineRoot, "java");
const stage = await mkdtemp(path.join(engineRoot, ".java-install-"));
try {
for (const name of PACK_FILES) {
await copyFile(
path.join(source, name),
path.join(stage, name),
fsConstants.COPYFILE_EXCL,
);
}
await verifyJavaPack(stage, repositoryRoot);
await replaceDirectory(stage, target, "public/engines/java");
return { output: target, metadata };
} finally {
await rm(stage, { recursive: true, force: true });
}
}
const invokedFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
if (invokedFile === fileURLToPath(import.meta.url)) {
if (process.argv.length > 3) {
throw new Error(
"Usage: node scripts/java-engine-pack.mjs [.engine-build/java]",
);
}
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const result = await buildJavaPack(root, process.argv[2]);
console.log(
`Built deterministic ${result.metadata.semanticIdentity} module: ${result.moduleSha256} (${result.moduleBytes.toLocaleString()} bytes).`,
);
}

View File

@@ -18,6 +18,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const zipEpoch = new Date(1980, 0, 1, 0, 0, 0); const zipEpoch = new Date(1980, 0, 1, 0, 0, 0);
const gplVersion3TextSha256 = const gplVersion3TextSha256 =
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0"; "fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
const expectedApplicationVersion = "0.3.0";
const requiredRootFiles = [ const requiredRootFiles = [
"LICENSE", "LICENSE",
"README.md", "README.md",
@@ -332,13 +333,27 @@ async function collectReleaseEntries() {
await readFile(path.join(root, "package.json"), "utf8"), await readFile(path.join(root, "package.json"), "utf8"),
); );
if ( if (
packageJson.version !== "0.1.0" || packageJson.version !== expectedApplicationVersion ||
packageJson.license !== "GPL-3.0-or-later" || packageJson.license !== "GPL-3.0-or-later" ||
packageJson.repository?.url !== packageJson.repository?.url !==
"git+https://git.add-ideas.de/zemion/regex-tools.git" "git+https://git.add-ideas.de/zemion/regex-tools.git"
) { ) {
throw new Error("Package version, licence or source identity drifted."); throw new Error("Package version, licence or source identity drifted.");
} }
const sourceIdentity = await readFile(path.join(root, "SOURCE.md"), "utf8");
if (
!sourceIdentity.includes(
`- Release version: \`${expectedApplicationVersion}\``,
) ||
!sourceIdentity.includes(
`- Release tag: \`v${expectedApplicationVersion}\``,
) ||
!sourceIdentity.includes(
"- Repository: <https://git.add-ideas.de/zemion/regex-tools>",
)
) {
throw new Error("SOURCE.md release identity drifted.");
}
const licence = await readFile(path.join(root, "LICENSE"), "utf8"); const licence = await readFile(path.join(root, "LICENSE"), "utf8");
if ( if (
!licence.includes("GNU GENERAL PUBLIC LICENSE") || !licence.includes("GNU GENERAL PUBLIC LICENSE") ||
@@ -371,6 +386,28 @@ async function collectReleaseEntries() {
"CHANGELOG.md", "CHANGELOG.md",
"SOURCE.md", "SOURCE.md",
"THIRD_PARTY_NOTICES.md", "THIRD_PARTY_NOTICES.md",
"engines/README.md",
"engines/java/LICENSE.txt",
"engines/java/NOTICE.txt",
"engines/java/SHA256SUMS",
"engines/java/engine-metadata.json",
"engines/java/java-regex.mjs",
"engines/pcre2/LICENSE.txt",
"engines/pcre2/SHA256SUMS",
"engines/pcre2/engine-metadata.json",
"engines/pcre2/pcre2.mjs",
"engines/pcre2/pcre2.wasm",
"engines/python/LICENSE.cpython.txt",
"engines/python/LICENSE.pyodide.txt",
"engines/python/SHA256SUMS",
"engines/python/engine-metadata.json",
"engines/python/pyodide-lock.json",
"engines/python/pyodide.asm.mjs",
"engines/python/pyodide.asm.wasm",
"engines/python/pyodide.mjs",
"engines/python/python_stdlib.zip",
"LICENSES/Emscripten-MIT.txt",
"LICENSES/PCRE2.txt",
"LICENSES/build/rolldown-1.1.5/LICENSE", "LICENSES/build/rolldown-1.1.5/LICENSE",
"LICENSES/build/vite-8.1.5/LICENSE.md", "LICENSES/build/vite-8.1.5/LICENSE.md",
]) { ]) {

View File

@@ -0,0 +1,110 @@
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import {
generatePcre2C,
type Pcre2CGenerationRequest,
} from "../src/regex/codegen/pcre2-c";
function request(operation: "match" | "replace"): Pcre2CGenerationRequest {
return {
flavour: "pcre2",
flavourVersion: "PCRE2 10.47 8-bit WebAssembly",
operation,
pattern: String.raw`(?<word>\p{L}+)-(\d+)`,
flags: ["g"],
options: {
matchLimit: 1_000_000,
depthLimit: 1_000,
heapLimitKib: 32_768,
},
subject: "x Grüße-42 y alpha-7",
scanAll: true,
...(operation === "replace" ? { replacement: "$2:$<word>" } : {}),
maximumMatches: 100,
maximumCaptureRows: 1_000,
maximumOutputBytes: 4_096,
};
}
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
const exactToolchainPrefix = process.env.PCRE2_CODEGEN_PREFIX;
const compileWithExactPcre2 = exactToolchainPrefix ? it : it.skip;
describe("PCRE2 C code-generation golden", () => {
it("retains a reviewed deterministic source identity", () => {
expect(sha256(generatePcre2C(request("match")).source)).toBe(
"22b91d578a449b0ed5c30e6eadad30f19a2bcb26ac9db355c8ec7a69328675da",
);
expect(sha256(generatePcre2C(request("replace")).source)).toBe(
"ab90a0d9831ea7b7145aa0072ac352b42baf9a795319d81f531b0f341b04a636",
);
});
compileWithExactPcre2(
"compiles and executes against the explicitly supplied PCRE2 10.47 C toolchain",
async () => {
if (!exactToolchainPrefix) return;
const header = path.join(exactToolchainPrefix, "include", "pcre2.h");
const library =
process.env.PCRE2_CODEGEN_LIBRARY ??
path.join(exactToolchainPrefix, "lib64", "libpcre2-8.a");
await expect(readFile(header)).resolves.toBeInstanceOf(Buffer);
await expect(readFile(library)).resolves.toBeInstanceOf(Buffer);
const directory = await mkdtemp(
path.join(tmpdir(), "regex-tools-codegen-"),
);
try {
for (const operation of ["match", "replace"] as const) {
const generated = generatePcre2C(request(operation));
const source = path.join(directory, generated.fileName);
const executable = path.join(directory, `pcre2-${operation}`);
await writeFile(source, generated.source);
const compiled = spawnSync(
process.env.CC ?? "cc",
[
"-std=c17",
"-Wall",
"-Wextra",
"-Werror",
`-I${path.join(exactToolchainPrefix, "include")}`,
source,
library,
"-o",
executable,
],
{ encoding: "utf8" },
);
expect(
`${compiled.stdout}${compiled.stderr}`,
`${operation} fixture did not compile`,
).toBe("");
expect(compiled.status).toBe(0);
const executed = spawnSync(executable, [], { encoding: "utf8" });
expect(
executed.status,
`${operation} fixture failed: ${executed.stdout}${executed.stderr}`,
).toBe(0);
if (operation === "match") {
expect(executed.stdout).toContain("match 1: UTF-8 bytes 2..12");
expect(executed.stdout).toContain("group 1 / word");
expect(executed.stdout).toContain(
"completed: 2 match(es), 4 capture row(s)",
);
} else {
expect(executed.stdout).toBe("x 42:Grüße y 7:alpha");
expect(executed.stderr).toContain("completed: 2 substitution(s)");
}
}
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
// @vitest-environment node
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
PCRE2_LOCK,
parsePcre2BuildArguments,
verifyPcre2Pack,
} from "./pcre2-engine-pack.mjs";
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
describe("PCRE2 engine-pack gate", () => {
it("keeps the default build on the checked-in production-assets path", () => {
expect(parsePcre2BuildArguments([])).toEqual({ mode: "ecmascript" });
});
it("requires explicit offline source and compiler paths", () => {
expect(() => parsePcre2BuildArguments(["--pcre2"])).toThrow(
"requires both --source-dir and --emcc",
);
expect(() =>
parsePcre2BuildArguments([
"--source-dir",
"/source",
"--emcc",
"/compiler",
]),
).toThrow("explicit --pcre2 gate");
});
it("parses the exact staged-build contract", () => {
expect(
parsePcre2BuildArguments([
"--pcre2",
"--source-dir",
"/source",
"--emcc",
"/compiler",
"--force",
]),
).toEqual({
mode: "pcre2",
sourceDirectory: "/source",
emccFile: "/compiler",
force: true,
});
});
it.each([
["--unknown"],
["--pcre2", "--pcre2"],
["--pcre2", "--source-dir"],
["--pcre2", "--force", "--force"],
])("rejects ambiguous arguments %j", (...arguments_) => {
expect(() => parsePcre2BuildArguments(arguments_)).toThrow();
});
it("pins source, signed-tag object and compiler identities", () => {
expect(PCRE2_LOCK.source.tag).toBe("pcre2-10.47");
expect(PCRE2_LOCK.source.tagObject).toHaveLength(40);
expect(PCRE2_LOCK.source.commit).toHaveLength(40);
expect(PCRE2_LOCK.source.tree).toHaveLength(40);
expect(PCRE2_LOCK.emscripten.version).toBe("6.0.4");
expect(PCRE2_LOCK.emscripten.emccSha256).toHaveLength(64);
expect(PCRE2_LOCK.bridgeAbi).toBe(3);
expect(PCRE2_LOCK.maximumSubjectBytes).toBe(16 * 1024 * 1024);
expect(PCRE2_LOCK.maximumMatches).toBe(10_000);
});
it("rejects an incomplete staged pack before attempting to load it", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "regex-pcre2-test-"));
temporaryDirectories.push(directory);
await expect(verifyPcre2Pack(directory, process.cwd())).rejects.toThrow(
"must contain only",
);
});
});

View File

@@ -0,0 +1,572 @@
import { createHash } from "node:crypto";
import {
constants as fsConstants,
copyFile,
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rename,
rm,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
export const PYTHON_ENGINE_LOCK = Object.freeze({
bridgeAbi: 1,
pythonVersion: "3.14.2",
pyodideVersion: "314.0.3",
maximumPatternUtf16: 65_536,
maximumSubjectBytes: 16_777_216,
maximumReplacementUtf16: 65_536,
maximumOutputBytes: 67_108_864,
maximumMatches: 10_000,
maximumCaptureRows: 100_000,
maximumCaptureGroups: 1_000,
maximumNativeExpansionCodePoints: 16_777_216,
npm: Object.freeze({
package: "pyodide@314.0.3",
resolved: "https://registry.npmjs.org/pyodide/-/pyodide-314.0.3.tgz",
integrity:
"sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==",
packageJsonSha256:
"0393eb01a0707cddc251f1a66090998773fe1aa3cfde66eedc96e83e43861845",
}),
pyodide: Object.freeze({
repository: "https://github.com/pyodide/pyodide.git",
tag: "314.0.3",
commit: "ac57031be7564f864d061cb37c5c152e59f83ad4",
license: "MPL-2.0",
licenseUrl:
"https://raw.githubusercontent.com/pyodide/pyodide/314.0.3/LICENSE",
licenseSha256:
"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
}),
cpython: Object.freeze({
repository: "https://github.com/python/cpython.git",
tag: "v3.14.2",
tagObject: "a1d0069daf8e85b25a0c3f96abc43182be6d429e",
commit: "df793163d5821791d4e7caf88885a2c11a107986",
license: "Python-2.0",
licenseUrl:
"https://raw.githubusercontent.com/python/cpython/v3.14.2/LICENSE",
licenseSha256:
"b0e25a78cffb43f4d92de8b61ccfa1f1f98ecbc22330b54b5251e7b6ba010231",
}),
emscriptenVersion: "5.0.3",
abiVersion: "2026_0",
lockPythonVersion: "3.14.0",
assets: Object.freeze({
"pyodide-lock.json":
"c963d22858f6bcb8f41586a2142f03905ab370c88ea22a86a2736e95fac2a8f3",
"pyodide.asm.mjs":
"1a9775427ef6e8abaa7db88ece0515422d1886915ae5c9093776410c865dfd8d",
"pyodide.asm.wasm":
"e7f8fac36f8bf11085309cbc5c829b3ec3057c18bf1d73b05a6741612d63cdbf",
"pyodide.mjs":
"5cfc46f5dcbaf2a16f26e2363f441873eb424762609cc03db00d6a2ace4d00e5",
"python_stdlib.zip":
"444c770dfd75a32097fc0a7d5c1413fd3140601f49c3a1f2e9af0376fcd124b4",
}),
strippedLoaderSha256:
"640ac750bf7cf42ee58b524faa09f803732d2237753cd630d6878edda8a107bf",
});
const PACK_FILES = Object.freeze(
[
"LICENSE.cpython.txt",
"LICENSE.pyodide.txt",
"SHA256SUMS",
"engine-metadata.json",
...Object.keys(PYTHON_ENGINE_LOCK.assets),
].sort(),
);
const CHECKSUM_FILES = Object.freeze(
PACK_FILES.filter((name) => name !== "SHA256SUMS"),
);
function sha256(data) {
return createHash("sha256").update(data).digest("hex");
}
async function sha256File(file) {
return sha256(await readFile(file));
}
async function assertRealDirectory(candidate, label) {
const details = await lstat(candidate).catch(() => null);
if (!details?.isDirectory() || details.isSymbolicLink()) {
throw new Error(`${label} must be a real directory: ${candidate}`);
}
return realpath(candidate);
}
async function assertRegularFile(candidate, label) {
const details = await lstat(candidate).catch(() => null);
if (!details?.isFile() || details.isSymbolicLink()) {
throw new Error(
`${label} must be a regular, non-symlink file: ${candidate}`,
);
}
return realpath(candidate);
}
async function assertHash(file, expected, label) {
const actual = await sha256File(file);
if (actual !== expected) {
throw new Error(
`${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`,
);
}
}
function withoutSourceMapReference(source) {
const result = source.replace(
/\n\/\/# sourceMappingURL=pyodide\.mjs\.map\s*$/u,
"\n",
);
if (result === source) {
throw new Error(
"The pinned Pyodide loader no longer has the expected source-map trailer.",
);
}
return result;
}
async function verifyPyodideSource(sourceDirectory) {
const source = await assertRealDirectory(
sourceDirectory,
"Pyodide npm package",
);
for (const [name, expected] of Object.entries(PYTHON_ENGINE_LOCK.assets)) {
await assertRegularFile(path.join(source, name), `Pyodide ${name}`);
await assertHash(path.join(source, name), expected, `Pyodide ${name}`);
}
const packageFile = path.join(source, "package.json");
await assertHash(
packageFile,
PYTHON_ENGINE_LOCK.npm.packageJsonSha256,
"Pyodide package.json",
);
const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
if (
packageJson.name !== "pyodide" ||
packageJson.version !== PYTHON_ENGINE_LOCK.pyodideVersion ||
packageJson.license !== PYTHON_ENGINE_LOCK.pyodide.license ||
packageJson.repository?.url !== "git+https://github.com/pyodide/pyodide.git"
) {
throw new Error("The Pyodide npm package identity is not pinned.");
}
const lock = JSON.parse(
await readFile(path.join(source, "pyodide-lock.json"), "utf8"),
);
if (
lock.info?.abi_version !== PYTHON_ENGINE_LOCK.abiVersion ||
lock.info?.arch !== "wasm32" ||
lock.info?.python !== PYTHON_ENGINE_LOCK.lockPythonVersion ||
lock.info?.platform !==
`emscripten_${PYTHON_ENGINE_LOCK.emscriptenVersion.replaceAll(".", "_")}`
) {
throw new Error("The Pyodide lock-file platform identity is not pinned.");
}
return source;
}
async function fetchPinned(url, expectedHash, label) {
const response = await fetch(url, {
redirect: "follow",
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error(`${label} download failed with HTTP ${response.status}.`);
}
const value = Buffer.from(await response.arrayBuffer());
const actual = sha256(value);
if (actual !== expectedHash) {
throw new Error(
`${label} SHA-256 mismatch: expected ${expectedHash}, got ${actual}.`,
);
}
return value;
}
async function fileRecord(directory, name) {
const file = path.join(directory, name);
const value = await readFile(file);
return {
path: name,
sha256: sha256(value),
bytes: value.byteLength,
};
}
async function expectedMetadata(root, pack) {
const bridgePath = path.join(root, "engines", "python", "bridge.py");
const bridge = await readFile(bridgePath);
const files = [];
for (const name of CHECKSUM_FILES) {
if (name !== "engine-metadata.json") {
files.push(await fileRecord(pack, name));
}
}
return {
schemaVersion: 1,
engine: "python",
engineVersion: `CPython ${PYTHON_ENGINE_LOCK.pythonVersion} re`,
runtime: `Pyodide ${PYTHON_ENGINE_LOCK.pyodideVersion}`,
status: "production-runtime",
bridge: {
abiVersion: PYTHON_ENGINE_LOCK.bridgeAbi,
sourceFile: {
path: "engines/python/bridge.py",
sha256: sha256(bridge),
bytes: bridge.byteLength,
},
userValuesAreFunctionArguments: true,
regexModule: "re",
supportModules: {
serialization: "json",
runtimeIdentity: "sys",
},
maximumPatternUtf16: PYTHON_ENGINE_LOCK.maximumPatternUtf16,
maximumSubjectBytes: PYTHON_ENGINE_LOCK.maximumSubjectBytes,
maximumReplacementUtf16: PYTHON_ENGINE_LOCK.maximumReplacementUtf16,
maximumOutputBytes: PYTHON_ENGINE_LOCK.maximumOutputBytes,
maximumMatches: PYTHON_ENGINE_LOCK.maximumMatches,
maximumCaptureRows: PYTHON_ENGINE_LOCK.maximumCaptureRows,
maximumCaptureGroups: PYTHON_ENGINE_LOCK.maximumCaptureGroups,
maximumNativeExpansionCodePoints:
PYTHON_ENGINE_LOCK.maximumNativeExpansionCodePoints,
},
source: {
pyodide: {
repository: PYTHON_ENGINE_LOCK.pyodide.repository,
tag: PYTHON_ENGINE_LOCK.pyodide.tag,
commitSha1: PYTHON_ENGINE_LOCK.pyodide.commit,
license: PYTHON_ENGINE_LOCK.pyodide.license,
npmPackage: PYTHON_ENGINE_LOCK.npm.package,
npmResolved: PYTHON_ENGINE_LOCK.npm.resolved,
npmIntegrity: PYTHON_ENGINE_LOCK.npm.integrity,
},
cpython: {
repository: PYTHON_ENGINE_LOCK.cpython.repository,
tag: PYTHON_ENGINE_LOCK.cpython.tag,
tagObjectSha1: PYTHON_ENGINE_LOCK.cpython.tagObject,
commitSha1: PYTHON_ENGINE_LOCK.cpython.commit,
license: PYTHON_ENGINE_LOCK.cpython.license,
},
},
toolchain: {
abiVersion: PYTHON_ENGINE_LOCK.abiVersion,
architecture: "wasm32",
emscriptenVersion: PYTHON_ENGINE_LOCK.emscriptenVersion,
lockPythonVersion: PYTHON_ENGINE_LOCK.lockPythonVersion,
executablePythonVersion: PYTHON_ENGINE_LOCK.pythonVersion,
threads: false,
},
packaging: {
loaderTransformation:
"Removed only the upstream sourceMappingURL trailer; executable JavaScript is unchanged.",
upstreamLoaderSha256: PYTHON_ENGINE_LOCK.assets["pyodide.mjs"],
packedLoaderSha256: PYTHON_ENGINE_LOCK.strippedLoaderSha256,
externalPackagesIncluded: false,
lockPythonVersionNote:
"The upstream lock's 3.14.0 field identifies its CPython 3.14 ABI baseline; the executable runtime self-identifies as 3.14.2.",
},
files,
};
}
function equalJson(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
async function expectedChecksums(pack) {
const lines = [];
for (const name of CHECKSUM_FILES) {
lines.push(`${await sha256File(path.join(pack, name))} ${name}`);
}
return `${lines.join("\n")}\n`;
}
async function smokePythonPack(pack) {
await WebAssembly.compile(
await readFile(path.join(pack, "pyodide.asm.wasm")),
);
const moduleUrl = pathToFileURL(path.join(pack, "pyodide.mjs"));
moduleUrl.searchParams.set("regex-tools-verification", String(Date.now()));
const imported = await import(moduleUrl.href);
if (
imported.version !== PYTHON_ENGINE_LOCK.pyodideVersion ||
typeof imported.loadPyodide !== "function"
) {
throw new Error("The staged Pyodide module identity is invalid.");
}
const runtime = await imported.loadPyodide({
indexURL: `${pack}${path.sep}`,
stdout: () => undefined,
stderr: () => undefined,
});
const result = JSON.parse(
runtime.runPython(`
import json, re, sys
expression = re.compile(r"(?P<emoji>😀)|(?P<word>[a-z]+)", re.ASCII)
matches = [
[match.span(0), match.lastgroup]
for match in expression.finditer("😀 abc")
]
replacement_match = re.compile(r"(?P<word>[a-z]+)").search("abc")
json.dumps({
"python": ".".join(str(part) for part in sys.version_info[:3]),
"implementation": sys.implementation.name,
"matches": matches,
"replacement": replacement_match.expand(r"[\\g<word>]"),
})
`),
);
if (
result.python !== PYTHON_ENGINE_LOCK.pythonVersion ||
result.implementation !== "cpython" ||
JSON.stringify(result.matches) !==
JSON.stringify([
[[0, 1], "emoji"],
[[2, 5], "word"],
]) ||
result.replacement !== "[abc]"
) {
throw new Error("The staged CPython re runtime smoke test failed.");
}
}
export async function verifyPythonPack(packDirectory, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const pack = await assertRealDirectory(packDirectory, "Python engine pack");
const entries = (await readdir(pack, { withFileTypes: true })).sort(
(left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
);
if (
entries.length !== PACK_FILES.length ||
entries.some(
(entry, index) =>
entry.name !== PACK_FILES[index] ||
!entry.isFile() ||
entry.isSymbolicLink(),
)
) {
throw new Error(
`The staged Python pack must contain only: ${PACK_FILES.join(", ")}.`,
);
}
await assertHash(
path.join(pack, "LICENSE.pyodide.txt"),
PYTHON_ENGINE_LOCK.pyodide.licenseSha256,
"staged Pyodide licence",
);
await assertHash(
path.join(pack, "LICENSE.cpython.txt"),
PYTHON_ENGINE_LOCK.cpython.licenseSha256,
"staged CPython licence",
);
await assertHash(
path.join(pack, "pyodide.mjs"),
PYTHON_ENGINE_LOCK.strippedLoaderSha256,
"staged Pyodide loader",
);
for (const [name, expected] of Object.entries(PYTHON_ENGINE_LOCK.assets)) {
if (name !== "pyodide.mjs") {
await assertHash(path.join(pack, name), expected, `staged ${name}`);
}
}
const loader = await readFile(path.join(pack, "pyodide.mjs"), "utf8");
if (
loader.includes("sourceMappingURL") ||
loader.includes("/mnt/DATA/") ||
loader.includes("/home/zemion/")
) {
throw new Error(
"The staged Pyodide loader leaks a source-map or local build path.",
);
}
let metadata;
try {
metadata = JSON.parse(
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
);
} catch (error) {
throw new Error("engine-metadata.json is not valid JSON.", {
cause: error,
});
}
const expected = await expectedMetadata(repositoryRoot, pack);
if (!equalJson(metadata, expected)) {
throw new Error(
"The staged Python metadata does not match the pinned pack contract.",
);
}
if (
(await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !==
(await expectedChecksums(pack))
) {
throw new Error("The staged Python SHA256SUMS file is incorrect.");
}
await smokePythonPack(pack);
return metadata;
}
async function replaceDirectory(stage, target, label) {
const targetDetails = await lstat(target).catch(() => null);
if (
targetDetails &&
(!targetDetails.isDirectory() || targetDetails.isSymbolicLink())
) {
throw new Error(`${label} must be a real directory.`);
}
if (!targetDetails) {
await rename(stage, target);
return;
}
const backup = `${target}.replaced-${process.pid}-${Date.now()}`;
await rename(target, backup);
try {
await rename(stage, target);
} catch (error) {
await rename(backup, target);
throw error;
}
await rm(backup, { recursive: true });
}
export async function installPythonPack(packDirectory, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const sourcePack = await assertRealDirectory(
packDirectory,
"verified Python engine pack",
);
const metadata = await verifyPythonPack(sourcePack, repositoryRoot);
const engineRoot = await assertRealDirectory(
path.join(repositoryRoot, "public", "engines"),
"public engine directory",
);
const target = path.join(engineRoot, "python");
const stage = await mkdtemp(path.join(engineRoot, ".python-install-"));
try {
for (const name of PACK_FILES) {
await copyFile(
path.join(sourcePack, name),
path.join(stage, name),
fsConstants.COPYFILE_EXCL,
);
}
await verifyPythonPack(stage, repositoryRoot);
await replaceDirectory(stage, target, "public/engines/python");
return { output: target, metadata };
} finally {
await rm(stage, { recursive: true, force: true });
}
}
export async function buildPythonPack(options, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const source = await verifyPyodideSource(options.sourceDirectory);
const output = path.resolve(options.outputDirectory);
await mkdir(path.dirname(output), { recursive: true });
const stage = await mkdtemp(
path.join(path.dirname(output), ".python-pack-build-"),
);
try {
const [pyodideLicense, cpythonLicense] = await Promise.all([
fetchPinned(
PYTHON_ENGINE_LOCK.pyodide.licenseUrl,
PYTHON_ENGINE_LOCK.pyodide.licenseSha256,
"Pyodide licence",
),
fetchPinned(
PYTHON_ENGINE_LOCK.cpython.licenseUrl,
PYTHON_ENGINE_LOCK.cpython.licenseSha256,
"CPython licence",
),
]);
await writeFile(path.join(stage, "LICENSE.pyodide.txt"), pyodideLicense, {
flag: "wx",
});
await writeFile(path.join(stage, "LICENSE.cpython.txt"), cpythonLicense, {
flag: "wx",
});
for (const name of Object.keys(PYTHON_ENGINE_LOCK.assets)) {
if (name === "pyodide.mjs") {
const upstream = await readFile(path.join(source, name), "utf8");
const packed = withoutSourceMapReference(upstream);
if (sha256(packed) !== PYTHON_ENGINE_LOCK.strippedLoaderSha256) {
throw new Error("The deterministic Pyodide loader output changed.");
}
await writeFile(path.join(stage, name), packed, { flag: "wx" });
} else {
await copyFile(
path.join(source, name),
path.join(stage, name),
fsConstants.COPYFILE_EXCL,
);
}
}
const metadata = await expectedMetadata(repositoryRoot, stage);
await writeFile(
path.join(stage, "engine-metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
{ flag: "wx" },
);
await writeFile(
path.join(stage, "SHA256SUMS"),
await expectedChecksums(stage),
{ flag: "wx" },
);
await verifyPythonPack(stage, repositoryRoot);
await replaceDirectory(stage, output, "Python pack output");
return { output, metadata };
} finally {
await rm(stage, { recursive: true, force: true });
}
}
const invokedFile = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: "";
if (import.meta.url === invokedFile) {
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 2) {
throw new Error(
"Usage: node scripts/python-engine-pack.mjs [node_modules/pyodide] [.engine-build/python]",
);
}
const result = await buildPythonPack(
{
sourceDirectory: path.resolve(
root,
arguments_[0] ?? "node_modules/pyodide",
),
outputDirectory: path.resolve(
root,
arguments_[1] ?? ".engine-build/python",
),
},
root,
);
console.log(
`Built and verified ${result.metadata.runtime} / ${result.metadata.engineVersion} at ${result.output}.`,
);
}

View File

@@ -13,6 +13,7 @@ const mediaTypes = new Map([
[".css", "text/css; charset=utf-8"], [".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"], [".html", "text/html; charset=utf-8"],
[".js", "text/javascript; charset=utf-8"], [".js", "text/javascript; charset=utf-8"],
[".mjs", "text/javascript; charset=utf-8"],
[".json", "application/json; charset=utf-8"], [".json", "application/json; charset=utf-8"],
[".svg", "image/svg+xml"], [".svg", "image/svg+xml"],
[".wasm", "application/wasm"], [".wasm", "application/wasm"],
@@ -25,6 +26,14 @@ const testCatalogue = {
theme: { mode: "system", brand: "add·ideas" }, theme: { mode: "system", brand: "add·ideas" },
apps: [{ manifest: "./toolbox-app.json", enabled: true }], apps: [{ manifest: "./toolbox-app.json", enabled: true }],
}; };
const productionSecurityHeaders = {
"Content-Security-Policy":
"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "same-origin",
"X-Content-Type-Options": "nosniff",
};
function safeFile(requestPath) { function safeFile(requestPath) {
const decoded = decodeURIComponent(requestPath); const decoded = decodeURIComponent(requestPath);
@@ -49,6 +58,7 @@ const server = createServer(async (request, response) => {
response.writeHead(200, { response.writeHead(200, {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
...productionSecurityHeaders,
}); });
response.end(JSON.stringify(testCatalogue)); response.end(JSON.stringify(testCatalogue));
return; return;
@@ -65,7 +75,7 @@ const server = createServer(async (request, response) => {
"Content-Type": "Content-Type":
mediaTypes.get(path.extname(file)) ?? "application/octet-stream", mediaTypes.get(path.extname(file)) ?? "application/octet-stream",
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
"Cross-Origin-Resource-Policy": "same-origin", ...productionSecurityHeaders,
}); });
response.end(content); response.end(content);
} catch { } catch {

View File

@@ -1,8 +1,31 @@
import { lstat, readFile, readdir } from "node:fs/promises"; import { lstat, readFile, readdir } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { verifyJavaPack } from "./java-engine-pack.mjs";
import { verifyPcre2Pack } from "./pcre2-engine-pack.mjs";
import { verifyPythonPack } from "./python-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 0) {
const verifiers = new Map([
["--java-pack", verifyJavaPack],
["--pcre2-pack", verifyPcre2Pack],
["--python-pack", verifyPythonPack],
]);
const verifier = verifiers.get(arguments_[0]);
if (arguments_.length !== 2 || !verifier) {
throw new Error(
"Usage: node scripts/verify-engine-assets.mjs [--java-pack|--pcre2-pack|--python-pack PATH]",
);
}
const pack = path.resolve(root, arguments_[1]);
const metadata = await verifier(pack, root);
console.log(`Verified staged ${metadata.engineVersion} engine pack.`);
process.exit(0);
}
const engineDirectory = path.join(root, "dist", "engines"); const engineDirectory = path.join(root, "dist", "engines");
const details = await lstat(engineDirectory); const details = await lstat(engineDirectory);
if (details.isSymbolicLink() || !details.isDirectory()) { if (details.isSymbolicLink() || !details.isDirectory()) {
@@ -14,21 +37,34 @@ const entries = (await readdir(engineDirectory, { withFileTypes: true })).sort(
left.name < right.name ? -1 : left.name > right.name ? 1 : 0, left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
); );
if ( if (
entries.length !== 1 || entries.length !== 4 ||
!entries[0]?.isFile() || !entries[0]?.isFile() ||
entries[0].name !== "README.md" entries[0].name !== "README.md" ||
!entries[1]?.isDirectory() ||
entries[1].name !== "java" ||
!entries[2]?.isDirectory() ||
entries[2].name !== "pcre2" ||
!entries[3]?.isDirectory() ||
entries[3].name !== "python"
) { ) {
throw new Error( throw new Error(
"Regex Tools 0.1.0 must not ship an undeclared external engine pack.", "The production build must contain only README.md and the declared Java, PCRE2 and Python packs.",
); );
} }
const notice = await readFile(path.join(engineDirectory, "README.md"), "utf8"); const notice = await readFile(path.join(engineDirectory, "README.md"), "utf8");
if ( if (
!notice.includes("native `RegExp`") || !notice.includes("native `RegExp`") ||
!notice.includes("no external engine") !notice.includes("PCRE2 10.47") ||
!notice.includes("Pyodide 314.0.3") ||
!notice.includes("TeaVM 0.15.0")
) { ) {
throw new Error("The engine asset notice does not describe this release."); throw new Error("The engine asset notice does not describe this release.");
} }
await verifyPcre2Pack(path.join(engineDirectory, "pcre2"), root);
await verifyPythonPack(path.join(engineDirectory, "python"), root);
await verifyJavaPack(path.join(engineDirectory, "java"), root);
console.log("Verified ECMAScript-only engine assets (no WebAssembly pack)."); console.log(
"Verified native ECMAScript and bundled PCRE2, CPython/Pyodide and TeaVM Java engine assets.",
);

View File

@@ -0,0 +1,521 @@
.analysis-dialog {
width: min(90rem, calc(100% - 2rem));
}
.analysis-panel {
--analysis-high: var(--toolbox-danger);
--analysis-warning: var(--regex-warning);
--analysis-notice: var(--toolbox-accent);
min-height: 0;
}
.analysis-view-tabs {
display: flex;
gap: 0.25rem;
padding: 0.65rem 0.75rem 0;
border-bottom: 1px solid var(--toolbox-border);
background: var(--toolbox-surface-soft);
}
.analysis-view-tabs button {
border: 1px solid transparent;
border-radius: calc(var(--toolbox-radius) * 0.7)
calc(var(--toolbox-radius) * 0.7) 0 0;
padding: 0.55rem 0.8rem;
background: transparent;
color: var(--toolbox-muted);
font-weight: 750;
}
.analysis-view-tabs button[aria-selected="true"] {
border-color: var(--toolbox-border);
border-bottom-color: var(--toolbox-surface);
background: var(--toolbox-surface);
color: var(--toolbox-accent);
}
.analysis-content {
display: grid;
gap: 0.8rem;
padding: 0.75rem;
}
.analysis-section,
.analysis-result-block {
display: grid;
gap: 0.75rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.82);
background: var(--toolbox-surface);
}
.analysis-section > header,
.analysis-result-block > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem;
border-bottom: 1px solid var(--toolbox-border);
background: var(--toolbox-surface-soft);
}
.analysis-section > header h3,
.analysis-result-block > header h3 {
margin: 0.12rem 0 0;
font-size: 0.96rem;
}
.analysis-section > header > span,
.analysis-result-block > header > span {
color: var(--toolbox-muted);
font-size: 0.72rem;
font-weight: 700;
text-align: right;
}
.analysis-summary,
.analysis-advisory,
.analysis-stop-reason,
.analysis-error,
.analysis-unavailable {
margin: 0;
padding: 0.7rem 0.75rem;
color: var(--toolbox-muted);
font-size: 0.79rem;
line-height: 1.5;
}
.analysis-advisory,
.analysis-stop-reason {
border-left: 3px solid var(--toolbox-accent);
background: var(--toolbox-accent-soft);
}
.analysis-unavailable,
.analysis-error {
margin: 0.75rem 0.75rem 0;
border: 1px solid
color-mix(in srgb, var(--toolbox-danger) 45%, var(--toolbox-border));
border-radius: calc(var(--toolbox-radius) * 0.72);
background: color-mix(
in srgb,
var(--toolbox-danger) 8%,
var(--toolbox-surface)
);
color: color-mix(in srgb, var(--toolbox-danger) 78%, var(--toolbox-text));
}
.analysis-status {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 0.6rem;
margin: 0.75rem 0.75rem 0;
padding: 0.55rem 0.7rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.72);
background: var(--toolbox-surface-soft);
font-size: 0.78rem;
}
.analysis-status > span {
border-radius: 99rem;
padding: 0.2rem 0.42rem;
background: var(--toolbox-accent-soft);
color: var(--toolbox-accent);
font-size: 0.65rem;
font-weight: 800;
text-transform: uppercase;
}
.analysis-status > strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.analysis-progress {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 0.6rem;
margin: 0.5rem 0.75rem 0;
color: var(--toolbox-muted);
font-size: 0.7rem;
}
.analysis-progress progress {
width: 100%;
accent-color: var(--toolbox-accent);
}
.analysis-findings {
display: grid;
gap: 0.55rem;
padding: 0 0.75rem;
}
.analysis-finding {
overflow: hidden;
border: 1px solid var(--toolbox-border);
border-left-width: 4px;
border-radius: calc(var(--toolbox-radius) * 0.72);
background: var(--toolbox-surface-soft);
}
.analysis-finding.is-high {
border-left-color: var(--analysis-high);
}
.analysis-finding.is-warning {
border-left-color: var(--analysis-warning);
}
.analysis-finding.is-notice {
border-left-color: var(--analysis-notice);
}
.analysis-finding-title {
display: grid;
width: 100%;
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: 0.55rem;
border: 0;
padding: 0.65rem;
background: transparent;
color: var(--toolbox-text);
text-align: left;
}
button.analysis-finding-title:hover {
background: var(--toolbox-accent-soft);
}
.analysis-finding-title > span:last-child {
display: grid;
gap: 0.12rem;
}
.analysis-finding-title small {
color: var(--toolbox-muted);
font-size: 0.67rem;
font-weight: 500;
}
.analysis-risk-level,
.analysis-run-badge {
border: 1px solid currentColor;
border-radius: 99rem;
padding: 0.2rem 0.42rem;
font-size: 0.62rem;
font-weight: 850;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.analysis-risk-level.is-high {
color: var(--analysis-high);
}
.analysis-risk-level.is-warning {
color: color-mix(in srgb, var(--analysis-warning) 78%, var(--toolbox-text));
}
.analysis-risk-level.is-notice {
color: var(--analysis-notice);
}
.analysis-run-badge.is-complete {
color: color-mix(in srgb, var(--regex-mint) 72%, var(--toolbox-text));
}
.analysis-run-badge.is-timeout,
.analysis-run-badge.is-error {
color: var(--toolbox-danger);
}
.analysis-run-badge.is-cancelled,
.analysis-run-badge.is-wall-time-limit {
color: var(--analysis-warning);
}
.analysis-finding > p {
padding: 0 0.65rem 0.65rem;
color: var(--toolbox-text);
font-size: 0.78rem;
line-height: 1.45;
}
.analysis-finding dl {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
margin: 0;
border-top: 1px solid var(--toolbox-border);
}
.analysis-finding dl > div {
padding: 0.6rem;
}
.analysis-finding dt,
.analysis-result-meta dt,
.analysis-input-summary dt {
color: var(--toolbox-muted);
font-size: 0.64rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.analysis-finding dd,
.analysis-result-meta dd,
.analysis-input-summary dd {
margin: 0.2rem 0 0;
overflow-wrap: anywhere;
font-size: 0.74rem;
line-height: 1.4;
}
.analysis-details {
margin: 0 0.75rem 0.75rem;
color: var(--toolbox-muted);
font-size: 0.75rem;
}
.analysis-details summary {
cursor: pointer;
font-weight: 750;
}
.analysis-details li,
.analysis-limitations li {
margin-block: 0.35rem;
line-height: 1.4;
}
.analysis-growth-text-controls,
.analysis-number-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: 0.55rem;
padding: 0 0.75rem;
}
.analysis-growth-text-controls > label,
.analysis-number-controls > label {
display: grid;
align-content: start;
gap: 0.25rem;
}
.analysis-growth-text-controls label > span,
.analysis-number-controls label > span {
color: var(--toolbox-muted);
font-size: 0.68rem;
font-weight: 750;
}
.analysis-run-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0 0.75rem 0.75rem;
}
.analysis-result-block {
margin: 0 0.75rem 0.75rem;
}
.analysis-result-meta,
.analysis-input-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
margin: 0;
padding: 0 0.75rem;
}
.analysis-result-meta > div,
.analysis-input-summary > div {
padding: 0.55rem;
border-bottom: 1px solid var(--toolbox-border);
}
.analysis-table-scroll {
overflow: auto;
border-block: 1px solid var(--toolbox-border);
}
.analysis-table-scroll table {
min-width: 48rem;
}
.analysis-table-scroll th,
.analysis-table-scroll td {
white-space: nowrap;
}
.analysis-chart-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 0.65rem;
padding: 0 0.75rem;
}
.analysis-chart {
display: grid;
min-width: 0;
gap: 0.5rem;
margin: 0;
padding: 0.65rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.72);
background: var(--toolbox-surface-soft);
}
.analysis-chart figcaption {
display: grid;
gap: 0.12rem;
}
.analysis-chart figcaption strong {
font-size: 0.78rem;
}
.analysis-chart figcaption span {
color: var(--toolbox-muted);
font-size: 0.65rem;
}
.analysis-chart svg {
width: 100%;
height: 10rem;
overflow: visible;
}
.analysis-chart-axis {
fill: none;
stroke: var(--toolbox-border);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.analysis-chart-line {
fill: none;
stroke: var(--toolbox-accent);
stroke-width: 2;
vector-effect: non-scaling-stroke;
}
.analysis-chart circle {
fill: var(--toolbox-surface);
stroke: var(--toolbox-accent);
stroke-width: 2;
}
.analysis-empty-chart {
display: grid;
min-height: 10rem;
place-items: center;
color: var(--toolbox-muted);
font-size: 0.75rem;
}
.analysis-outcome-plot {
display: flex;
min-height: 10rem;
align-items: end;
gap: 0.22rem;
}
.analysis-outcome-plot > span {
min-width: 0.35rem;
max-width: 2rem;
height: 45%;
flex: 1;
border-radius: 0.2rem 0.2rem 0 0;
background: var(--toolbox-muted);
}
.analysis-outcome-plot > span.is-match {
height: 88%;
background: var(--regex-mint);
}
.analysis-outcome-plot > span.is-no-match {
background: var(--toolbox-accent);
}
.analysis-outcome-plot > span.is-timeout {
height: 100%;
background: var(--toolbox-danger);
}
.analysis-outcome-plot > span.is-crash,
.analysis-outcome-plot > span.is-compile-error {
height: 100%;
background: var(--analysis-warning);
}
.analysis-outcome-key {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.7rem;
color: var(--toolbox-muted);
font-size: 0.64rem;
}
.analysis-outcome-key span::before {
display: inline-block;
width: 0.55rem;
height: 0.55rem;
margin-right: 0.25rem;
border-radius: 0.15rem;
background: var(--toolbox-muted);
content: "";
}
.analysis-outcome-key .is-match::before {
background: var(--regex-mint);
}
.analysis-outcome-key .is-no-match::before {
background: var(--toolbox-accent);
}
.analysis-outcome-key .is-timeout::before {
background: var(--toolbox-danger);
}
.analysis-outcome-key .is-crash::before {
background: var(--analysis-warning);
}
.analysis-limitations {
margin: 0;
padding: 0 1.6rem 0.75rem 2rem;
color: var(--toolbox-muted);
font-size: 0.7rem;
}
@media (max-width: 40rem) {
.analysis-section > header,
.analysis-result-block > header {
align-items: start;
flex-direction: column;
}
.analysis-section > header > span,
.analysis-result-block > header > span {
text-align: left;
}
.analysis-status {
grid-template-columns: 1fr;
}
.analysis-status > strong {
white-space: normal;
}
}

View File

@@ -0,0 +1,248 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { AnalysisWorkerClient } from "../regex/analysis/AnalysisSupervisor";
import type {
BenchmarkWorkerSample,
GrowthWorkerSample,
} from "../regex/analysis/analysis.types";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { EcmaScriptSyntaxProvider } from "../regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { AnalysisPanel } from "./AnalysisPanel";
const provider = new EcmaScriptSyntaxProvider();
function benchmarkSample(milliseconds = 1): BenchmarkWorkerSample {
return {
accepted: true,
effectiveFlags: "dg",
subjectBytes: 5,
subjectUtf16: 5,
compileMs: milliseconds,
firstMatchMs: milliseconds + 1,
allMatchesMs: milliseconds + 2,
replacementMs: milliseconds + 3,
throughputBytesPerSecond: 1024 * 1024,
matchCount: 1,
matched: true,
matchCollectionTruncated: false,
replacementOutputUtf16: 1,
};
}
function growthSample(executionMs: number): GrowthWorkerSample {
return {
accepted: true,
effectiveFlags: "d",
subjectBytes: 5,
subjectUtf16: 5,
executionMs,
matchCount: 0,
matched: false,
matchCollectionTruncated: false,
};
}
function worker(
overrides: Partial<AnalysisWorkerClient> = {},
): AnalysisWorkerClient {
return {
identity: vi.fn().mockResolvedValue({
flavour: "ecmascript",
engineName: "Native ECMAScript RegExp",
engineVersion: "Fixture Browser 1",
runtimeVersion: "Fixture Browser 1",
nativeOffsetUnit: "utf16",
}),
benchmarkSample: vi.fn().mockResolvedValue(benchmarkSample()),
growthProbe: vi.fn().mockResolvedValue(growthSample(1)),
cancel: vi.fn(),
dispose: vi.fn(),
...overrides,
};
}
async function renderPanel(
overrides: Partial<Parameters<typeof AnalysisPanel>[0]> = {},
) {
const pattern = overrides.pattern ?? "(a+)+$";
const flavour = overrides.flavour ?? "ecmascript";
const syntax =
overrides.syntax ??
(await provider.parsePattern({
flavour: "ecmascript",
flavourVersion: "2025",
pattern,
flags: [],
options: {},
}));
const currentWorker = worker();
const rendered = render(
<AnalysisPanel
active
flavour={flavour}
pattern={pattern}
flags={[]}
subject="aaaa!"
replacement="x"
scanAll={false}
syntax={syntax}
createSupervisor={() => currentWorker}
{...overrides}
/>,
);
return { ...rendered, currentWorker, syntax };
}
afterEach(() => {
cleanup();
});
describe("AnalysisPanel", () => {
it("shows flavour-scoped advisory static findings and selects their source range", async () => {
const onSelectPatternRange = vi.fn();
await renderPanel({ onSelectPatternRange });
expect(
screen.getByRole("heading", { name: "Potential risk findings" }),
).toBeInTheDocument();
expect(screen.getByText("Potential nested-quantifier risk")).toBeVisible();
expect(screen.getByText(/potential risks found/u)).toBeVisible();
await userEvent.click(
screen.getByRole("button", {
name: /Potential nested-quantifier risk/u,
}),
);
expect(onSelectPatternRange).toHaveBeenCalledWith({
startUtf16: 0,
endUtf16: 5,
});
});
it("runs bounded cold and warm benchmark samples and renders p95", async () => {
const { currentWorker } = await renderPanel();
await userEvent.click(screen.getByRole("tab", { name: "Benchmark" }));
await userEvent.clear(
screen.getByRole("spinbutton", { name: "Measured samples" }),
);
await userEvent.type(
screen.getByRole("spinbutton", { name: "Measured samples" }),
"3",
);
await userEvent.click(
screen.getByRole("button", { name: "Run bounded benchmark" }),
);
const table = await screen.findByRole("table", {
name: "Cold and warm benchmark metrics",
});
expect(table).toHaveTextContent("Warm p95");
expect(table).toHaveTextContent("Compile");
expect(screen.getByText(/Fixture Browser 1/u)).toBeVisible();
expect(screen.getAllByText(/3 measured/u)).toHaveLength(2);
expect(currentWorker.identity).toHaveBeenCalled();
expect(currentWorker.benchmarkSample).toHaveBeenCalledTimes(7);
});
it("renders separate timing and outcome plots for bounded growth", async () => {
const samples = [growthSample(0.5), growthSample(10)];
const currentWorker = worker({
growthProbe: vi
.fn()
.mockImplementation(() => Promise.resolve(samples.shift()!)),
});
await renderPanel({ createSupervisor: () => currentWorker });
await userEvent.click(
screen.getByRole("button", { name: "Run bounded growth probe" }),
);
expect(
await screen.findByRole("img", {
name: /Execution-time growth plot/u,
}),
).toBeVisible();
expect(
screen.getByRole("img", { name: /Outcome plot with 2 samples/u }),
).toBeVisible();
expect(screen.getByText("Observed disproportionate growth")).toBeVisible();
expect(
screen.getByRole("table", { name: "Dynamic growth samples" }),
).toHaveTextContent("Normalized growth");
});
it("does not apply ECMAScript analysis to PCRE2", async () => {
const currentWorker = worker();
await renderPanel({
flavour: "pcre2",
createSupervisor: () => currentWorker,
});
expect(screen.getByRole("alert")).toHaveTextContent(
"supports ECMAScript 2025 only",
);
expect(
screen.getByRole("button", { name: "Run bounded growth probe" }),
).toBeDisabled();
expect(currentWorker.identity).not.toHaveBeenCalled();
});
it("never renders benchmark results against a changed subject", async () => {
const rendered = await renderPanel();
await userEvent.click(screen.getByRole("tab", { name: "Benchmark" }));
await userEvent.click(
screen.getByRole("button", { name: "Run bounded benchmark" }),
);
expect(
await screen.findByRole("table", {
name: "Cold and warm benchmark metrics",
}),
).toBeVisible();
rendered.rerender(
<AnalysisPanel
active
flavour="ecmascript"
pattern="(a+)+$"
flags={[]}
subject="changed subject"
replacement="x"
scanAll={false}
syntax={rendered.syntax}
createSupervisor={() => rendered.currentWorker}
/>,
);
expect(
screen.queryByRole("table", {
name: "Cold and warm benchmark metrics",
}),
).not.toBeInTheDocument();
});
it("terminates the active worker when the user cancels", async () => {
let rejectSample: ((reason: Error) => void) | undefined;
const pending = new Promise<BenchmarkWorkerSample>((_resolve, reject) => {
rejectSample = reject;
});
const currentWorker = worker({
benchmarkSample: vi.fn().mockReturnValue(pending),
cancel: vi.fn().mockImplementation(() => {
rejectSample?.(
new WorkerRequestError("cancelled", "fixture cancellation"),
);
}),
});
await renderPanel({ createSupervisor: () => currentWorker });
await userEvent.click(screen.getByRole("tab", { name: "Benchmark" }));
await userEvent.click(
screen.getByRole("button", { name: "Run bounded benchmark" }),
);
await userEvent.click(
await screen.findByRole("button", { name: "Cancel benchmark" }),
);
await waitFor(() => expect(currentWorker.cancel).toHaveBeenCalled());
expect(await screen.findAllByText(/Benchmark cancelled/u)).toHaveLength(2);
expect(currentWorker.dispose).toHaveBeenCalled();
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest"; import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import type { RegexExecutionResult } from "../regex/model/match"; import type { RegexExecutionResult } from "../regex/model/match";
import type { RegexSyntaxResult } from "../regex/model/syntax"; import type { RegexSyntaxResult } from "../regex/model/syntax";
import { CapabilityPanel } from "./CapabilityPanel"; import { CapabilityPanel } from "./CapabilityPanel";
@@ -78,11 +79,124 @@ describe("CapabilityPanel", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText("Fixture engine")).toBeInTheDocument(); expect(screen.getByText("Fixture engine")).toBeInTheDocument();
expect(screen.getByText("123")).toBeInTheDocument(); expect(screen.getByText("123")).toBeInTheDocument();
expect(
screen.getByText("Not separately reported by this adapter"),
).toBeInTheDocument();
expect(screen.getByText("1", { selector: "dd" })).toBeInTheDocument();
expect(screen.getByText("UTF-8 bytes")).toBeInTheDocument(); expect(screen.getByText("UTF-8 bytes")).toBeInTheDocument();
expect(screen.getByText("fixture")).toBeInTheDocument();
const replacement = screen.getByText("Replacement").closest("div"); const replacement = screen.getByText("Replacement").closest("div");
expect(replacement).toHaveTextContent("Unavailable"); expect(replacement).toHaveTextContent("Unavailable");
const compilation = screen.getByText("Compilation").closest("div"); const compilation = screen.getByText("Compilation").closest("div");
expect(compilation).toHaveTextContent("Available"); expect(compilation).toHaveTextContent("Available");
const generatedCases = screen.getByText("Generated cases").closest("div");
expect(generatedCases).toHaveTextContent(
"deterministic AST candidates, retained only after actual-engine verification",
);
const formatting = screen.getByText("Pattern formatting").closest("div");
expect(formatting).toHaveTextContent(
"grammar-backed literal/control escaping with mandatory exact-snapshot validation",
);
});
it("keeps PCRE2-only trace and code generation separate from ECMAScript-only tools", () => {
render(
<CapabilityPanel
syntax={syntax}
execution={{
...execution,
engine: {
...execution.engine,
flavour: "pcre2",
engineName: "PCRE2",
capabilities: {
...execution.engine.capabilities,
actualTrace: true,
},
},
}}
/>,
);
const trace = screen.getByText("Actual execution trace").closest("div");
expect(trace).toHaveTextContent("Available");
const generatedCases = screen.getByText("Generated cases").closest("div");
expect(generatedCases).toHaveTextContent(
"Unavailable — the ECMAScript case generator is not applied to PCRE2",
);
const formatting = screen.getByText("Pattern formatting").closest("div");
expect(formatting).toHaveTextContent(
"Unavailable — the ECMAScript formatter is not applied to PCRE2",
);
const codeGeneration = screen.getByText("Code generation").closest("div");
expect(codeGeneration).toHaveTextContent(
"Available — reviewed PCRE2 10.47 8-bit C17 target",
);
});
it.each([
["python", "Python re"],
["java", "Java Pattern"],
] as const)(
"does not borrow ECMAScript or PCRE2 feature semantics for %s",
(flavour, label) => {
render(
<CapabilityPanel
syntax={syntax}
execution={{
...execution,
engine: {
...execution.engine,
flavour,
engineName: `${label} fixture`,
},
}}
/>,
);
expect(
screen.getByText("Actual execution trace").closest("div"),
).toHaveTextContent(
"Unavailable — automatic-callout tracing is implemented only for PCRE2",
);
expect(
screen.getByText("Static risk analysis").closest("div"),
).toHaveTextContent(
`Unavailable — ECMAScript heuristics are not applied to ${label}`,
);
expect(
screen.getByText("Generated cases").closest("div"),
).toHaveTextContent(
`Unavailable — the ECMAScript case generator is not applied to ${label}`,
);
expect(
screen.getByText("Pattern formatting").closest("div"),
).toHaveTextContent(
`Unavailable — the ECMAScript formatter is not applied to ${label}`,
);
expect(
screen.getByText("Code generation").closest("div"),
).toHaveTextContent(
"Unavailable — C17 code generation is implemented only for PCRE2",
);
},
);
it("offers an accessible close control when used as a modal panel", async () => {
const onClose = vi.fn();
const user = userEvent.setup();
render(
<CapabilityPanel
syntax={syntax}
execution={execution}
onClose={onClose}
/>,
);
await user.click(
screen.getByRole("button", { name: "Close capabilities" }),
);
expect(onClose).toHaveBeenCalledOnce();
}); });
}); });

View File

@@ -1,7 +1,21 @@
import { REGEXPP_VERSION, SYNTAX_PROFILE } from "../version"; import { SYNTAX_PROFILE } from "../version";
import type { RegexExecutionResult } from "../regex/model/match"; import type { RegexExecutionResult } from "../regex/model/match";
import type { RegexSyntaxResult } from "../regex/model/syntax"; import type { RegexSyntaxResult } from "../regex/model/syntax";
import type { RegexEngineCapabilities } from "../regex/model/flavour"; import type {
RegexEngineCapabilities,
RegexFlavourId,
} from "../regex/model/flavour";
const FLAVOUR_LABELS: Readonly<Record<RegexFlavourId, string>> = {
ecmascript: "ECMAScript",
pcre2: "PCRE2",
pcre: "PCRE",
python: "Python re",
go: "Go regexp",
rust: "Rust regex",
dotnet: ".NET Regex",
java: "Java Pattern",
};
function capability( function capability(
capabilities: RegexEngineCapabilities | undefined, capabilities: RegexEngineCapabilities | undefined,
@@ -35,23 +49,30 @@ function offsetLabel(
export function CapabilityPanel({ export function CapabilityPanel({
syntax, syntax,
execution, execution,
onClose,
}: { }: {
readonly syntax?: RegexSyntaxResult; readonly syntax?: RegexSyntaxResult;
readonly execution?: RegexExecutionResult; readonly execution?: RegexExecutionResult;
readonly onClose?: () => void;
}) { }) {
const capabilities = execution?.engine.capabilities; const capabilities = execution?.engine.capabilities;
const activeFlavour =
execution?.engine.flavour ?? syntax?.root.support.flavour;
const activeFlavourLabel = activeFlavour
? FLAVOUR_LABELS[activeFlavour]
: undefined;
const rows = [ const rows = [
[ [
"Flavour", "Flavour",
execution?.engine.flavour ?? execution?.engine.flavour ??
syntax?.root.support.flavour ?? syntax?.root.support.flavour ??
"ECMAScript (awaiting workers)", "Awaiting flavour workers",
], ],
[ [
"Syntax provider", "Syntax provider",
syntax syntax
? `${syntax.provider.id} ${syntax.provider.version}` ? `${syntax.provider.id} ${syntax.provider.version}`
: `regexpp ${REGEXPP_VERSION} (awaiting syntax worker)`, : "Awaiting syntax worker result",
], ],
["Syntax profile", SYNTAX_PROFILE], ["Syntax profile", SYNTAX_PROFILE],
[ [
@@ -65,9 +86,20 @@ export function CapabilityPanel({
execution?.engine.engineName ?? "Awaiting first engine result", execution?.engine.engineName ?? "Awaiting first engine result",
], ],
[ [
"Engine/runtime version", "Engine version",
execution?.engine.engineVersion ?? "Shown after first execution", execution?.engine.engineVersion ?? "Shown after first execution",
], ],
[
"Runtime version",
execution?.engine.runtimeVersion ??
(execution
? "Not separately reported by this adapter"
: "Shown after first execution"),
],
[
"Adapter version",
execution?.engine.adapterVersion ?? "Shown after first execution",
],
["Native offsets", offsetLabel(execution?.engine.offsetUnit)], ["Native offsets", offsetLabel(execution?.engine.offsetUnit)],
["Compilation", capability(capabilities, "compilation")], ["Compilation", capability(capabilities, "compilation")],
["Matching", capability(capabilities, "matching")], ["Matching", capability(capabilities, "matching")],
@@ -83,13 +115,57 @@ export function CapabilityPanel({
], ],
[ [
"Actual execution trace", "Actual execution trace",
capability( activeFlavour === "pcre2"
? capability(
capabilities, capabilities,
"actualTrace", "actualTrace",
"the adapter exposes no engine trace", "this PCRE2 adapter exposes no bounded automatic-callout trace",
), )
: activeFlavour
? "Unavailable — automatic-callout tracing is implemented only for PCRE2"
: "Awaiting flavour worker result",
], ],
["Benchmark", capability(capabilities, "benchmark")], ["Benchmark", capability(capabilities, "benchmark")],
[
"Static risk analysis",
activeFlavour === "ecmascript"
? "Available — advisory ECMAScript 2025 heuristics"
: activeFlavourLabel
? `Unavailable — ECMAScript heuristics are not applied to ${activeFlavourLabel}`
: "Awaiting flavour worker result",
],
[
"Generated cases",
activeFlavour === "ecmascript"
? "Available — deterministic AST candidates, retained only after actual-engine verification"
: activeFlavourLabel
? `Unavailable — the ECMAScript case generator is not applied to ${activeFlavourLabel}`
: "Awaiting flavour worker result",
],
[
"Pattern formatting",
activeFlavour === "ecmascript"
? "Available — grammar-backed literal/control escaping with mandatory exact-snapshot validation"
: activeFlavourLabel
? `Unavailable — the ECMAScript formatter is not applied to ${activeFlavourLabel}`
: "Awaiting flavour worker result",
],
[
"Code generation",
activeFlavour === "pcre2"
? "Available — reviewed PCRE2 10.47 8-bit C17 target"
: activeFlavour
? "Unavailable — C17 code generation is implemented only for PCRE2"
: "Awaiting flavour worker result",
],
[
"Known syntax gaps",
syntax
? syntax.coverage.unsupportedConstructs.length > 0
? syntax.coverage.unsupportedConstructs.join("; ")
: "None reported by the active provider"
: "Awaiting syntax worker result",
],
] as const; ] as const;
return ( return (
<section <section
@@ -101,7 +177,20 @@ export function CapabilityPanel({
<p className="eyebrow">Current provider and adapter metadata</p> <p className="eyebrow">Current provider and adapter metadata</p>
<h2 id="capability-heading">Capabilities</h2> <h2 id="capability-heading">Capabilities</h2>
</div> </div>
<div className="panel-heading-actions">
<span className="provenance-badge">Community build</span> <span className="provenance-badge">Community build</span>
{onClose ? (
<button
type="button"
className="icon-button"
aria-label="Close capabilities"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
) : null}
</div>
</header> </header>
<dl className="capability-grid"> <dl className="capability-grid">
{rows.map(([term, value]) => ( {rows.map(([term, value]) => (
@@ -112,10 +201,11 @@ export function CapabilityPanel({
))} ))}
</dl> </dl>
<p className="capability-roadmap"> <p className="capability-roadmap">
Next flavour: official PCRE2 10.47 WebAssembly with actual callout Availability is never inferred by substituting one flavour for another.
traces. Python, Go, Rust, .NET and Java remain unavailable until their Static analysis, generated cases and formatting are ECMAScript-only;
named runtimes pass the same worker, offset, conformance and licensing automatic-callout tracing and reviewed C17 generation are PCRE2-only.
gates. Python and Java retain the engine identity and compatibility boundaries
reported by their own workers.
</p> </p>
</section> </section>
); );

View File

@@ -0,0 +1,331 @@
.comparison-code-dialog {
width: min(88rem, calc(100% - 2rem));
}
.comparison-code-panel {
max-height: calc(100vh - 2rem);
overflow: auto;
}
.comparison-tabs {
display: flex;
gap: 0.35rem;
padding: 0.7rem 0.8rem 0;
}
.comparison-tabs button {
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.72);
padding: 0.55rem 0.8rem;
background: var(--toolbox-surface);
color: var(--toolbox-text);
font-weight: 750;
}
.comparison-tabs button[aria-selected="true"] {
border-color: var(--toolbox-accent);
background: var(--toolbox-accent-soft);
}
.comparison-config,
.comparison-tab-panel {
display: grid;
gap: 0.8rem;
padding: 0.8rem;
}
.comparison-config {
border-bottom: 1px solid var(--toolbox-border);
background: var(--toolbox-surface-soft);
}
.comparison-config-row,
.comparison-actions {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 0.65rem;
}
.comparison-config label,
.comparison-limit-grid label,
.comparison-text-field {
display: grid;
gap: 0.3rem;
color: var(--toolbox-muted);
font-size: 0.75rem;
font-weight: 700;
}
.comparison-config select,
.comparison-config input,
.comparison-config textarea {
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.65);
padding: 0.5rem 0.6rem;
background: var(--toolbox-surface);
color: var(--toolbox-text);
}
.comparison-config textarea {
min-height: 4.8rem;
resize: vertical;
font-family: ui-monospace, monospace;
font-size: 0.78rem;
line-height: 1.45;
}
.comparison-scan-control {
display: flex !important;
align-items: center;
padding-bottom: 0.5rem;
}
.comparison-variant-grid,
.comparison-side-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.8rem;
}
.comparison-flag-selector,
.comparison-limit-grid {
min-width: 0;
margin: 0;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.7);
padding: 0.55rem;
}
.comparison-flag-selector legend,
.comparison-limit-grid legend {
padding-inline: 0.3rem;
color: var(--toolbox-muted);
font-size: 0.72rem;
font-weight: 750;
}
.comparison-flag-selector {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.comparison-flag-selector label {
display: inline-flex;
align-items: center;
gap: 0.25rem;
border: 1px solid var(--toolbox-border);
border-radius: 99rem;
padding: 0.25rem 0.45rem;
background: var(--toolbox-surface);
}
.comparison-flag-selector span {
font-size: 0.67rem;
}
.comparison-limit-grid {
display: grid;
grid-template-columns: repeat(6, minmax(6rem, 1fr));
gap: 0.5rem;
}
.comparison-subject textarea {
min-height: 7rem;
}
.comparison-actions p {
margin: 0;
color: var(--toolbox-muted);
font-size: 0.78rem;
}
.comparison-panel-status.status-error,
.comparison-error {
color: var(--toolbox-danger);
}
.comparison-result {
display: grid;
gap: 0.8rem;
}
.comparison-verdict,
.comparison-reasons,
.comparison-differences,
.comparison-alignments,
.generated-code-result {
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.78);
padding: 0.8rem;
background: var(--toolbox-surface);
}
.comparison-verdict h3,
.comparison-verdict p,
.comparison-verdict small,
.comparison-reasons h4,
.comparison-differences h4,
.comparison-alignments h4,
.generated-code-result h3,
.generated-code-result p {
margin: 0;
}
.comparison-verdict {
display: grid;
gap: 0.3rem;
border-left: 4px solid var(--regex-success);
}
.comparison-verdict.verdict-not-comparable {
border-left-color: var(--regex-warning);
}
.comparison-verdict.verdict-different-for-current-input {
border-left-color: var(--toolbox-danger);
}
.comparison-verdict p,
.comparison-verdict small,
.comparison-reasons,
.comparison-differences > p,
.comparison-alignments > p,
.generated-code-result li {
color: var(--toolbox-muted);
font-size: 0.77rem;
line-height: 1.5;
}
.comparison-reasons ul,
.generated-code-result ul {
margin-bottom: 0;
}
.comparison-side-card {
min-width: 0;
overflow: hidden;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.78);
background: var(--toolbox-surface);
}
.comparison-side-card > header,
.generated-code-result > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.7rem;
padding: 0.7rem;
border-bottom: 1px solid var(--toolbox-border);
}
.comparison-side-card h4,
.comparison-side-card p {
margin: 0;
}
.comparison-side-card dl {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin: 0;
}
.comparison-side-card dl > div {
min-width: 0;
padding: 0.55rem 0.7rem;
border-bottom: 1px solid var(--toolbox-border);
}
.comparison-side-card dt {
color: var(--toolbox-muted);
font-size: 0.67rem;
font-weight: 750;
text-transform: uppercase;
}
.comparison-side-card dd {
margin: 0.15rem 0 0;
overflow-wrap: anywhere;
font-size: 0.76rem;
}
.comparison-side-card details,
.comparison-side-card > .comparison-error {
padding: 0.55rem 0.7rem;
font-size: 0.74rem;
}
.comparison-side-card details ul {
margin-bottom: 0;
}
.comparison-differences,
.comparison-alignments {
overflow: auto;
}
.comparison-differences h4,
.comparison-alignments h4 {
margin-bottom: 0.65rem;
}
.comparison-differences td code {
display: block;
max-width: 22rem;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.comparison-limit-note {
margin-bottom: 0 !important;
}
.generated-code-result {
min-width: 0;
padding: 0;
overflow: hidden;
}
.generated-code-result > p,
.generated-code-result > ul {
margin-inline: 0.8rem;
}
.generated-code-result pre {
max-height: 34rem;
overflow: auto;
margin: 0;
border-top: 1px solid var(--toolbox-border);
padding: 0.8rem;
background: #111827;
color: #e5e7eb;
font-size: 0.72rem;
line-height: 1.45;
}
.comparison-empty-state {
margin: 0;
padding: 1.2rem;
color: var(--toolbox-muted);
text-align: center;
}
@media (max-width: 64rem) {
.comparison-limit-grid {
grid-template-columns: repeat(3, minmax(6rem, 1fr));
}
}
@media (max-width: 48rem) {
.comparison-variant-grid,
.comparison-side-grid,
.comparison-side-card dl {
grid-template-columns: 1fr;
}
.comparison-limit-grid {
grid-template-columns: repeat(2, minmax(6rem, 1fr));
}
}

View File

@@ -0,0 +1,82 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { ComparisonCodePanel } from "./ComparisonCodePanel";
function renderPanel() {
const onClose = vi.fn();
render(
<ComparisonCodePanel
activeFlavour="ecmascript"
activeFlags={["g", "u"]}
activeOptions={{}}
pattern={"(?<word>\\p{Letter}+)"}
subject="Grüße"
replacement="$<word>!"
scanAll
timeoutMs={2_000}
onClose={onClose}
/>,
);
return { onClose };
}
describe("ComparisonCodePanel", () => {
it("exposes comparison and reviewed PCRE2 C generation through one usable path", () => {
renderPanel();
expect(
screen.getByRole("tab", { name: "Compare engines" }),
).toHaveAttribute("aria-selected", "true");
expect(
screen.getByRole("textbox", { name: "Shared comparison pattern" }),
).toHaveValue("(?<word>\\p{Letter}+)");
expect(
screen.getByRole("button", { name: "Run comparison" }),
).toBeEnabled();
fireEvent.click(screen.getByRole("tab", { name: "PCRE2 C code" }));
fireEvent.click(
screen.getByRole("button", { name: "Generate reviewed C17" }),
);
expect(screen.getByText("PCRE2 10.47 8-bit · all-matches")).toBeVisible();
expect(screen.getByText("Exact UTF-8 byte arrays")).toBeVisible();
expect(
screen.getByText(/PCRE2_UTF and PCRE2_UCP are mandatory/u),
).toBeVisible();
const source = screen.getByText(
/This generated program requires PCRE2 10\.47 exactly/u,
);
expect(source).toHaveTextContent("pcre2_set_match_limit");
expect(source).not.toHaveTextContent("(?<word>\\p{Letter}+)");
});
it("uses the explicit PCRE2 variant without rewriting it", () => {
renderPanel();
fireEvent.change(screen.getByLabelText("Pattern model"), {
target: { value: "variants" },
});
const pcrePattern = screen.getByLabelText("PCRE2 pattern variant");
fireEvent.change(pcrePattern, {
target: { value: "(?<word>\\w++)" },
});
fireEvent.click(screen.getByRole("tab", { name: "PCRE2 C code" }));
fireEvent.click(
screen.getByRole("button", { name: "Generate reviewed C17" }),
);
const source = screen.getByText(
/This generated program requires PCRE2 10\.47 exactly/u,
);
expect(source).toHaveTextContent("0x2b, 0x2b");
expect(source).not.toHaveTextContent("(?<word>\\w++)");
});
it("closes through the dialog action", () => {
const { onClose } = renderPanel();
fireEvent.click(
screen.getByRole("button", { name: "Close compare and generate" }),
);
expect(onClose).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,884 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { writeClipboardText } from "../browser/clipboard";
import {
AVAILABLE_REGEX_FLAVOURS,
defaultRegexOptions,
} from "../regex/flavours/flavour-registry";
import { ComparisonOrchestrator } from "../regex/comparison/ComparisonOrchestrator";
import type {
ComparisonFlavour,
ComparisonPatternModel,
RegexComparisonResult,
} from "../regex/comparison/comparison.types";
import {
generatePcre2C,
type GeneratedPcre2CProgram,
} from "../regex/codegen/pcre2-c";
import {
DEFAULT_REGEX_LIMITS,
utf8ByteLength,
} from "../regex/execution/request-limits";
import type {
RegexEngineOptions,
RegexFlavourId,
} from "../regex/model/flavour";
import "./ComparisonCodePanel.css";
const ECMASCRIPT = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");
const PCRE2 = AVAILABLE_REGEX_FLAVOURS.require("pcre2");
const MAXIMUM_RENDERED_DIFFERENCES = 250;
const MAXIMUM_RENDERED_ALIGNMENTS = 100;
type PanelTab = "compare" | "code";
type PanelStatus =
| { readonly kind: "idle"; readonly message: string }
| { readonly kind: "running"; readonly message: string }
| { readonly kind: "ready"; readonly message: string }
| { readonly kind: "error"; readonly message: string };
export interface ComparisonCodePanelProps {
readonly activeFlavour: RegexFlavourId;
readonly activeFlags: readonly string[];
readonly activeOptions: RegexEngineOptions;
readonly pattern: string;
readonly subject: string;
readonly replacement: string;
readonly scanAll: boolean;
readonly timeoutMs: number;
readonly onClose: () => void;
}
function initialFlags(
activeFlavour: RegexFlavourId,
activeFlags: readonly string[],
flavour: ComparisonFlavour,
): readonly string[] {
const definition = flavour === "ecmascript" ? ECMASCRIPT : PCRE2;
return activeFlavour === flavour
? definition.flags
.map((flag) => flag.value)
.filter((flag) => activeFlags.includes(flag))
: definition.defaultFlags;
}
function initialOptions(
activeFlavour: RegexFlavourId,
activeOptions: RegexEngineOptions,
): RegexEngineOptions {
return activeFlavour === "pcre2" ? activeOptions : defaultRegexOptions(PCRE2);
}
function toggleFlag(
current: readonly string[],
flag: string,
enabled: boolean,
flavour: ComparisonFlavour,
): readonly string[] {
const definition = flavour === "ecmascript" ? ECMASCRIPT : PCRE2;
const selected = new Set(current);
if (enabled) {
selected.add(flag);
for (const group of definition.mutuallyExclusiveFlags ?? []) {
if (!group.includes(flag)) continue;
for (const incompatible of group) {
if (incompatible !== flag) selected.delete(incompatible);
}
}
} else {
selected.delete(flag);
}
return definition.flags
.map((candidate) => candidate.value)
.filter((candidate) => selected.has(candidate));
}
function preview(value: string | undefined, maximum = 240): string {
if (value === undefined) return "—";
return value.length <= maximum
? value
: `${value.slice(0, maximum)}… (${value.length.toLocaleString()} UTF-16 units)`;
}
function downloadSource(program: GeneratedPcre2CProgram): void {
const url = URL.createObjectURL(
new Blob([program.source], { type: "text/x-c;charset=utf-8" }),
);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = program.fileName;
anchor.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
}
function engineExecution(result: RegexComparisonResult["sides"][number]) {
return result.runtime.replacement?.execution ?? result.runtime.execution;
}
function SideResult({
side,
}: {
readonly side: RegexComparisonResult["sides"][number];
}) {
const execution = engineExecution(side);
const diagnostics = [
...(side.syntax.pattern?.diagnostics ?? []),
...(side.syntax.replacement?.diagnostics ?? []),
...(execution?.diagnostics ?? []),
];
return (
<article className="comparison-side-card">
<header>
<div>
<p className="eyebrow">Exact request {side.requestIdentity}</p>
<h4>
{side.input.flavour === "ecmascript" ? "ECMAScript" : "PCRE2"}
</h4>
</div>
<span className={`status status-${side.runtime.status}`}>
{side.runtime.status}
</span>
</header>
<dl>
<div>
<dt>Pattern syntax</dt>
<dd>
{side.syntax.pattern
? side.syntax.pattern.accepted
? "Accepted"
: "Rejected"
: side.syntax.status}
</dd>
</div>
<div>
<dt>Provider</dt>
<dd>
{side.syntax.pattern
? `${side.syntax.pattern.provider.id} ${side.syntax.pattern.provider.version}`
: "Unavailable"}
</dd>
</div>
<div>
<dt>Engine compile</dt>
<dd>
{execution
? execution.accepted
? "Accepted"
: "Rejected"
: side.runtime.status}
</dd>
</div>
<div>
<dt>Engine identity</dt>
<dd>
{execution
? `${execution.engine.engineName} · ${execution.engine.engineVersion}`
: "Unavailable"}
</dd>
</div>
<div>
<dt>Exact flags</dt>
<dd>
user {side.input.flags.join("") || "none"} · effective{" "}
{execution?.flags.effectiveFlags || "unavailable"}
</dd>
</div>
<div>
<dt>Options</dt>
<dd>
<code>{JSON.stringify(side.input.options)}</code>
</dd>
</div>
<div>
<dt>Matches</dt>
<dd>{execution?.matches.length.toLocaleString() ?? "Unavailable"}</dd>
</div>
<div>
<dt>Offsets</dt>
<dd>
native {execution?.engine.offsetUnit ?? "unavailable"} · comparison
UTF-16
</dd>
</div>
{side.runtime.replacement ? (
<div>
<dt>Replacement</dt>
<dd>
{side.runtime.replacement.outputBytes.toLocaleString()} bytes
{side.runtime.replacement.truncated ? " · incomplete" : ""}
</dd>
</div>
) : null}
</dl>
{side.syntax.pattern?.coverage.unsupportedConstructs.length ? (
<details>
<summary>Provider coverage gaps</summary>
<ul>
{side.syntax.pattern.coverage.unsupportedConstructs.map((gap) => (
<li key={gap}>{gap}</li>
))}
</ul>
</details>
) : null}
{diagnostics.length > 0 ? (
<details>
<summary>{diagnostics.length.toLocaleString()} diagnostic(s)</summary>
<ul>
{diagnostics.slice(0, 100).map((diagnostic) => (
<li key={`${diagnostic.source}-${diagnostic.id}`}>
<strong>{diagnostic.severity}:</strong> {diagnostic.message}
</li>
))}
</ul>
</details>
) : null}
{side.syntax.error || side.runtime.error ? (
<p className="comparison-error">
{side.syntax.error ?? side.runtime.error}
</p>
) : null}
</article>
);
}
function ComparisonResultView({
result,
}: {
readonly result: RegexComparisonResult;
}) {
const visibleDifferences = result.differences.slice(
0,
MAXIMUM_RENDERED_DIFFERENCES,
);
const visibleAlignments = result.matchAlignments.slice(
0,
MAXIMUM_RENDERED_ALIGNMENTS,
);
return (
<div className="comparison-result" aria-live="polite">
<section className={`comparison-verdict verdict-${result.status}`}>
<p className="eyebrow">Bounded comparison verdict</p>
<h3>{result.status.replaceAll("-", " ")}</h3>
<p>{result.notices[0]?.message}</p>
<small>
{result.elapsedMs.toFixed(1)} ms wall time ·{" "}
{result.totalDifferences.toLocaleString()} recorded difference(s)
</small>
</section>
{result.notComparable.length > 0 ? (
<section className="comparison-reasons">
<h4>Why this run is not comparable</h4>
<ul>
{result.notComparable.map((reason, index) => (
<li key={`${reason.code}-${reason.flavour ?? "both"}-${index}`}>
<strong>{reason.code.replaceAll("-", " ")}:</strong>{" "}
{reason.message}
</li>
))}
</ul>
</section>
) : null}
<div className="comparison-side-grid">
<SideResult side={result.sides[0]} />
<SideResult side={result.sides[1]} />
</div>
<section className="comparison-differences">
<h4>Syntax, execution and replacement differences</h4>
{visibleDifferences.length === 0 ? (
<p>
No retained semantic differences for this subject. Engine metadata
can still differ.
</p>
) : (
<table>
<thead>
<tr>
<th>Kind</th>
<th>Finding</th>
<th>ECMAScript</th>
<th>PCRE2</th>
</tr>
</thead>
<tbody>
{visibleDifferences.map((difference, index) => (
<tr key={`${difference.kind}-${index}`}>
<td>{difference.kind.replaceAll("-", " ")}</td>
<td>{difference.summary}</td>
<td>
<code>{preview(difference.left)}</code>
</td>
<td>
<code>{preview(difference.right)}</code>
</td>
</tr>
))}
</tbody>
</table>
)}
{visibleDifferences.length < result.totalDifferences ? (
<p className="comparison-limit-note">
Rendering the first {visibleDifferences.length.toLocaleString()} of{" "}
{result.totalDifferences.toLocaleString()} differences.
</p>
) : null}
</section>
{visibleAlignments.length > 0 ? (
<section className="comparison-alignments">
<h4>Match alignment by normalized editor range</h4>
<table>
<thead>
<tr>
<th>Alignment</th>
<th>ECMAScript UTF-16</th>
<th>PCRE2 UTF-16</th>
<th>Equal</th>
</tr>
</thead>
<tbody>
{visibleAlignments.map((alignment, index) => (
<tr key={`${alignment.alignment}-${index}`}>
<td>{alignment.alignment.replaceAll("-", " ")}</td>
<td>
{alignment.left
? `${alignment.left.range.startUtf16}${alignment.left.range.endUtf16}`
: "—"}
</td>
<td>
{alignment.right
? `${alignment.right.range.startUtf16}${alignment.right.range.endUtf16}`
: "—"}
</td>
<td>{alignment.equal ? "yes" : "no"}</td>
</tr>
))}
</tbody>
</table>
{visibleAlignments.length < result.totalMatchAlignments ? (
<p className="comparison-limit-note">
Rendering the first {visibleAlignments.length.toLocaleString()} of{" "}
{result.totalMatchAlignments.toLocaleString()} alignments.
</p>
) : null}
</section>
) : null}
</div>
);
}
export function ComparisonCodePanel({
activeFlavour,
activeFlags,
activeOptions,
pattern: initialPattern,
subject: initialSubject,
replacement: initialReplacement,
scanAll: initialScanAll,
timeoutMs: initialTimeoutMs,
onClose,
}: ComparisonCodePanelProps) {
const [tab, setTab] = useState<PanelTab>("compare");
const [patternModel, setPatternModel] =
useState<ComparisonPatternModel>("shared");
const [sharedPattern, setSharedPattern] = useState(initialPattern);
const [ecmaPattern, setEcmaPattern] = useState(initialPattern);
const [pcrePattern, setPcrePattern] = useState(initialPattern);
const [subject, setSubject] = useState(initialSubject);
const [operation, setOperation] = useState<"match" | "replace">("match");
const [ecmaReplacement, setEcmaReplacement] = useState(initialReplacement);
const [pcreReplacement, setPcreReplacement] = useState(initialReplacement);
const [ecmaFlags, setEcmaFlags] = useState(() =>
initialFlags(activeFlavour, activeFlags, "ecmascript"),
);
const [pcreFlags, setPcreFlags] = useState(() =>
initialFlags(activeFlavour, activeFlags, "pcre2"),
);
const [pcreOptions, setPcreOptions] = useState(() =>
initialOptions(activeFlavour, activeOptions),
);
const [scanAll, setScanAll] = useState(initialScanAll);
const [timeoutMs, setTimeoutMs] = useState(initialTimeoutMs);
const [maximumMatches, setMaximumMatches] = useState(1_000);
const [maximumCaptureRows, setMaximumCaptureRows] = useState(10_000);
const [maximumOutputBytes, setMaximumOutputBytes] = useState(1024 * 1024);
const [status, setStatus] = useState<PanelStatus>({
kind: "idle",
message: "Configure two exact engine requests.",
});
const [result, setResult] = useState<RegexComparisonResult>();
const [generated, setGenerated] = useState<GeneratedPcre2CProgram>();
const [generatedStatus, setGeneratedStatus] = useState("");
const orchestrator = useRef<ComparisonOrchestrator | undefined>(undefined);
const comparisonRevision = useRef(0);
useEffect(
() => () => {
comparisonRevision.current += 1;
orchestrator.current?.dispose();
orchestrator.current = undefined;
},
[],
);
const pcreOptionValues = useMemo(
() => ({
matchLimit: Number(pcreOptions.matchLimit),
depthLimit: Number(pcreOptions.depthLimit),
heapLimitKib: Number(pcreOptions.heapLimitKib),
}),
[pcreOptions],
);
const compare = async () => {
const revision = ++comparisonRevision.current;
setResult(undefined);
setStatus({
kind: "running",
message: "Running independently killable ECMAScript and PCRE2 workers…",
});
try {
orchestrator.current ??= new ComparisonOrchestrator();
const comparison = await orchestrator.current.compare({
schemaVersion: 1,
patternModel,
operation,
subject,
scanAll,
maximumMatches,
maximumCaptureRows,
maximumOutputBytes,
timeoutMs,
sides: [
{
flavour: "ecmascript",
flavourVersion: ECMASCRIPT.defaultVersion,
pattern: patternModel === "shared" ? sharedPattern : ecmaPattern,
flags: ecmaFlags,
options: {},
...(operation === "replace"
? { replacement: ecmaReplacement }
: {}),
},
{
flavour: "pcre2",
flavourVersion: PCRE2.defaultVersion,
pattern: patternModel === "shared" ? sharedPattern : pcrePattern,
flags: pcreFlags,
options: pcreOptionValues,
...(operation === "replace"
? { replacement: pcreReplacement }
: {}),
},
],
});
if (revision !== comparisonRevision.current) return;
setResult(comparison);
setStatus({
kind: "ready",
message:
comparison.status === "not-comparable"
? "Both side outcomes are retained; equivalence is not claimed."
: "Both exact requests completed.",
});
} catch (error) {
if (revision !== comparisonRevision.current) return;
setStatus({
kind: "error",
message: error instanceof Error ? error.message : String(error),
});
}
};
const cancel = () => {
comparisonRevision.current += 1;
orchestrator.current?.cancel();
setStatus({
kind: "idle",
message: "Comparison cancelled; both active workers were terminated.",
});
};
const generate = () => {
setGenerated(undefined);
setGeneratedStatus("");
try {
const program = generatePcre2C({
flavour: "pcre2",
flavourVersion: PCRE2.defaultVersion,
operation,
pattern: patternModel === "shared" ? sharedPattern : pcrePattern,
flags: pcreFlags,
options: pcreOptionValues,
subject,
scanAll,
...(operation === "replace" ? { replacement: pcreReplacement } : {}),
maximumMatches,
maximumCaptureRows,
maximumOutputBytes,
});
setGenerated(program);
setGeneratedStatus(
"Generated locally from the exact PCRE2 snapshot shown above.",
);
} catch (error) {
setGeneratedStatus(
error instanceof Error ? error.message : String(error),
);
}
};
const setPcreOption = (name: string, value: number) => {
setPcreOptions((current) => ({ ...current, [name]: value }));
};
return (
<section
className="panel comparison-code-panel"
aria-labelledby="comparison-code-heading"
>
<header className="panel-heading">
<div>
<p className="eyebrow">Evidence, not automatic translation</p>
<h2 id="comparison-code-heading">Compare &amp; generate</h2>
</div>
<button
type="button"
className="icon-button"
aria-label="Close compare and generate"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
</header>
<div className="comparison-tabs" role="tablist" aria-label="Tool">
<button
type="button"
role="tab"
aria-selected={tab === "compare"}
onClick={() => setTab("compare")}
>
Compare engines
</button>
<button
type="button"
role="tab"
aria-selected={tab === "code"}
onClick={() => setTab("code")}
>
PCRE2 C code
</button>
</div>
<div className="comparison-config">
<div className="comparison-config-row">
<label>
<span>Pattern model</span>
<select
value={patternModel}
onChange={(event) =>
setPatternModel(event.target.value as ComparisonPatternModel)
}
>
<option value="shared">Shared pattern · unchanged</option>
<option value="variants">Explicit per-flavour variants</option>
</select>
</label>
<label>
<span>Operation</span>
<select
value={operation}
onChange={(event) =>
setOperation(event.target.value as "match" | "replace")
}
>
<option value="match">Match</option>
<option value="replace">Replace</option>
</select>
</label>
<label className="check-control comparison-scan-control">
<input
type="checkbox"
checked={scanAll}
onChange={(event) => setScanAll(event.target.checked)}
/>
Scan all explicitly
</label>
</div>
{patternModel === "shared" ? (
<label className="comparison-text-field">
<span>Shared pattern sent unchanged to both engines</span>
<textarea
aria-label="Shared comparison pattern"
value={sharedPattern}
maxLength={DEFAULT_REGEX_LIMITS.patternHardLengthUtf16}
onChange={(event) => setSharedPattern(event.target.value)}
/>
</label>
) : (
<div className="comparison-variant-grid">
<label className="comparison-text-field">
<span>ECMAScript pattern variant</span>
<textarea
value={ecmaPattern}
maxLength={DEFAULT_REGEX_LIMITS.patternHardLengthUtf16}
onChange={(event) => setEcmaPattern(event.target.value)}
/>
</label>
<label className="comparison-text-field">
<span>PCRE2 pattern variant</span>
<textarea
value={pcrePattern}
maxLength={DEFAULT_REGEX_LIMITS.patternHardLengthUtf16}
onChange={(event) => setPcrePattern(event.target.value)}
/>
</label>
</div>
)}
<div className="comparison-variant-grid">
{(
[
["ecmascript", ECMASCRIPT, ecmaFlags, setEcmaFlags],
["pcre2", PCRE2, pcreFlags, setPcreFlags],
] as const
).map(([flavour, definition, selected, setSelected]) => (
<fieldset className="comparison-flag-selector" key={flavour}>
<legend>{definition.label} flags · exact</legend>
{definition.flags.map((flag) => (
<label key={flag.value} title={flag.label}>
<input
type="checkbox"
checked={selected.includes(flag.value)}
onChange={(event) =>
setSelected(
toggleFlag(
selected,
flag.value,
event.target.checked,
flavour,
),
)
}
/>
<code>{flag.value}</code>
<span>{flag.label}</span>
</label>
))}
</fieldset>
))}
</div>
<fieldset className="comparison-limit-grid">
<legend>PCRE2 native and host bounds</legend>
<label>
<span>Match steps</span>
<input
type="number"
value={pcreOptionValues.matchLimit}
min={1}
max={100_000_000}
onChange={(event) =>
setPcreOption("matchLimit", Number(event.target.value))
}
/>
</label>
<label>
<span>Depth</span>
<input
type="number"
value={pcreOptionValues.depthLimit}
min={1}
max={100_000}
onChange={(event) =>
setPcreOption("depthLimit", Number(event.target.value))
}
/>
</label>
<label>
<span>Heap KiB</span>
<input
type="number"
value={pcreOptionValues.heapLimitKib}
min={1}
max={131_072}
onChange={(event) =>
setPcreOption("heapLimitKib", Number(event.target.value))
}
/>
</label>
<label>
<span>Matches</span>
<input
type="number"
value={maximumMatches}
min={1}
max={DEFAULT_REGEX_LIMITS.maximumMatches}
onChange={(event) =>
setMaximumMatches(Number(event.target.value))
}
/>
</label>
<label>
<span>Capture rows</span>
<input
type="number"
value={maximumCaptureRows}
min={1}
max={DEFAULT_REGEX_LIMITS.maximumCaptureRows}
onChange={(event) =>
setMaximumCaptureRows(Number(event.target.value))
}
/>
</label>
<label>
<span>Worker ms</span>
<input
type="number"
value={timeoutMs}
min={1}
max={DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs}
onChange={(event) => setTimeoutMs(Number(event.target.value))}
/>
</label>
</fieldset>
{operation === "replace" ? (
<div className="comparison-variant-grid">
<label className="comparison-text-field">
<span>ECMAScript replacement · exact syntax</span>
<textarea
value={ecmaReplacement}
maxLength={DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16}
onChange={(event) => setEcmaReplacement(event.target.value)}
/>
</label>
<label className="comparison-text-field">
<span>PCRE2 replacement · exact syntax</span>
<textarea
value={pcreReplacement}
maxLength={DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16}
onChange={(event) => setPcreReplacement(event.target.value)}
/>
</label>
<label>
<span>Output byte cap</span>
<input
type="number"
value={maximumOutputBytes}
min={1}
max={DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes}
onChange={(event) =>
setMaximumOutputBytes(Number(event.target.value))
}
/>
</label>
</div>
) : null}
<label className="comparison-text-field comparison-subject">
<span>
Shared subject · {utf8ByteLength(subject).toLocaleString()} UTF-8
bytes
</span>
<textarea
aria-label="Comparison subject"
value={subject}
onChange={(event) => setSubject(event.target.value)}
/>
</label>
</div>
{tab === "compare" ? (
<div role="tabpanel" className="comparison-tab-panel">
<div className="comparison-actions">
<button
type="button"
className="primary-button"
disabled={status.kind === "running"}
onClick={() => void compare()}
>
{status.kind === "running" ? "Comparing…" : "Run comparison"}
</button>
{status.kind === "running" ? (
<button
type="button"
className="secondary-button"
onClick={cancel}
>
Cancel both workers
</button>
) : null}
<p className={`comparison-panel-status status-${status.kind}`}>
{status.message}
</p>
</div>
{result ? <ComparisonResultView result={result} /> : null}
</div>
) : (
<div role="tabpanel" className="comparison-tab-panel codegen-panel">
<div className="comparison-actions">
<button type="button" className="primary-button" onClick={generate}>
Generate reviewed C17
</button>
{generated ? (
<>
<button
type="button"
className="secondary-button"
onClick={() => {
void writeClipboardText(generated.source)
.then(() =>
setGeneratedStatus("Complete generated source copied."),
)
.catch((error: unknown) =>
setGeneratedStatus(
error instanceof Error
? error.message
: String(error),
),
);
}}
>
Copy complete source
</button>
<button
type="button"
className="secondary-button"
onClick={() => downloadSource(generated)}
>
Download .c
</button>
</>
) : null}
<p role="status">{generatedStatus}</p>
</div>
{generated ? (
<section className="generated-code-result">
<header>
<div>
<p className="eyebrow">
{generated.engineIdentity} · {generated.represents}
</p>
<h3>{generated.fileName}</h3>
</div>
<span className="provenance-badge">
Exact UTF-8 byte arrays
</span>
</header>
<p>
Compile: <code>{generated.compileCommand}</code>
</p>
<ul>
{generated.caveats.map((caveat) => (
<li key={caveat}>{caveat}</li>
))}
</ul>
<pre tabIndex={0}>
<code>{generated.source}</code>
</pre>
</section>
) : (
<p className="comparison-empty-state">
Generation consumes the exact PCRE2 pattern, flags, native limits,
subject and optional replacement above. It never powers the
browser runtime.
</p>
)}
</div>
)}
</section>
);
}

View File

@@ -0,0 +1,253 @@
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RegexExecutionRequest } from "../regex/model/match";
const engineHarness = vi.hoisted(() => ({
cancelCalls: 0,
disposeCalls: 0,
executeRequests: [] as RegexExecutionRequest[],
}));
vi.mock("../regex/execution/EngineSupervisor", () => ({
EngineSupervisor: class {
execute(request: RegexExecutionRequest) {
engineHarness.executeRequests.push(request);
return Promise.resolve({
accepted: true,
engine: {
flavour: "ecmascript",
adapterVersion: "test",
engineName: "Test engine",
engineVersion: "1",
runtimeVersion: "1",
offsetUnit: "utf16",
capabilities: {
compilation: true,
matching: true,
replacement: true,
namedCaptures: true,
captureHistory: false,
actualTrace: false,
benchmark: false,
},
},
flags: {
userFlags: "",
effectiveFlags: "g",
internallyAddedIndicesFlag: true,
internallyAddedGlobalFlag: true,
},
matches: [
{
matchNumber: 1,
value: "42",
valueStatus: "complete",
range: { startUtf16: 0, endUtf16: 2 },
nativeRange: { start: 0, end: 2, unit: "utf16" },
captures: [],
},
],
diagnostics: [],
elapsedMs: 2,
truncated: false,
});
}
replace(): Promise<never> {
return Promise.reject(new Error("Unexpected replacement call"));
}
cancel(): void {
engineHarness.cancelCalls += 1;
}
dispose(): void {
engineHarness.disposeCalls += 1;
}
},
}));
import { CorpusPanel } from "./CorpusPanel";
afterEach(() => {
cleanup();
engineHarness.cancelCalls = 0;
engineHarness.disposeCalls = 0;
engineHarness.executeRequests.length = 0;
});
function renderPanel(
overrides: Partial<Parameters<typeof CorpusPanel>[0]> = {},
) {
return render(
<CorpusPanel
active
flavour="ecmascript"
flavourVersion="2025"
options={{}}
pattern="\\d+"
flags={[]}
captureMetadata={[]}
patternAccepted
replacement="[$&]"
replacementAccepted
onReplacementChange={() => {}}
timeoutMs={2_000}
{...overrides}
/>,
);
}
describe("CorpusPanel", () => {
it("adds pasted text and produces an exhaustive per-document summary", async () => {
renderPanel();
await userEvent.type(
screen.getByRole("textbox", { name: "Pasted corpus text" }),
"42",
);
await userEvent.click(
screen.getByRole("button", { name: "Add pasted text" }),
);
expect(
screen.getByRole("list", { name: "Corpus documents" }),
).toHaveTextContent("pasted-text.txt");
await userEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
expect(
await screen.findByRole("table", { name: "Corpus document results" }),
).toHaveTextContent("complete");
expect(engineHarness.executeRequests).toHaveLength(1);
expect(engineHarness.executeRequests[0]).toMatchObject({
subject: "42",
scanAll: true,
});
expect(
screen.getByRole("button", { name: "Download summary JSON" }),
).toBeEnabled();
});
it("blocks invalid runs and clears results when configuration changes", async () => {
const rendered = renderPanel({ patternAccepted: false });
fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
expect(screen.getByRole("button", { name: "Scan corpus" })).toBeDisabled();
expect(screen.getByRole("alert")).toHaveTextContent(
"syntax provider accepts",
);
rendered.rerender(
<CorpusPanel
active
flavour="ecmascript"
flavourVersion="2025"
options={{}}
pattern="changed"
flags={[]}
captureMetadata={[]}
patternAccepted
replacement="[$&]"
replacementAccepted
onReplacementChange={() => {}}
timeoutMs={2_000}
/>,
);
expect(screen.getByRole("button", { name: "Scan corpus" })).toBeEnabled();
expect(screen.getByRole("status")).toHaveTextContent(
"previous corpus results were cleared",
);
});
it("cancels an active batch when the workspace is left", async () => {
let resolveExecution: ((value: never) => void) | undefined;
const pending = new Promise<never>((resolve) => {
resolveExecution = resolve;
});
const execute = vi
.spyOn(
(await import("../regex/execution/EngineSupervisor")).EngineSupervisor
.prototype,
"execute",
)
.mockReturnValue(pending);
const rendered = renderPanel();
fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
fireEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
expect(
screen.getByRole("button", { name: "Cancel corpus run" }),
).toBeEnabled();
rendered.rerender(
<CorpusPanel
active={false}
flavour="ecmascript"
flavourVersion="2025"
options={{}}
pattern="\\d+"
flags={[]}
captureMetadata={[]}
patternAccepted
replacement="[$&]"
replacementAccepted
onReplacementChange={() => {}}
timeoutMs={2_000}
/>,
);
expect(engineHarness.cancelCalls).toBeGreaterThan(0);
execute.mockRestore();
await act(async () => {
resolveExecution?.(undefined as never);
});
});
it("does not publish stale batch results after configuration changes", async () => {
let resolveExecution: ((value: never) => void) | undefined;
const pending = new Promise<never>((resolve) => {
resolveExecution = resolve;
});
const execute = vi
.spyOn(
(await import("../regex/execution/EngineSupervisor")).EngineSupervisor
.prototype,
"execute",
)
.mockReturnValue(pending);
const rendered = renderPanel();
fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
fireEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
rendered.rerender(
<CorpusPanel
active
flavour="ecmascript"
flavourVersion="2025"
options={{}}
pattern="changed"
flags={[]}
captureMetadata={[]}
patternAccepted
replacement="[$&]"
replacementAccepted
onReplacementChange={() => {}}
timeoutMs={2_000}
/>,
);
await act(async () => {
resolveExecution?.(undefined as never);
});
expect(
screen.queryByRole("table", { name: "Corpus document results" }),
).not.toBeInTheDocument();
expect(screen.getByRole("status")).toHaveTextContent(
"previous corpus results were cleared",
);
execute.mockRestore();
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,314 @@
.generation-dialog {
width: min(90rem, calc(100% - 2rem));
}
.generation-panel {
display: grid;
gap: 1rem;
min-width: 0;
max-height: calc(100vh - 2rem);
overflow: auto;
}
.generation-disclaimer,
.generation-unavailable,
.generation-status,
.generation-summary,
.generation-coverage,
.generation-cases,
.generation-discarded {
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.82);
background: color-mix(
in srgb,
var(--toolbox-surface) 94%,
var(--toolbox-accent) 6%
);
}
.generation-disclaimer {
margin: 0;
padding: 0.8rem 0.95rem;
color: var(--toolbox-muted);
}
.generation-unavailable {
display: grid;
gap: 0.35rem;
padding: 1rem;
}
.generation-unavailable span {
color: var(--toolbox-muted);
}
.generation-settings {
display: grid;
gap: 0.8rem;
padding: 1rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.82);
background: var(--toolbox-surface);
}
.generation-settings-primary,
.generation-settings-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.7rem;
}
.generation-settings label {
display: grid;
gap: 0.3rem;
min-width: 0;
color: var(--toolbox-muted);
font-size: 0.82rem;
font-weight: 700;
}
.generation-settings input {
min-width: 0;
width: 100%;
}
.generation-settings details {
border-top: 1px solid var(--toolbox-border);
padding-top: 0.65rem;
}
.generation-settings details summary,
.generation-discarded summary,
.generation-coverage details summary {
cursor: pointer;
font-weight: 750;
}
.generation-settings details[open] summary {
margin-bottom: 0.65rem;
}
.generation-run-actions,
.generation-case-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
}
.generation-run-actions small {
color: var(--toolbox-muted);
}
.generation-status {
display: grid;
gap: 0.4rem;
padding: 0.75rem 0.9rem;
}
.generation-status.is-error {
border-color: color-mix(
in srgb,
var(--toolbox-danger) 55%,
var(--toolbox-border)
);
}
.generation-status progress {
width: 100%;
}
.generation-status span {
color: var(--toolbox-muted);
font-size: 0.82rem;
}
.generation-results {
display: grid;
gap: 1rem;
}
.generation-summary,
.generation-coverage,
.generation-cases,
.generation-discarded {
padding: 1rem;
}
.generation-summary dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.55rem;
margin: 0;
}
.generation-summary dl > div {
min-width: 0;
padding: 0.65rem;
border-radius: calc(var(--toolbox-radius) * 0.68);
background: var(--toolbox-surface-soft);
}
.generation-summary dt {
color: var(--toolbox-muted);
font-size: 0.75rem;
font-weight: 750;
text-transform: uppercase;
}
.generation-summary dd {
overflow-wrap: anywhere;
margin: 0.2rem 0 0;
}
.generation-warnings {
margin: 0.75rem 0 0;
color: var(--toolbox-muted);
}
.generation-coverage > header,
.generation-cases > header {
display: flex;
justify-content: space-between;
align-items: start;
gap: 1rem;
margin-bottom: 0.75rem;
}
.generation-coverage h3,
.generation-cases h3 {
margin: 0;
}
.generation-coverage > ul,
.unsupported-generation-list,
.generated-case-list,
.generation-discarded ul {
display: grid;
gap: 0.55rem;
margin: 0;
padding: 0;
list-style: none;
}
.generation-coverage > ul > li {
display: grid;
grid-template-columns: 7.5rem minmax(9rem, 0.45fr) minmax(0, 1fr);
align-items: start;
gap: 0.55rem;
padding: 0.6rem;
border-radius: calc(var(--toolbox-radius) * 0.68);
background: var(--toolbox-surface-soft);
}
.generation-coverage .status {
justify-self: start;
}
.generation-coverage details {
margin-top: 0.8rem;
}
.unsupported-generation-list {
margin-top: 0.65rem;
}
.unsupported-generation-list li,
.generation-discarded li {
display: grid;
grid-template-columns: minmax(10rem, 0.35fr) minmax(8rem, 0.25fr) minmax(
0,
1fr
);
gap: 0.6rem;
align-items: start;
padding: 0.55rem;
border-radius: calc(var(--toolbox-radius) * 0.68);
background: var(--toolbox-surface-soft);
}
.unsupported-generation-list code,
.generation-discarded code {
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.generated-case-list li {
display: grid;
gap: 0.5rem;
min-width: 0;
padding: 0.7rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.68);
background: var(--toolbox-surface-soft);
}
.generated-case-list label {
display: flex;
align-items: start;
gap: 0.55rem;
}
.generated-case-list label span {
display: grid;
gap: 0.15rem;
}
.generated-case-list small {
color: var(--toolbox-muted);
}
.generated-case-list > li > code {
display: block;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.generation-feature-chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.generation-feature-chips span {
padding: 0.2rem 0.45rem;
border: 1px solid var(--toolbox-border);
border-radius: 999px;
color: var(--toolbox-muted);
font-size: 0.72rem;
}
.generation-discarded > p {
color: var(--toolbox-muted);
}
.generation-discarded[open] summary {
margin-bottom: 0.5rem;
}
@media (max-width: 900px) {
.generation-settings-primary,
.generation-settings-grid,
.generation-summary dl {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.generation-coverage > ul > li,
.unsupported-generation-list li,
.generation-discarded li {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.generation-settings-primary,
.generation-settings-grid,
.generation-summary dl {
grid-template-columns: 1fr;
}
.generation-coverage > header,
.generation-cases > header {
display: grid;
}
}

View File

@@ -0,0 +1,326 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CaseGenerationClient } from "../regex/generation/CaseGenerationOrchestrator";
import type {
CaseGenerationResult,
VerifiedGeneratedCase,
} from "../regex/generation/generation.types";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { EcmaScriptSyntaxProvider } from "../regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { GenerationPanel } from "./GenerationPanel";
const provider = new EcmaScriptSyntaxProvider();
function generatedCase(
id: string,
expectation: "should-match" | "should-not-match",
): VerifiedGeneratedCase {
const subject = expectation === "should-match" ? "cat" : "dog";
return {
id,
name: `Generated ${id}`,
subject,
subjectBytes: subject.length,
expectation,
features:
expectation === "should-match" ? ["shortest"] : ["likely-near-miss"],
notes: [],
elapsedMs: 0.5,
matched: expectation === "should-match",
matchCount: expectation === "should-match" ? 1 : 0,
provenance: {
kind: "generated",
generatorId: "regex-tools-ast-cases",
generatorVersion: "1",
seed: "case-seed-1",
candidateId: id,
intendedOutcome: expectation === "should-match" ? "match" : "no-match",
},
};
}
function fixtureResult(): CaseGenerationResult {
return {
status: "partial",
seed: "case-seed-1",
seedHash: "1234abcd",
flavour: "ecmascript",
cases: [
generatedCase("positive", "should-match"),
generatedCase("negative", "should-not-match"),
],
discarded: [
{
candidateId: "discarded",
intendedOutcome: "no-match",
reason: "unexpected-match",
detail: "The actual engine matched it.",
subjectBytes: 3,
subjectPreview: "cat",
},
],
coverage: [
{
feature: "shortest",
status: "covered",
detail: "Shortest path requested.",
relevantNodes: 1,
},
{
feature: "alternative-coverage",
status: "not-applicable",
detail: "No alternatives.",
relevantNodes: 0,
},
],
unsupportedConstructs: [
{
id: "unsupported-lookahead",
kind: "lookahead",
range: { startUtf16: 0, endUtf16: 3 },
rawPreview: "(?=cat)",
reason: "Lookaround is verified but not synthesized.",
},
],
engine: {
flavour: "ecmascript",
adapterVersion: "fixture",
engineName: "Native ECMAScript RegExp",
engineVersion: "Fixture Browser 1",
runtimeVersion: "Fixture Browser 1",
offsetUnit: "utf16",
capabilities: {
compilation: true,
matching: true,
replacement: true,
namedCaptures: true,
captureHistory: false,
actualTrace: false,
benchmark: true,
},
},
candidateAttempts: 5,
engineExecutions: 3,
generatedCandidateBytes: 9,
verifiedSubjectBytes: 6,
wallTimeMs: 4.5,
warnings: [],
};
}
function client(
overrides: Partial<CaseGenerationClient> = {},
): CaseGenerationClient {
return {
run: vi.fn().mockImplementation(async (_input, onProgress) => {
onProgress?.({
phase: "verifying",
completed: 1,
total: 3,
retained: 1,
message: "Verifying fixture candidate.",
});
return fixtureResult();
}),
cancel: vi.fn(),
dispose: vi.fn(),
...overrides,
};
}
async function renderPanel(
overrides: Partial<Parameters<typeof GenerationPanel>[0]> = {},
) {
const pattern = overrides.pattern ?? "cat";
const syntax =
overrides.syntax ??
(await provider.parsePattern({
flavour: "ecmascript",
flavourVersion: "2025",
pattern,
flags: [],
options: {},
}));
const currentClient = client();
const rendered = render(
<GenerationPanel
active
flavour="ecmascript"
flavourVersion="2025"
pattern={pattern}
flags={[]}
options={{}}
scanAll={false}
syntax={syntax}
createOrchestrator={() => currentClient}
{...overrides}
/>,
);
return { ...rendered, currentClient, syntax };
}
afterEach(() => {
cleanup();
});
describe("GenerationPanel", () => {
it("runs deterministic generation, renders honest coverage and adds verified cases to tests", async () => {
const onAddToTests = vi.fn();
const onSelectPatternRange = vi.fn();
const { currentClient } = await renderPanel({
onAddToTests,
onSelectPatternRange,
});
await userEvent.click(
screen.getByRole("button", { name: "Generate & verify" }),
);
expect(
await screen.findByText(/Native ECMAScript RegExp · Fixture Browser 1/u),
).toBeVisible();
expect(screen.getByText("case-seed-1")).toBeVisible();
expect(
screen.getByRole("region", { name: "Generation summary" }),
).toHaveTextContent("1234abcd");
expect(screen.getByText("Shortest path requested.")).toBeVisible();
expect(screen.getAllByText("actual engine verified")).toHaveLength(2);
await userEvent.click(
screen.getByText(/1 unsupported or partially synthesized construct/u),
);
expect(currentClient.run).toHaveBeenCalledWith(
expect.objectContaining({
seed: "case-seed-1",
flavour: "ecmascript",
pattern: "cat",
}),
expect.any(Function),
);
await userEvent.click(
screen.getByRole("button", {
name: /lookahead · 03/u,
}),
);
expect(onSelectPatternRange).toHaveBeenCalledWith({
startUtf16: 0,
endUtf16: 3,
});
await userEvent.click(
screen.getByText(/Advanced: 1 rejected candidate diagnostic/u),
);
expect(screen.getByText(/unexpected match/u)).toBeVisible();
await userEvent.click(
screen.getByRole("button", { name: "Add selected to tests" }),
);
expect(onAddToTests).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
expectation: "should-match",
provenance: expect.objectContaining({ seed: "case-seed-1" }),
}),
expect.objectContaining({ expectation: "should-not-match" }),
]),
);
expect(
screen.getByText(/Added 2 generated cases to the unit-test suite/u),
).toBeVisible();
});
it("shows strict setting errors without starting worker execution", async () => {
const { currentClient } = await renderPanel();
const maximumCases = screen.getByRole("spinbutton", {
name: "Retain at most",
});
await userEvent.clear(maximumCases);
await userEvent.type(maximumCases, "500");
await userEvent.click(screen.getByText("Advanced generation bounds"));
const attempts = screen.getByRole("spinbutton", {
name: "Candidate attempts",
});
await userEvent.clear(attempts);
await userEvent.type(attempts, "100");
await userEvent.click(
screen.getByRole("button", { name: "Generate & verify" }),
);
expect(
await screen.findByText(/Maximum candidate attempts must be/u),
).toBeVisible();
expect(currentClient.run).not.toHaveBeenCalled();
});
it("does not expose the ECMAScript generator for PCRE2", async () => {
const { currentClient } = await renderPanel({ flavour: "pcre2" });
expect(
screen.getByText(
/current PCRE2 syntax provider is intentionally partial/u,
),
).toBeVisible();
expect(
screen.queryByRole("button", { name: "Generate & verify" }),
).not.toBeInTheDocument();
expect(currentClient.run).not.toHaveBeenCalled();
});
it("clears verified cases immediately when the pattern changes", async () => {
const rendered = await renderPanel();
await userEvent.click(
screen.getByRole("button", { name: "Generate & verify" }),
);
expect(await screen.findByText("Generated positive")).toBeVisible();
rendered.rerender(
<GenerationPanel
active
flavour="ecmascript"
flavourVersion="2025"
pattern="changed"
flags={[]}
options={{}}
scanAll={false}
syntax={rendered.syntax}
createOrchestrator={() => rendered.currentClient}
/>,
);
expect(screen.queryByText("Generated positive")).not.toBeInTheDocument();
expect(
await screen.findByText(/previous generated cases were cleared/u),
).toBeVisible();
});
it("terminates synthesis and engine workers on cancellation", async () => {
let rejectRun: ((error: Error) => void) | undefined;
const pending = new Promise<CaseGenerationResult>((_resolve, reject) => {
rejectRun = reject;
});
const currentClient = client({
run: vi.fn().mockReturnValue(pending),
cancel: vi.fn().mockImplementation(() => {
rejectRun?.(
new WorkerRequestError("cancelled", "fixture cancellation"),
);
}),
});
await renderPanel({ createOrchestrator: () => currentClient });
await userEvent.click(
screen.getByRole("button", { name: "Generate & verify" }),
);
await userEvent.click(
await screen.findByRole("button", { name: "Cancel generation" }),
);
await waitFor(() => expect(currentClient.cancel).toHaveBeenCalled());
expect(
await screen.findByText(
/Generation cancelled; its workers were terminated/u,
),
).toBeVisible();
expect(currentClient.dispose).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,829 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
CaseGenerationOrchestrator,
type CaseGenerationClient,
} from "../regex/generation/CaseGenerationOrchestrator";
import {
DEFAULT_GENERATION_SETTINGS,
GENERATION_LIMITS,
} from "../regex/generation/generation-limits";
import type {
CaseGenerationProgress,
CaseGenerationResult,
CandidateGenerationSettings,
VerifiedGeneratedCase,
} from "../regex/generation/generation.types";
import {
GENERATED_CASE_GENERATOR_ID,
GENERATED_CASE_GENERATOR_VERSION,
} from "../regex/generation/generation.types";
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import type {
RegexEngineOptions,
RegexFlavourId,
} from "../regex/model/flavour";
import type { RegexSyntaxResult, SourceRange } from "../regex/model/syntax";
import "./GenerationPanel.css";
const MAXIMUM_SUBJECT_PREVIEW_UTF16 = 240;
function defaultOrchestrator(): CaseGenerationClient {
return new CaseGenerationOrchestrator();
}
function integer(
source: string,
label: string,
minimum: number,
maximum: number,
): number {
const value = Number(source);
if (
source.trim() === "" ||
!Number.isSafeInteger(value) ||
value < minimum ||
value > maximum
) {
throw new RangeError(
`${label} must be a whole number from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
);
}
return value;
}
function subjectPreview(subject: string): string {
if (subject.length <= MAXIMUM_SUBJECT_PREVIEW_UTF16) {
return JSON.stringify(subject);
}
return `${JSON.stringify(subject.slice(0, MAXIMUM_SUBJECT_PREVIEW_UTF16))}… (${subject.length.toLocaleString()} UTF-16 units)`;
}
function downloadCases(
result: CaseGenerationResult,
configuration: {
readonly flavour: RegexFlavourId;
readonly flavourVersion?: string;
readonly pattern: string;
readonly flags: readonly string[];
readonly options: RegexEngineOptions;
readonly scanAll: boolean;
},
): void {
const payload = {
schemaVersion: 1,
kind: "regex-tools-generated-cases",
generator: {
id: GENERATED_CASE_GENERATOR_ID,
version: GENERATED_CASE_GENERATOR_VERSION,
seed: result.seed,
seedHash: result.seedHash,
},
configuration,
engine: result.engine,
cases: result.cases.map((candidate) => ({
id: candidate.id,
name: candidate.name,
subject: candidate.subject,
expectation: { kind: candidate.expectation },
features: candidate.features,
provenance: candidate.provenance,
})),
};
const url = URL.createObjectURL(
new Blob([`${JSON.stringify(payload, null, 2)}\n`], {
type: "application/json",
}),
);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `regex-generated-${result.seedHash}.json`;
anchor.click();
globalThis.setTimeout(() => URL.revokeObjectURL(url), 1_000);
}
export interface GenerationPanelProps {
readonly active: boolean;
readonly flavour: RegexFlavourId;
readonly flavourVersion?: string;
readonly pattern: string;
readonly flags: readonly string[];
readonly options: RegexEngineOptions;
readonly scanAll: boolean;
readonly syntax?: RegexSyntaxResult;
readonly onClose?: () => void;
readonly onAddToTests?: (cases: readonly VerifiedGeneratedCase[]) => void;
readonly onSelectPatternRange?: (range: SourceRange) => void;
readonly createOrchestrator?: () => CaseGenerationClient;
}
export function GenerationPanel({
active,
flavour,
flavourVersion,
pattern,
flags,
options,
scanAll,
syntax,
onClose,
onAddToTests,
onSelectPatternRange,
createOrchestrator = defaultOrchestrator,
}: GenerationPanelProps) {
const [seed, setSeed] = useState("case-seed-1");
const [maximumCases, setMaximumCases] = useState(
String(DEFAULT_GENERATION_SETTINGS.maximumCases),
);
const [maximumCandidateAttempts, setMaximumCandidateAttempts] = useState(
String(DEFAULT_GENERATION_SETTINGS.maximumCandidateAttempts),
);
const [randomVariantCount, setRandomVariantCount] = useState(
String(DEFAULT_GENERATION_SETTINGS.randomVariantCount),
);
const [maximumQuantifierRepetitions, setMaximumQuantifierRepetitions] =
useState(String(DEFAULT_GENERATION_SETTINGS.maximumQuantifierRepetitions));
const [maximumSubjectBytes, setMaximumSubjectBytes] = useState(
String(DEFAULT_GENERATION_SETTINGS.maximumSubjectBytes),
);
const [maximumTotalSubjectBytes, setMaximumTotalSubjectBytes] = useState(
String(DEFAULT_GENERATION_SETTINGS.maximumTotalSubjectBytes),
);
const [perCaseTimeoutMs, setPerCaseTimeoutMs] = useState(
String(DEFAULT_GENERATION_SETTINGS.perCaseTimeoutMs),
);
const [maximumWallTimeMs, setMaximumWallTimeMs] = useState(
String(DEFAULT_GENERATION_SETTINGS.maximumWallTimeMs),
);
const [running, setRunning] = useState(false);
const [progress, setProgress] = useState<CaseGenerationProgress>();
const [message, setMessage] = useState(
"No cases have been generated for this configuration.",
);
const [error, setError] = useState<string>();
const [resultRecord, setResultRecord] = useState<{
readonly configurationKey: symbol;
readonly result: CaseGenerationResult;
}>();
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(
new Set(),
);
const orchestrator = useRef<CaseGenerationClient | undefined>(undefined);
const runRevision = useRef(0);
const syntaxCurrent =
syntax?.accepted === true &&
syntax.root.raw === pattern &&
syntax.root.support.flavour === flavour;
const generationAvailable = flavour === "ecmascript" && syntaxCurrent;
const configurationKey = useMemo(
() =>
Symbol(
`generation-${flavour}-${flavourVersion ?? ""}-${flags.join("")}-${JSON.stringify(options)}-${pattern.length}-${String(scanAll)}`,
),
[flags, flavour, flavourVersion, options, pattern, scanAll],
);
const previousConfiguration = useRef(configurationKey);
const result =
resultRecord?.configurationKey === configurationKey
? resultRecord.result
: undefined;
const disposeActive = useCallback(() => {
orchestrator.current?.cancel();
orchestrator.current?.dispose();
orchestrator.current = undefined;
}, []);
useEffect(() => {
if (active) return;
runRevision.current += 1;
disposeActive();
queueMicrotask(() => setRunning(false));
}, [active, disposeActive]);
useEffect(() => {
if (previousConfiguration.current === configurationKey) return;
previousConfiguration.current = configurationKey;
const revision = ++runRevision.current;
disposeActive();
queueMicrotask(() => {
if (runRevision.current !== revision) return;
setRunning(false);
setProgress(undefined);
setResultRecord(undefined);
setSelectedIds(new Set());
setError(undefined);
setMessage(
"Pattern or engine configuration changed; previous generated cases were cleared.",
);
});
}, [configurationKey, disposeActive]);
useEffect(
() => () => {
runRevision.current += 1;
disposeActive();
},
[disposeActive],
);
const settings = (): CandidateGenerationSettings => {
const retained = integer(
maximumCases,
"Maximum retained cases",
1,
DEFAULT_REGEX_LIMITS.maximumGeneratedCases,
);
return {
maximumCases: retained,
maximumCandidateAttempts: integer(
maximumCandidateAttempts,
"Maximum candidate attempts",
retained,
GENERATION_LIMITS.maximumCandidateAttempts,
),
maximumTotalSubjectBytes: integer(
maximumTotalSubjectBytes,
"Maximum aggregate subject bytes",
1,
GENERATION_LIMITS.maximumTotalSubjectBytes,
),
maximumSubjectBytes: integer(
maximumSubjectBytes,
"Maximum bytes per subject",
1,
GENERATION_LIMITS.maximumSubjectBytes,
),
maximumAstNodes: DEFAULT_GENERATION_SETTINGS.maximumAstNodes,
maximumAstDepth: DEFAULT_GENERATION_SETTINGS.maximumAstDepth,
maximumQuantifierRepetitions: integer(
maximumQuantifierRepetitions,
"Maximum synthesized repetitions",
1,
GENERATION_LIMITS.maximumQuantifierRepetitions,
),
randomVariantCount: integer(
randomVariantCount,
"Random variants",
0,
DEFAULT_REGEX_LIMITS.maximumGeneratedCases,
),
perCaseTimeoutMs: integer(
perCaseTimeoutMs,
"Per-case timeout",
GENERATION_LIMITS.minimumPerCaseTimeoutMs,
GENERATION_LIMITS.maximumPerCaseTimeoutMs,
),
maximumWallTimeMs: integer(
maximumWallTimeMs,
"Aggregate wall time",
1,
GENERATION_LIMITS.maximumWallTimeMs,
),
};
};
const generate = async () => {
if (!generationAvailable || !syntax) return;
const revision = ++runRevision.current;
disposeActive();
const client = createOrchestrator();
orchestrator.current = client;
setRunning(true);
setProgress(undefined);
setError(undefined);
setResultRecord(undefined);
setSelectedIds(new Set());
setMessage(
"Starting deterministic synthesis and actual-engine verification…",
);
try {
const next = await client.run(
{
flavour,
...(flavourVersion === undefined ? {} : { flavourVersion }),
pattern,
flags,
options,
scanAll,
root: syntax.root,
captureMetadata: syntax.captures,
syntaxAccepted: syntax.accepted,
seed,
settings: settings(),
},
(value) => {
if (runRevision.current !== revision) return;
setProgress(value);
setMessage(value.message);
},
);
if (runRevision.current !== revision) return;
setResultRecord({ configurationKey, result: next });
setSelectedIds(new Set(next.cases.map((candidate) => candidate.id)));
setMessage(
next.stoppedReason ??
`Generation ${next.status}: ${next.cases.length.toLocaleString()} actual-engine-verified cases retained.`,
);
} catch (cause) {
if (runRevision.current !== revision) return;
if (cause instanceof WorkerRequestError && cause.kind === "cancelled") {
setMessage("Generation cancelled; its workers were terminated.");
} else {
const detail =
cause instanceof Error
? cause.message
: "Generated cases could not be produced.";
setError(detail);
setMessage("Generation settings or worker execution require review.");
}
} finally {
client.dispose();
if (orchestrator.current === client) orchestrator.current = undefined;
if (runRevision.current === revision) {
setRunning(false);
setProgress(undefined);
}
}
};
const cancel = () => {
orchestrator.current?.cancel();
setMessage(
"Cancelling synthesis and verification; active workers are being terminated…",
);
};
const selectedCases =
result?.cases.filter((candidate) => selectedIds.has(candidate.id)) ?? [];
const progressMaximum = Math.max(1, progress?.total ?? 1);
const progressValue = Math.min(progressMaximum, progress?.completed ?? 0);
return (
<section
className="panel generation-panel"
aria-labelledby="generation-heading"
>
<header className="panel-heading">
<div>
<p className="eyebrow">
Normalized AST candidates · actual-engine truth
</p>
<h2 id="generation-heading">Generated test cases</h2>
</div>
<div className="panel-heading-actions">
<span className="provenance-badge">
{generationAvailable
? "ECMAScript generator v1"
: "Coverage unavailable"}
</span>
{onClose ? (
<button
type="button"
className="icon-button"
aria-label="Close generated test cases"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
) : null}
</div>
</header>
<p className="generation-disclaimer">
Generation samples supported structure; it is not proof of complete
coverage. A positive or negative label appears only after the selected
actual engine confirms that exact subject. Rejected candidates remain
diagnostics, never tests.
</p>
{!generationAvailable ? (
<div className="generation-unavailable" role="status">
<strong>
Generated cases are unavailable for this configuration.
</strong>
<span>
{flavour === "pcre2"
? "The current PCRE2 syntax provider is intentionally partial and does not expose a complete normalized AST. It is not reinterpreted as ECMAScript."
: syntaxCurrent
? "This flavour does not have an implemented generator."
: "Wait for an accepted, current syntax result before generating."}
</span>
</div>
) : (
<form
className="generation-settings"
onSubmit={(event) => {
event.preventDefault();
void generate();
}}
>
<div className="generation-settings-primary">
<label>
<span>Deterministic seed</span>
<input
value={seed}
maxLength={GENERATION_LIMITS.maximumSeedUtf16}
disabled={running}
required
onChange={(event) => setSeed(event.target.value)}
/>
</label>
<label>
<span>Retain at most</span>
<input
type="number"
min={1}
max={DEFAULT_REGEX_LIMITS.maximumGeneratedCases}
value={maximumCases}
disabled={running}
onChange={(event) => setMaximumCases(event.target.value)}
/>
</label>
<label>
<span>Seeded variants</span>
<input
type="number"
min={0}
max={DEFAULT_REGEX_LIMITS.maximumGeneratedCases}
value={randomVariantCount}
disabled={running}
onChange={(event) => setRandomVariantCount(event.target.value)}
/>
</label>
</div>
<details>
<summary>Advanced generation bounds</summary>
<div className="generation-settings-grid">
<label>
<span>Candidate attempts</span>
<input
type="number"
min={1}
max={GENERATION_LIMITS.maximumCandidateAttempts}
value={maximumCandidateAttempts}
disabled={running}
onChange={(event) =>
setMaximumCandidateAttempts(event.target.value)
}
/>
</label>
<label>
<span>Per-case timeout (ms)</span>
<input
type="number"
min={GENERATION_LIMITS.minimumPerCaseTimeoutMs}
max={GENERATION_LIMITS.maximumPerCaseTimeoutMs}
value={perCaseTimeoutMs}
disabled={running}
onChange={(event) => setPerCaseTimeoutMs(event.target.value)}
/>
</label>
<label>
<span>Aggregate wall time (ms)</span>
<input
type="number"
min={1}
max={GENERATION_LIMITS.maximumWallTimeMs}
value={maximumWallTimeMs}
disabled={running}
onChange={(event) => setMaximumWallTimeMs(event.target.value)}
/>
</label>
<label>
<span>Bytes per subject</span>
<input
type="number"
min={1}
max={GENERATION_LIMITS.maximumSubjectBytes}
value={maximumSubjectBytes}
disabled={running}
onChange={(event) =>
setMaximumSubjectBytes(event.target.value)
}
/>
</label>
<label>
<span>Aggregate subject bytes</span>
<input
type="number"
min={1}
max={GENERATION_LIMITS.maximumTotalSubjectBytes}
value={maximumTotalSubjectBytes}
disabled={running}
onChange={(event) =>
setMaximumTotalSubjectBytes(event.target.value)
}
/>
</label>
<label>
<span>Synthesized repetitions</span>
<input
type="number"
min={1}
max={GENERATION_LIMITS.maximumQuantifierRepetitions}
value={maximumQuantifierRepetitions}
disabled={running}
onChange={(event) =>
setMaximumQuantifierRepetitions(event.target.value)
}
/>
</label>
</div>
</details>
<div className="generation-run-actions">
{running ? (
<button type="button" className="primary-button" onClick={cancel}>
Cancel generation
</button>
) : (
<button type="submit" className="primary-button">
Generate &amp; verify
</button>
)}
<small>
Hard caps:{" "}
{DEFAULT_REGEX_LIMITS.maximumGeneratedCases.toLocaleString()}{" "}
retained cases ·{" "}
{GENERATION_LIMITS.maximumTotalSubjectBytes / 1024 / 1024} MiB
candidate text · {GENERATION_LIMITS.maximumWallTimeMs / 1_000} s
wall time
</small>
</div>
</form>
)}
<div
className={`generation-status ${error ? "is-error" : ""}`}
role={error ? "alert" : "status"}
aria-live="polite"
>
<strong>{message}</strong>
{progress ? (
<>
<progress
aria-label="Generated-case progress"
max={progressMaximum}
value={progressValue}
/>
<span>
{progress.phase} · {progress.completed.toLocaleString()} /{" "}
{progress.total.toLocaleString()} ·{" "}
{progress.retained.toLocaleString()} retained
</span>
</>
) : null}
{error ? <span>{error}</span> : null}
</div>
{result ? (
<div className="generation-results">
<section
className="generation-summary"
aria-label="Generation summary"
>
<dl>
<div>
<dt>Status</dt>
<dd>{result.status.replaceAll("-", " ")}</dd>
</div>
<div>
<dt>Seed</dt>
<dd>
<code>{result.seed}</code> · {result.seedHash}
</dd>
</div>
<div>
<dt>Actual engine</dt>
<dd>
{result.engine
? `${result.engine.engineName} · ${result.engine.engineVersion}`
: "No candidate reached engine verification"}
</dd>
</div>
<div>
<dt>Verified cases</dt>
<dd>{result.cases.length.toLocaleString()}</dd>
</div>
<div>
<dt>Engine executions</dt>
<dd>{result.engineExecutions.toLocaleString()}</dd>
</div>
<div>
<dt>Candidate bytes</dt>
<dd>{result.generatedCandidateBytes.toLocaleString()}</dd>
</div>
<div>
<dt>Wall time</dt>
<dd>{result.wallTimeMs.toFixed(1)} ms</dd>
</div>
</dl>
{result.warnings.length > 0 ? (
<ul className="generation-warnings">
{result.warnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
) : null}
</section>
<section
className="generation-coverage"
aria-labelledby="generation-coverage-heading"
>
<header>
<div>
<p className="eyebrow">Claimed only for generator v1</p>
<h3 id="generation-coverage-heading">Coverage report</h3>
</div>
</header>
<ul>
{result.coverage.map((entry) => (
<li key={entry.feature}>
<span className={`status status-${entry.status}`}>
{entry.status}
</span>
<strong>{entry.feature.replaceAll("-", " ")}</strong>
<span>{entry.detail}</span>
</li>
))}
</ul>
{result.unsupportedConstructs.length > 0 ? (
<details>
<summary>
{result.unsupportedConstructs.length.toLocaleString()}{" "}
unsupported or partially synthesized construct(s)
</summary>
<ul className="unsupported-generation-list">
{result.unsupportedConstructs.map((construct) => (
<li key={construct.id}>
<button
type="button"
className="text-button"
disabled={!onSelectPatternRange}
onClick={() => onSelectPatternRange?.(construct.range)}
>
{construct.kind} · {construct.range.startUtf16}
{construct.range.endUtf16}
</button>
<code>{construct.rawPreview || "empty"}</code>
<span>{construct.reason}</span>
</li>
))}
</ul>
</details>
) : null}
</section>
<section
className="generation-cases"
aria-labelledby="generated-cases-heading"
>
<header>
<div>
<p className="eyebrow">Exact verified subjects</p>
<h3 id="generated-cases-heading">
Retained cases{" "}
<small>({result.cases.length.toLocaleString()})</small>
</h3>
</div>
<div className="generation-case-actions">
<button
type="button"
className="secondary-button"
disabled={result.cases.length === 0}
onClick={() =>
setSelectedIds(
selectedIds.size === result.cases.length
? new Set()
: new Set(
result.cases.map((candidate) => candidate.id),
),
)
}
>
{selectedIds.size === result.cases.length
? "Select none"
: "Select all"}
</button>
<button
type="button"
className="secondary-button"
disabled={result.cases.length === 0}
onClick={() =>
downloadCases(result, {
flavour,
...(flavourVersion === undefined
? {}
: { flavourVersion }),
pattern,
flags,
options,
scanAll,
})
}
>
Export verified JSON
</button>
{onAddToTests ? (
<button
type="button"
className="primary-button"
disabled={selectedCases.length === 0}
onClick={() => {
try {
onAddToTests(selectedCases);
setError(undefined);
setMessage(
`Added ${selectedCases.length.toLocaleString()} generated cases to the unit-test suite with seed provenance.`,
);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: "Generated cases could not be added.",
);
}
}}
>
Add selected to tests
</button>
) : null}
</div>
</header>
{result.cases.length === 0 ? (
<p className="empty-state">
No candidate satisfied its intended property within the
configured bounds.
</p>
) : (
<ul className="generated-case-list">
{result.cases.map((candidate) => (
<li key={candidate.id}>
<label>
<input
type="checkbox"
aria-label={`Select ${candidate.name}`}
checked={selectedIds.has(candidate.id)}
onChange={(event) => {
const checked = event.target.checked;
setSelectedIds((current) => {
const next = new Set(current);
if (checked) next.add(candidate.id);
else next.delete(candidate.id);
return next;
});
}}
/>
<span>
<strong>{candidate.name}</strong>
<small>
{candidate.expectation.replaceAll("-", " ")} ·{" "}
{candidate.subjectBytes.toLocaleString()} bytes ·{" "}
{candidate.elapsedMs.toFixed(2)} ms
</small>
</span>
</label>
<code>{subjectPreview(candidate.subject)}</code>
<div className="generation-feature-chips">
{candidate.features.map((feature) => (
<span key={feature}>
{feature.replaceAll("-", " ")}
</span>
))}
<span>actual engine verified</span>
</div>
</li>
))}
</ul>
)}
</section>
{result.discarded.length > 0 ? (
<details className="generation-discarded">
<summary>
Advanced: {result.discarded.length.toLocaleString()} rejected
candidate diagnostic(s)
</summary>
<p>
These candidates are not tests. Subjects are clipped here; the
diagnostic records only why each intended property failed.
</p>
<ul>
{result.discarded.map((candidate) => (
<li key={`${candidate.candidateId}-${candidate.reason}`}>
<strong>{candidate.reason.replaceAll("-", " ")}</strong>
<code>{subjectPreview(candidate.subjectPreview)}</code>
<span>{candidate.detail}</span>
</li>
))}
</ul>
</details>
) : null}
</div>
) : null}
</section>
);
}

View File

@@ -1,5 +1,9 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits"; import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
import {
DEFAULT_GENERATION_SETTINGS,
GENERATION_LIMITS,
} from "../regex/generation/generation-limits";
import { manifest } from "../toolbox/manifest"; import { manifest } from "../toolbox/manifest";
const releaseSourceUrl = `https://git.add-ideas.de/zemion/regex-tools/src/tag/v${manifest.version}`; const releaseSourceUrl = `https://git.add-ideas.de/zemion/regex-tools/src/tag/v${manifest.version}`;
@@ -43,10 +47,19 @@ export function HelpDialog({
<section> <section>
<h3>What runs where</h3> <h3>What runs where</h3>
<p> <p>
Syntax parsing and actual ECMAScript execution run in separate, Syntax parsing and active-engine execution run in separate, killable
killable browser workers. Patterns, subjects and projects are never browser workers. ECMAScript uses the browser RegExp implementation;
uploaded. There is no backend, account, telemetry, CDN dependency, PCRE2 uses the bundled 10.47 WebAssembly engine; Python uses CPython
or executable replacement function. 3.14 through Pyodide; and Java uses TeaVM&apos;s java.util.regex
compatibility implementation. The result metadata names the runtime
actually used. Patterns, subjects and projects are never uploaded.
There is no backend, account, telemetry, CDN dependency, or
executable replacement function.
</p>
<p>
TeaVM 0.15.0 is not OpenJDK. Its replacement implementation accepts
single-digit $n references, so $12 means $1 followed by literal 2.
Java SE ${"{name}"} references are reported as unavailable.
</p> </p>
</section> </section>
<section> <section>
@@ -56,8 +69,47 @@ export function HelpDialog({
syntax provider. The extraction tree contains actual browser-engine syntax provider. The extraction tree contains actual browser-engine
matches and captures. Replacement mode adds a typed token tree and matches and captures. Replacement mode adds a typed token tree and
bounded per-match contribution/range mapping over those actual bounded per-match contribution/range mapping over those actual
matches. None is an internal V8, SpiderMonkey or JavaScriptCore matches. These views are not internal engine traces. The separate
execution trace. PCRE trace viewer shows actual bounded automatic callouts reported
by PCRE2; its adjacent-position movement labels are derived and it
is not a complete record of every internal action. Browser
ECMAScript exposes no corresponding V8, SpiderMonkey or
JavaScriptCore trace API.
</p>
</section>
<section>
<h3>Advisory analysis</h3>
<p>
Static risk findings are ECMAScript-2025-specific heuristics, and
generated-input growth observations describe only the selected
bounded samples. Neither proves safety or general complexity. Growth
and cold/warm benchmark timing run in a separate killable worker and
never include the PCRE2 trace path.
</p>
</section>
<section>
<h3>Generated cases</h3>
<p>
ECMAScript generator v1 deterministically samples supported
normalized-AST paths from an explicit seed. Every intended positive
and negative subject runs through the selected actual engine before
it can become a test; candidates with the wrong outcome are
discarded. The coverage report names unsupported constructs.
Sampling is not proof of complete language coverage. Other flavours
remain unavailable until their structural providers expose the
required AST; their syntax is never reinterpreted as ECMAScript.
</p>
</section>
<section>
<h3>Pattern formatting</h3>
<p>
The ECMAScript formatter makes only grammar-backed literal/control
escape changes; it does not insert layout whitespace or reinterpret
another flavour&apos;s syntax. Applying a preview requires
independent source and candidate reparsing, actual-engine
match/replacement comparison, every applicable exact unit test, and
an explicit confirmation. Passing bounded checks is evidence for
those snapshots, not proof for every possible subject.
</p> </p>
</section> </section>
<section> <section>
@@ -105,6 +157,78 @@ export function HelpDialog({
<dt>Unit-test suite wall time</dt> <dt>Unit-test suite wall time</dt>
<dd>60 seconds</dd> <dd>60 seconds</dd>
</div> </div>
<div>
<dt>Corpus input</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumCorpusDocuments.toLocaleString()}{" "}
documents · {DEFAULT_REGEX_LIMITS.corpusHardBytes / 1024 / 1024}{" "}
MiB
</dd>
</div>
<div>
<dt>Corpus wall time</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs / 60_000} minutes
</dd>
</div>
<div>
<dt>Analysis wall time</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumBenchmarkWallTimeMs / 1_000}{" "}
seconds
</dd>
</div>
<div>
<dt>Benchmark iteration cap</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumBenchmarkIterations.toLocaleString()}
</dd>
</div>
<div>
<dt>Growth probe steps</dt>
<dd>24</dd>
</div>
<div>
<dt>Generated cases</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumGeneratedCases.toLocaleString()}{" "}
retained ·{" "}
{GENERATION_LIMITS.maximumCandidateAttempts.toLocaleString()}{" "}
attempts
</dd>
</div>
<div>
<dt>Generation defaults</dt>
<dd>
{DEFAULT_GENERATION_SETTINGS.perCaseTimeoutMs} ms per case ·{" "}
{DEFAULT_GENERATION_SETTINGS.maximumWallTimeMs / 1_000} seconds
aggregate
</dd>
</div>
<div>
<dt>Generated subject text</dt>
<dd>
{GENERATION_LIMITS.maximumTotalSubjectBytes / 1024 / 1024} MiB
aggregate hard cap
</dd>
</div>
<div>
<dt>Formatter validation</dt>
<dd>1,000 tests · 60 seconds aggregate</dd>
</div>
<div>
<dt>Independent corpus lines</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumCorpusLines.toLocaleString()}
</dd>
</div>
<div>
<dt>Corpus applied output</dt>
<dd>
{DEFAULT_REGEX_LIMITS.maximumCorpusOutputBytes / 1024 / 1024}{" "}
MiB
</dd>
</div>
</dl> </dl>
<p> <p>
A timeout terminates the worker. It means timed out, never no A timeout terminates the worker. It means timed out, never no
@@ -116,7 +240,15 @@ export function HelpDialog({
<p> <p>
Ad-hoc test text is excluded from local saves and exports by Ad-hoc test text is excluded from local saves and exports by
default. Test definitions are deliberate project data. Import is default. Test definitions are deliberate project data. Import is
validated and paused until you review and run it. validated and paused until you review and run it. Corpus documents,
contents, outputs and results are always ephemeral and excluded from
projects and IndexedDB. Analysis settings and results are likewise
ephemeral. Generated-case settings and review results are ephemeral;
only cases explicitly added to the test suite persist, together with
their bounded seed and generator provenance. Test subjects remain
excluded unless separately opted in. Formatter previews and
validation observations are ephemeral; only an explicitly applied
pattern enters workbench/project state.
</p> </p>
</section> </section>
<section> <section>

View File

@@ -0,0 +1,241 @@
.minimize-dialog {
width: min(90rem, calc(100% - 2rem));
}
.minimize-panel {
max-height: calc(100vh - 2rem);
overflow: auto;
}
.minimizer-body {
display: grid;
gap: 0.8rem;
padding: 0.8rem;
}
.minimizer-advisory {
margin: 0;
border-left: 3px solid var(--toolbox-accent);
padding: 0.7rem 0.8rem;
background: var(--toolbox-accent-soft);
color: var(--toolbox-muted);
font-size: 0.78rem;
line-height: 1.5;
}
.minimizer-config,
.minimizer-result {
display: grid;
gap: 0.75rem;
overflow: hidden;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.8);
background: var(--toolbox-surface);
}
.minimizer-config > header,
.minimizer-result > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
border-bottom: 1px solid var(--toolbox-border);
padding: 0.7rem 0.8rem;
background: var(--toolbox-surface-soft);
}
.minimizer-config h3,
.minimizer-result h3,
.minimizer-result header p {
margin: 0;
}
.minimizer-config h3,
.minimizer-result h3 {
font-size: 0.95rem;
}
.minimizer-config > header span {
color: var(--toolbox-muted);
font-size: 0.72rem;
}
.minimizer-control-grid,
.minimizer-variant-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
align-items: end;
gap: 0.65rem;
padding: 0 0.8rem;
}
.minimizer-variant-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.minimizer-control-grid label:not(.check-control),
.minimizer-variant-grid label,
.minimizer-subject,
.minimizer-output {
display: grid;
gap: 0.3rem;
color: var(--toolbox-muted);
font-size: 0.72rem;
font-weight: 700;
}
.minimizer-wide-control {
grid-column: 1 / -1;
}
.minimizer-check {
min-height: 2.25rem;
align-items: center;
}
.minimizer-subject {
padding: 0 0.8rem 0.8rem;
}
.minimizer-subject textarea {
min-height: 8rem;
}
.minimizer-variant-grid textarea {
min-height: 4.5rem;
}
.minimizer-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
}
.minimizer-actions p,
.minimizer-status {
margin: 0;
color: var(--toolbox-muted);
font-size: 0.76rem;
}
.minimizer-progress {
display: grid;
grid-template-columns: minmax(8rem, 1fr) auto;
align-items: center;
gap: 0.65rem;
color: var(--toolbox-muted);
font-size: 0.7rem;
}
.minimizer-progress progress {
width: 100%;
accent-color: var(--toolbox-accent);
}
.minimizer-result > dl {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
margin: 0;
padding: 0 0.8rem;
}
.minimizer-result > dl > div {
padding: 0.55rem;
border-bottom: 1px solid var(--toolbox-border);
}
.minimizer-result dt {
color: var(--toolbox-muted);
font-size: 0.66rem;
font-weight: 750;
text-transform: uppercase;
}
.minimizer-result dd {
margin: 0.15rem 0 0;
font-size: 0.78rem;
}
.minimizer-result-badge {
border: 1px solid currentColor;
border-radius: 99rem;
padding: 0.2rem 0.45rem;
color: var(--toolbox-accent);
font-size: 0.65rem;
font-weight: 800;
text-transform: uppercase;
}
.minimizer-result-badge.is-partial,
.minimizer-result-badge.is-cancelled {
color: var(--regex-warning);
}
.minimizer-result-badge.is-unsupported {
color: var(--toolbox-danger);
}
.minimizer-observation-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
padding: 0 0.8rem;
}
.minimizer-observation {
display: grid;
gap: 0.3rem;
min-width: 0;
border: 1px solid var(--toolbox-border);
border-left: 4px solid var(--toolbox-accent);
border-radius: calc(var(--toolbox-radius) * 0.7);
padding: 0.65rem;
background: var(--toolbox-surface-soft);
}
.minimizer-observation.is-timeout {
border-left-color: var(--regex-warning);
}
.minimizer-observation.is-crash,
.minimizer-observation.is-worker-error {
border-left-color: var(--toolbox-danger);
}
.minimizer-observation span,
.minimizer-observation p,
.minimizer-observation small {
margin: 0;
color: var(--toolbox-muted);
font-size: 0.72rem;
line-height: 1.45;
}
.minimizer-observation code {
overflow-wrap: anywhere;
font-size: 0.68rem;
}
.minimizer-output {
padding: 0 0.8rem;
}
.minimizer-output textarea {
min-height: 6rem;
}
.minimizer-result > .minimizer-actions {
padding: 0 0.8rem 0.8rem;
}
@media (max-width: 720px) {
.minimizer-variant-grid,
.minimizer-observation-grid {
grid-template-columns: 1fr;
}
.minimizer-progress {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,184 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type {
SubjectMinimizationProgress,
SubjectMinimizationRequest,
SubjectMinimizationResult,
} from "../regex/minimization/minimization.types";
import { MinimizePanel, type SubjectMinimizerClient } from "./MinimizePanel";
function result(): SubjectMinimizationResult {
const observation = {
status: "complete" as const,
reproduced: true,
fingerprint: ["expected-no-match:present"],
summary: "The forbidden match is still present.",
sides: [
{
flavour: "ecmascript" as const,
status: "complete" as const,
engineIdentity: "ecmascript · Fixture · 1 · utf16",
},
],
};
return {
schemaVersion: 1,
targetIdentity: "minimization-fixture",
status: "complete",
stopReason: "locally-minimal",
locallyMinimal: true,
originalSubject: "abXcd",
minimizedSubject: "X",
originalScalars: 5,
minimizedScalars: 1,
originalUtf8Bytes: 5,
minimizedUtf8Bytes: 1,
evaluations: 12,
acceptedReductions: 3,
elapsedMs: 4,
budgets: {
maximumEvaluations: 500,
maximumWallTimeMs: 15_000,
candidateTimeoutMs: 2_000,
},
baseline: observation,
final: observation,
retainedHistory: [],
historyTruncated: false,
};
}
function renderPanel(client: SubjectMinimizerClient) {
const onUseSubject = vi.fn();
const onClose = vi.fn();
render(
<MinimizePanel
activeFlavour="ecmascript"
activeFlavourVersion="ECMAScript 2025 syntax / current browser runtime"
activeFlags={["g", "u"]}
activeOptions={{}}
pattern="X"
subject="abXcd"
replacement="!"
scanAll
timeoutMs={2_000}
onUseSubject={onUseSubject}
onClose={onClose}
createMinimizer={() => client}
/>,
);
return { onUseSubject, onClose };
}
function completingClient(
requests: SubjectMinimizationRequest[],
): SubjectMinimizerClient {
return {
minimize: vi.fn(
async (
request: SubjectMinimizationRequest,
onProgress?: (progress: SubjectMinimizationProgress) => void,
) => {
requests.push(request);
onProgress?.({
phase: "local-sweep",
evaluations: 12,
maximumEvaluations: 500,
acceptedReductions: 3,
currentScalars: 1,
currentUtf8Bytes: 1,
elapsedMs: 4,
maximumWallTimeMs: 15_000,
});
return result();
},
),
cancel: vi.fn(),
dispose: vi.fn(),
};
}
describe("MinimizePanel", () => {
it("runs a failing unit-test predicate and can apply the verified result", async () => {
const requests: SubjectMinimizationRequest[] = [];
const { onUseSubject } = renderPanel(completingClient(requests));
expect(
screen.getByText(/does not claim a globally or semantically minimal/u),
).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "Verify & minimize" }));
expect(
await screen.findByRole("heading", {
name: "Locally minimal under the documented transforms",
}),
).toBeVisible();
expect(screen.getByLabelText("Minimized subject")).toHaveValue("X");
expect(requests[0]?.target).toMatchObject({
kind: "engine",
oracle: {
kind: "unit-test-failure",
expectation: { kind: "should-not-match" },
},
});
fireEvent.click(
screen.getByRole("button", { name: "Use minimized subject" }),
);
expect(onUseSubject).toHaveBeenCalledWith("X");
});
it("exposes capture-range, timeout and two-engine mismatch targets", async () => {
const requests: SubjectMinimizationRequest[] = [];
renderPanel(completingClient(requests));
const kind = screen.getByLabelText("Minimization failure kind");
expect(kind).toContainHTML("capture-range-mismatch");
expect(kind).toContainHTML("engine-timeout");
expect(kind).toContainHTML("comparison-mismatch");
fireEvent.change(kind, { target: { value: "engine-timeout" } });
fireEvent.click(screen.getByRole("button", { name: "Verify & minimize" }));
await waitFor(() => expect(requests).toHaveLength(1));
expect(requests[0]?.target).toMatchObject({
kind: "engine",
oracle: { kind: "engine-timeout" },
});
fireEvent.change(kind, {
target: { value: "comparison-mismatch" },
});
fireEvent.click(screen.getByRole("button", { name: "Verify & minimize" }));
await waitFor(() => expect(requests).toHaveLength(2));
expect(requests[1]?.target).toMatchObject({
kind: "comparison-mismatch",
operation: "match",
sides: [
{ flavour: "ecmascript", pattern: "X" },
{ flavour: "pcre2", pattern: "X" },
],
});
});
it("cancels the active worker set and closes independently", async () => {
const client: SubjectMinimizerClient = {
minimize: vi.fn(
() => new Promise<SubjectMinimizationResult>(() => undefined),
),
cancel: vi.fn(),
dispose: vi.fn(),
};
const { onClose } = renderPanel(client);
fireEvent.click(screen.getByRole("button", { name: "Verify & minimize" }));
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
expect(client.cancel).toHaveBeenCalledOnce();
expect(
screen.getByText(/active syntax and engine workers were terminated/u),
).toBeVisible();
fireEvent.click(
screen.getByRole("button", { name: "Close subject minimizer" }),
);
expect(onClose).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,870 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { writeClipboardText } from "../browser/clipboard";
import {
AVAILABLE_REGEX_FLAVOURS,
defaultRegexOptions,
} from "../regex/flavours/flavour-registry";
import {
DEFAULT_REGEX_LIMITS,
utf8ByteLength,
} from "../regex/execution/request-limits";
import type {
RegexEngineOptions,
RegexFlavourId,
} from "../regex/model/flavour";
import type { RegexTestExpectation } from "../regex/tests/test-case.types";
import { SubjectMinimizer } from "../regex/minimization/SubjectMinimizer";
import type {
EngineFailureOracle,
SubjectMinimizationProgress,
SubjectMinimizationRequest,
SubjectMinimizationResult,
} from "../regex/minimization/minimization.types";
import "./MinimizePanel.css";
type MinimizationMode =
| "unit-test-failure"
| "capture-range-mismatch"
| "engine-timeout"
| "comparison-mismatch";
type UnitExpectationKind = RegexTestExpectation["kind"];
export interface SubjectMinimizerClient {
minimize(
request: SubjectMinimizationRequest,
onProgress?: (progress: SubjectMinimizationProgress) => void,
): Promise<SubjectMinimizationResult>;
cancel(): void;
dispose(): void;
}
export interface MinimizePanelProps {
readonly activeFlavour: RegexFlavourId;
readonly activeFlavourVersion: string;
readonly activeFlags: readonly string[];
readonly activeOptions: RegexEngineOptions;
readonly pattern: string;
readonly subject: string;
readonly replacement: string;
readonly scanAll: boolean;
readonly timeoutMs: number;
readonly onUseSubject: (subject: string) => void;
readonly onClose: () => void;
readonly createMinimizer?: () => SubjectMinimizerClient;
}
const ECMASCRIPT = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");
const PCRE2 = AVAILABLE_REGEX_FLAVOURS.require("pcre2");
function flagsFor(
activeFlavour: RegexFlavourId,
activeFlags: readonly string[],
flavour: "ecmascript" | "pcre2",
): readonly string[] {
const definition = flavour === "ecmascript" ? ECMASCRIPT : PCRE2;
return activeFlavour === flavour
? definition.flags
.map((flag) => flag.value)
.filter((flag) => activeFlags.includes(flag))
: definition.defaultFlags;
}
function optionsFor(
activeFlavour: RegexFlavourId,
activeOptions: RegexEngineOptions,
flavour: "ecmascript" | "pcre2",
): RegexEngineOptions {
if (activeFlavour === flavour) return activeOptions;
return defaultRegexOptions(flavour === "ecmascript" ? ECMASCRIPT : PCRE2);
}
function parseCaptureSelector(value: string): {
readonly groupNumber?: number;
readonly groupName?: string;
} {
const trimmed = value.trim();
const number = Number(trimmed);
return trimmed !== "" && Number.isSafeInteger(number) && number > 0
? { groupNumber: number }
: { groupName: trimmed };
}
function resultLabel(result: SubjectMinimizationResult): string {
if (result.locallyMinimal) {
return "Locally minimal under the documented transforms";
}
return result.stopReason.replaceAll("-", " ");
}
function Observation({
label,
observation,
}: {
readonly label: string;
readonly observation: SubjectMinimizationResult["baseline"];
}) {
return (
<article className={`minimizer-observation is-${observation.status}`}>
<strong>{label}</strong>
<span>
{observation.status} ·{" "}
{observation.reproduced ? "predicate reproduced" : "not reproduced"}
</span>
<p>{observation.summary}</p>
{observation.fingerprint.length > 0 ? (
<code>{observation.fingerprint.join(" · ")}</code>
) : null}
{observation.sides?.length ? (
<small>
{observation.sides
.map(
(side) =>
`${side.flavour}: ${side.status}${
side.engineIdentity ? ` · ${side.engineIdentity}` : ""
}`,
)
.join(" | ")}
</small>
) : null}
</article>
);
}
export function MinimizePanel({
activeFlavour,
activeFlavourVersion,
activeFlags,
activeOptions,
pattern: initialPattern,
subject: initialSubject,
replacement: initialReplacement,
scanAll: initialScanAll,
timeoutMs: initialTimeoutMs,
onUseSubject,
onClose,
createMinimizer = () => new SubjectMinimizer(),
}: MinimizePanelProps) {
const [mode, setMode] = useState<MinimizationMode>("unit-test-failure");
const [subject, setSubject] = useState(initialSubject);
const [scanAll, setScanAll] = useState(initialScanAll);
const [unitKind, setUnitKind] =
useState<UnitExpectationKind>("should-not-match");
const [expectedCount, setExpectedCount] = useState(1);
const [matchIndex, setMatchIndex] = useState(0);
const [expectedValue, setExpectedValue] = useState("");
const [captureSelector, setCaptureSelector] = useState("1");
const [captureStatus, setCaptureStatus] = useState<
"participated" | "did-not-participate" | "matched-empty"
>("participated");
const [checkCaptureValue, setCheckCaptureValue] = useState(false);
const [expectedStart, setExpectedStart] = useState(0);
const [expectedEnd, setExpectedEnd] = useState(0);
const [durationLimitMs, setDurationLimitMs] = useState(10);
const [comparisonOperation, setComparisonOperation] = useState<
"match" | "replace"
>("match");
const [ecmaPattern, setEcmaPattern] = useState(initialPattern);
const [pcrePattern, setPcrePattern] = useState(initialPattern);
const [ecmaReplacement, setEcmaReplacement] = useState(initialReplacement);
const [pcreReplacement, setPcreReplacement] = useState(initialReplacement);
const [maximumEvaluations, setMaximumEvaluations] = useState(500);
const [maximumWallTimeMs, setMaximumWallTimeMs] = useState(15_000);
const [candidateTimeoutMs, setCandidateTimeoutMs] = useState(
Math.min(
DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
Math.max(1, initialTimeoutMs),
),
);
const [running, setRunning] = useState(false);
const [status, setStatus] = useState(
"Select an exact failure predicate, then verify the baseline.",
);
const [progress, setProgress] = useState<SubjectMinimizationProgress>();
const [result, setResult] = useState<SubjectMinimizationResult>();
const [copyStatus, setCopyStatus] = useState("");
const minimizer = useRef<SubjectMinimizerClient | null>(null);
const revision = useRef(0);
useEffect(
() => () => {
revision.current += 1;
minimizer.current?.dispose();
minimizer.current = null;
},
[],
);
const ecmaFlags = useMemo(
() => flagsFor(activeFlavour, activeFlags, "ecmascript"),
[activeFlags, activeFlavour],
);
const pcreFlags = useMemo(
() => flagsFor(activeFlavour, activeFlags, "pcre2"),
[activeFlags, activeFlavour],
);
const ecmaOptions = useMemo(
() => optionsFor(activeFlavour, activeOptions, "ecmascript"),
[activeFlavour, activeOptions],
);
const pcreOptions = useMemo(
() => optionsFor(activeFlavour, activeOptions, "pcre2"),
[activeFlavour, activeOptions],
);
const activeEngineSupported =
activeFlavour === "ecmascript" || activeFlavour === "pcre2";
const unitExpectation = (): RegexTestExpectation => {
switch (unitKind) {
case "should-match":
case "should-not-match":
case "must-time-out":
return { kind: unitKind };
case "match-count":
return { kind: unitKind, count: expectedCount };
case "full-match":
return {
kind: unitKind,
matchIndex,
value: expectedValue,
};
case "capture":
return {
kind: unitKind,
matchIndex,
...parseCaptureSelector(captureSelector),
status: captureStatus,
...(checkCaptureValue ? { value: expectedValue } : {}),
};
case "replacement":
return { kind: unitKind, expected: expectedValue };
case "must-complete-within":
return { kind: unitKind, milliseconds: durationLimitMs };
}
};
const activeSide = {
flavour:
activeFlavour === "pcre2" ? ("pcre2" as const) : ("ecmascript" as const),
flavourVersion: activeEngineSupported
? activeFlavourVersion
: ECMASCRIPT.defaultVersion,
pattern: initialPattern,
flags: activeEngineSupported ? activeFlags : ECMASCRIPT.defaultFlags,
options: activeEngineSupported
? activeOptions
: defaultRegexOptions(ECMASCRIPT),
...(unitKind === "replacement" ? { replacement: initialReplacement } : {}),
};
const request = (): SubjectMinimizationRequest => {
let target: SubjectMinimizationRequest["target"];
if (mode === "comparison-mismatch") {
target = {
kind: "comparison-mismatch",
operation: comparisonOperation,
scanAll,
sides: [
{
flavour: "ecmascript",
flavourVersion: ECMASCRIPT.defaultVersion,
pattern: ecmaPattern,
flags: ecmaFlags,
options: ecmaOptions,
...(comparisonOperation === "replace"
? { replacement: ecmaReplacement }
: {}),
},
{
flavour: "pcre2",
flavourVersion: PCRE2.defaultVersion,
pattern: pcrePattern,
flags: pcreFlags,
options: pcreOptions,
...(comparisonOperation === "replace"
? { replacement: pcreReplacement }
: {}),
},
],
};
} else {
let oracle: EngineFailureOracle;
if (mode === "engine-timeout") {
oracle = { kind: "engine-timeout" };
} else if (mode === "capture-range-mismatch") {
oracle = {
kind: "capture-range-mismatch",
matchIndex,
...parseCaptureSelector(captureSelector),
expectedRange: {
startUtf16: expectedStart,
endUtf16: expectedEnd,
},
};
} else {
oracle = {
kind: "unit-test-failure",
expectation: unitExpectation(),
};
}
target = {
kind: "engine",
side: activeSide,
scanAll,
oracle,
};
}
return {
schemaVersion: 1,
subject,
target,
budgets: {
maximumEvaluations,
maximumWallTimeMs,
candidateTimeoutMs,
},
maximumMatches: 1_000,
maximumCaptureRows: 10_000,
maximumOutputBytes: 1024 * 1024,
};
};
const run = async () => {
if (mode !== "comparison-mismatch" && !activeEngineSupported) {
setStatus(
`Subject minimization is not yet implemented for ${activeFlavour}; select ECMAScript or PCRE2, or use the explicit ECMAScript ↔ PCRE2 comparison target.`,
);
return;
}
const currentRevision = ++revision.current;
setRunning(true);
setResult(undefined);
setCopyStatus("");
setStatus("Verifying the baseline with the selected real engine worker…");
try {
minimizer.current ??= createMinimizer();
const minimized = await minimizer.current.minimize(request(), (value) => {
if (currentRevision !== revision.current) return;
setProgress(value);
setStatus(
`${value.phase.replaceAll("-", " ")} · ${value.evaluations}/${value.maximumEvaluations} evaluations · ${value.acceptedReductions} accepted`,
);
});
if (currentRevision !== revision.current) return;
setResult(minimized);
setStatus(resultLabel(minimized));
} catch (error) {
if (currentRevision !== revision.current) return;
setStatus(error instanceof Error ? error.message : String(error));
} finally {
if (currentRevision === revision.current) setRunning(false);
}
};
const cancel = () => {
revision.current += 1;
minimizer.current?.cancel();
setRunning(false);
setStatus("Cancelled; active syntax and engine workers were terminated.");
};
return (
<section
className="panel minimize-panel"
aria-labelledby="minimize-heading"
>
<header className="panel-heading">
<div>
<p className="eyebrow">Bounded failing-subject reduction</p>
<h2 id="minimize-heading">Minimize a reproducer</h2>
</div>
<button
type="button"
className="icon-button"
aria-label="Close subject minimizer"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
</header>
<div className="minimizer-body">
<p className="minimizer-advisory">
This reducer proves only local minimality under deterministic
Unicode-scalar chunk deletion, single-scalar deletion and canonical
replacement. It does not claim a globally or semantically minimal
example.
</p>
{!activeEngineSupported ? (
<p className="minimizer-advisory" role="status">
Python and Java execution remain available in the main workbench,
corpus and unit-test modes. This reducer currently has verified
failure oracles only for ECMAScript and PCRE2.
</p>
) : null}
<section className="minimizer-config" aria-labelledby="minimize-target">
<header>
<h3 id="minimize-target">Exact predicate</h3>
<span>
{activeFlavour} · {utf8ByteLength(subject).toLocaleString()} bytes
</span>
</header>
<div className="minimizer-control-grid">
<label>
<span>Failure kind</span>
<select
aria-label="Minimization failure kind"
value={mode}
disabled={running}
onChange={(event) =>
setMode(event.target.value as MinimizationMode)
}
>
<option value="unit-test-failure">
Failing unit-test assertion
</option>
<option value="capture-range-mismatch">
Wrong capture range
</option>
<option value="engine-timeout">Exact engine timeout</option>
<option value="comparison-mismatch">
ECMAScript PCRE2 mismatch
</option>
</select>
</label>
<label className="check-control minimizer-check">
<input
type="checkbox"
checked={scanAll}
disabled={running}
onChange={(event) => setScanAll(event.target.checked)}
/>
Scan all
</label>
</div>
{mode === "unit-test-failure" ? (
<div className="minimizer-control-grid">
<label>
<span>Saved assertion</span>
<select
aria-label="Unit-test expectation"
value={unitKind}
disabled={running}
onChange={(event) =>
setUnitKind(event.target.value as UnitExpectationKind)
}
>
<option value="should-match">Should match</option>
<option value="should-not-match">Should not match</option>
<option value="match-count">Exact match count</option>
<option value="full-match">Full-match value</option>
<option value="capture">Capture status/value</option>
<option value="replacement">Replacement output</option>
<option value="must-complete-within">
Must complete within
</option>
<option value="must-time-out">Must time out</option>
</select>
</label>
{unitKind === "match-count" ? (
<label>
<span>Expected count</span>
<input
type="number"
value={expectedCount}
min={0}
max={DEFAULT_REGEX_LIMITS.maximumMatches}
onChange={(event) =>
setExpectedCount(Number(event.target.value))
}
/>
</label>
) : null}
{unitKind === "full-match" || unitKind === "capture" ? (
<label>
<span>Zero-based match index</span>
<input
type="number"
value={matchIndex}
min={0}
onChange={(event) =>
setMatchIndex(Number(event.target.value))
}
/>
</label>
) : null}
{unitKind === "capture" ? (
<>
<label>
<span>Capture number or name</span>
<input
type="text"
value={captureSelector}
onChange={(event) =>
setCaptureSelector(event.target.value)
}
/>
</label>
<label>
<span>Expected participation</span>
<select
value={captureStatus}
onChange={(event) =>
setCaptureStatus(
event.target.value as typeof captureStatus,
)
}
>
<option value="participated">Participated</option>
<option value="did-not-participate">
Did not participate
</option>
<option value="matched-empty">Matched empty</option>
</select>
</label>
<label className="check-control minimizer-check">
<input
type="checkbox"
checked={checkCaptureValue}
onChange={(event) =>
setCheckCaptureValue(event.target.checked)
}
/>
Assert value too
</label>
</>
) : null}
{unitKind === "full-match" ||
unitKind === "replacement" ||
(unitKind === "capture" && checkCaptureValue) ? (
<label className="minimizer-wide-control">
<span>Expected value</span>
<input
type="text"
value={expectedValue}
onChange={(event) => setExpectedValue(event.target.value)}
/>
</label>
) : null}
{unitKind === "must-complete-within" ? (
<label>
<span>Assertion milliseconds</span>
<input
type="number"
value={durationLimitMs}
min={1}
onChange={(event) =>
setDurationLimitMs(Number(event.target.value))
}
/>
</label>
) : null}
</div>
) : null}
{mode === "capture-range-mismatch" ? (
<div className="minimizer-control-grid">
<label>
<span>Zero-based match index</span>
<input
type="number"
value={matchIndex}
min={0}
onChange={(event) =>
setMatchIndex(Number(event.target.value))
}
/>
</label>
<label>
<span>Capture number or name</span>
<input
type="text"
value={captureSelector}
onChange={(event) => setCaptureSelector(event.target.value)}
/>
</label>
<label>
<span>Expected UTF-16 start</span>
<input
type="number"
value={expectedStart}
min={0}
max={subject.length}
onChange={(event) =>
setExpectedStart(Number(event.target.value))
}
/>
</label>
<label>
<span>Expected UTF-16 end</span>
<input
type="number"
value={expectedEnd}
min={0}
max={subject.length}
onChange={(event) =>
setExpectedEnd(Number(event.target.value))
}
/>
</label>
</div>
) : null}
{mode === "comparison-mismatch" ? (
<>
<div className="minimizer-control-grid">
<label>
<span>Operation</span>
<select
value={comparisonOperation}
onChange={(event) =>
setComparisonOperation(
event.target.value as "match" | "replace",
)
}
>
<option value="match">Match</option>
<option value="replace">Replace</option>
</select>
</label>
</div>
<div className="minimizer-variant-grid">
<label>
<span>ECMAScript pattern</span>
<textarea
value={ecmaPattern}
onChange={(event) => setEcmaPattern(event.target.value)}
/>
</label>
<label>
<span>PCRE2 pattern</span>
<textarea
value={pcrePattern}
onChange={(event) => setPcrePattern(event.target.value)}
/>
</label>
{comparisonOperation === "replace" ? (
<>
<label>
<span>ECMAScript replacement</span>
<textarea
value={ecmaReplacement}
onChange={(event) =>
setEcmaReplacement(event.target.value)
}
/>
</label>
<label>
<span>PCRE2 replacement</span>
<textarea
value={pcreReplacement}
onChange={(event) =>
setPcreReplacement(event.target.value)
}
/>
</label>
</>
) : null}
</div>
</>
) : null}
<label className="minimizer-subject">
<span>Failing subject · local text only</span>
<textarea
aria-label="Subject to minimize"
value={subject}
disabled={running}
onChange={(event) => setSubject(event.target.value)}
/>
</label>
</section>
<section className="minimizer-config" aria-labelledby="minimize-bounds">
<header>
<h3 id="minimize-bounds">Hard bounds</h3>
<span>Fixed deadline per candidate</span>
</header>
<div className="minimizer-control-grid">
<label>
<span>Candidate evaluations</span>
<input
type="number"
value={maximumEvaluations}
min={1}
max={DEFAULT_REGEX_LIMITS.maximumMinimizerRuns}
disabled={running}
onChange={(event) =>
setMaximumEvaluations(Number(event.target.value))
}
/>
</label>
<label>
<span>Aggregate wall time ms</span>
<input
type="number"
value={maximumWallTimeMs}
min={1}
max={60_000}
disabled={running}
onChange={(event) =>
setMaximumWallTimeMs(Number(event.target.value))
}
/>
</label>
<label>
<span>Each candidate ms</span>
<input
type="number"
value={candidateTimeoutMs}
min={1}
max={DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs}
disabled={running}
onChange={(event) =>
setCandidateTimeoutMs(Number(event.target.value))
}
/>
</label>
</div>
</section>
<div className="minimizer-actions">
<button
type="button"
className="primary-button"
disabled={
running ||
(mode !== "comparison-mismatch" && !activeEngineSupported)
}
onClick={() => void run()}
>
{running ? "Minimizing…" : "Verify & minimize"}
</button>
{running ? (
<button type="button" className="secondary-button" onClick={cancel}>
Cancel
</button>
) : null}
<p className="minimizer-status" aria-live="polite">
{status}
</p>
</div>
{progress ? (
<div className="minimizer-progress">
<progress
aria-label="Minimizer evaluation progress"
value={progress.evaluations}
max={progress.maximumEvaluations}
/>
<span>
{progress.currentScalars.toLocaleString()} scalars ·{" "}
{progress.currentUtf8Bytes.toLocaleString()} bytes ·{" "}
{progress.elapsedMs.toFixed(0)}/
{progress.maximumWallTimeMs.toLocaleString()} ms
</span>
</div>
) : null}
{result ? (
<section
className="minimizer-result"
aria-labelledby="minimize-result"
>
<header>
<div>
<p className="eyebrow">Exact target {result.targetIdentity}</p>
<h3 id="minimize-result">{resultLabel(result)}</h3>
</div>
<span className={`minimizer-result-badge is-${result.status}`}>
{result.status}
</span>
</header>
<dl>
<div>
<dt>Scalars</dt>
<dd>
{result.originalScalars.toLocaleString()} {" "}
{result.minimizedScalars.toLocaleString()}
</dd>
</div>
<div>
<dt>UTF-8 bytes</dt>
<dd>
{result.originalUtf8Bytes.toLocaleString()} {" "}
{result.minimizedUtf8Bytes.toLocaleString()}
</dd>
</div>
<div>
<dt>Evaluations</dt>
<dd>
{result.evaluations.toLocaleString()} /{" "}
{result.budgets.maximumEvaluations.toLocaleString()}
</dd>
</div>
<div>
<dt>Accepted reductions</dt>
<dd>{result.acceptedReductions.toLocaleString()}</dd>
</div>
<div>
<dt>Wall time</dt>
<dd>{result.elapsedMs.toFixed(1)} ms</dd>
</div>
<div>
<dt>Stop reason</dt>
<dd>{result.stopReason.replaceAll("-", " ")}</dd>
</div>
</dl>
<div className="minimizer-observation-grid">
<Observation
label="Baseline verification"
observation={result.baseline}
/>
<Observation
label="Final accepted candidate"
observation={result.final}
/>
</div>
<label className="minimizer-output">
<span>Minimized subject</span>
<textarea
aria-label="Minimized subject"
readOnly
value={result.minimizedSubject}
/>
</label>
<div className="minimizer-actions">
<button
type="button"
className="primary-button"
disabled={!result.final.reproduced}
onClick={() => onUseSubject(result.minimizedSubject)}
>
Use minimized subject
</button>
<button
type="button"
className="secondary-button"
onClick={() => {
void writeClipboardText(result.minimizedSubject)
.then(() => setCopyStatus("Copied locally."))
.catch((error: unknown) =>
setCopyStatus(
error instanceof Error ? error.message : String(error),
),
);
}}
>
Copy
</button>
<p aria-live="polite">{copyStatus}</p>
</div>
</section>
) : null}
</div>
</section>
);
}

View File

@@ -0,0 +1,71 @@
import { useEffect, useRef, type ReactNode } from "react";
export function ModalDialog({
id,
open,
labelledBy,
onClose,
children,
className = "",
}: {
readonly id: string;
readonly open: boolean;
readonly labelledBy: string;
readonly onClose: () => void;
readonly children: ReactNode;
readonly className?: string;
}) {
const dialog = useRef<HTMLDialogElement>(null);
useEffect(() => {
const element = dialog.current;
if (!element) return;
if (open && !element.open) {
if (typeof element.showModal === "function") {
element.showModal();
} else {
// jsdom and older browsers can still expose the content for a usable
// non-modal fallback.
element.setAttribute("open", "");
}
(
element.querySelector<HTMLElement>("[data-dialog-initial-focus]") ??
element
).focus();
return;
}
if (!open && element.open) {
if (typeof element.close === "function") {
element.close();
} else {
element.removeAttribute("open");
}
}
}, [open]);
return (
<dialog
ref={dialog}
id={id}
className={`app-dialog auxiliary-dialog ${className}`.trim()}
aria-labelledby={labelledBy}
aria-modal="true"
tabIndex={-1}
onClose={onClose}
onCancel={(event) => {
event.preventDefault();
onClose();
}}
onKeyDownCapture={(event) => {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopPropagation();
onClose();
}}
>
{children}
</dialog>
);
}

View File

@@ -0,0 +1,342 @@
.formatter-dialog {
width: min(90rem, calc(100% - 2rem));
}
.formatter-panel {
max-height: calc(100vh - 2rem);
overflow: auto;
}
.formatter-body {
display: grid;
gap: 0.8rem;
padding: 0.8rem;
}
.formatter-advisory,
.formatter-unavailable,
.formatter-empty,
.formatter-result-note {
margin: 0;
color: var(--toolbox-muted);
font-size: 0.78rem;
line-height: 1.5;
}
.formatter-advisory {
border-left: 3px solid var(--toolbox-accent);
padding: 0.7rem 0.8rem;
background: var(--toolbox-accent-soft);
}
.formatter-unavailable {
margin: 0.8rem;
border: 1px solid
color-mix(in srgb, var(--toolbox-danger) 45%, var(--toolbox-border));
border-radius: calc(var(--toolbox-radius) * 0.72);
padding: 0.8rem;
background: color-mix(
in srgb,
var(--toolbox-danger) 8%,
var(--toolbox-surface)
);
color: color-mix(in srgb, var(--toolbox-danger) 78%, var(--toolbox-text));
}
.formatter-preview,
.formatter-validation {
display: grid;
gap: 0.75rem;
overflow: hidden;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.82);
background: var(--toolbox-surface);
}
.formatter-preview > header,
.formatter-validation > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.7rem 0.8rem;
border-bottom: 1px solid var(--toolbox-border);
background: var(--toolbox-surface-soft);
}
.formatter-preview > header p,
.formatter-preview > header h3,
.formatter-validation > header p,
.formatter-validation > header h3 {
margin: 0;
}
.formatter-preview > header h3,
.formatter-validation > header h3 {
font-size: 0.95rem;
}
.formatter-preview > header > span,
.formatter-validation > header > span {
color: var(--toolbox-muted);
font-size: 0.7rem;
font-weight: 750;
text-transform: uppercase;
}
.formatter-pattern-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.7rem;
padding: 0 0.8rem;
}
.formatter-pattern-grid article {
display: grid;
min-width: 0;
gap: 0.4rem;
color: var(--toolbox-muted);
font-size: 0.72rem;
}
.formatter-pattern-grid pre {
min-height: 5rem;
max-height: 16rem;
overflow: auto;
margin: 0;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.66);
padding: 0.65rem;
background: var(--toolbox-surface-soft);
color: var(--toolbox-text);
font-size: 0.75rem;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.formatter-table-scroll {
overflow: auto;
margin: 0 0.8rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.66);
}
.formatter-table-scroll table {
width: 100%;
border-collapse: collapse;
font-size: 0.72rem;
}
.formatter-table-scroll th,
.formatter-table-scroll td {
padding: 0.5rem 0.6rem;
border-bottom: 1px solid var(--toolbox-border);
text-align: left;
vertical-align: top;
}
.formatter-table-scroll th {
position: sticky;
top: 0;
background: var(--toolbox-surface-soft);
color: var(--toolbox-muted);
}
.formatter-table-scroll code {
overflow-wrap: anywhere;
}
.formatter-table-scroll > p,
.formatter-empty {
padding: 0.65rem;
}
.formatter-notices {
display: grid;
gap: 0.35rem;
margin: 0;
padding: 0 1.8rem 0.8rem 2.2rem;
color: var(--toolbox-muted);
font-size: 0.72rem;
line-height: 1.45;
}
.formatter-status {
margin: 0 0.8rem;
border-left: 3px solid var(--toolbox-accent);
padding: 0.65rem 0.75rem;
background: var(--toolbox-surface-soft);
font-size: 0.76rem;
}
.formatter-status.is-different {
border-left-color: var(--toolbox-danger);
}
.formatter-status.is-inconclusive {
border-left-color: var(--regex-warning);
}
.formatter-status p {
margin: 0.25rem 0 0;
color: var(--toolbox-danger);
}
.formatter-progress {
display: grid;
grid-template-columns: minmax(8rem, 1fr) auto;
align-items: center;
gap: 0.65rem;
padding: 0 0.8rem;
color: var(--toolbox-muted);
font-size: 0.7rem;
}
.formatter-progress progress {
width: 100%;
accent-color: var(--toolbox-accent);
}
.formatter-actions {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
padding: 0 0.8rem 0.8rem;
}
.formatter-result {
display: grid;
gap: 0.75rem;
border-top: 1px solid var(--toolbox-border);
padding-top: 0.75rem;
}
.formatter-result > dl {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
margin: 0;
padding: 0 0.8rem;
}
.formatter-result > dl > div {
padding: 0.5rem;
border-bottom: 1px solid var(--toolbox-border);
}
.formatter-result dt {
color: var(--toolbox-muted);
font-size: 0.65rem;
font-weight: 750;
text-transform: uppercase;
}
.formatter-result dd {
margin: 0.15rem 0 0;
font-size: 0.77rem;
}
.formatter-checks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 0.6rem;
padding: 0 0.8rem;
}
.formatter-check {
display: grid;
gap: 0.35rem;
min-width: 0;
border: 1px solid var(--toolbox-border);
border-left: 4px solid var(--toolbox-accent);
border-radius: calc(var(--toolbox-radius) * 0.7);
padding: 0.65rem;
background: var(--toolbox-surface-soft);
}
.formatter-check.is-different {
border-left-color: var(--toolbox-danger);
}
.formatter-check.is-inconclusive {
border-left-color: var(--regex-warning);
}
.formatter-check header {
display: flex;
justify-content: space-between;
gap: 0.6rem;
}
.formatter-check header span {
color: var(--toolbox-muted);
font-size: 0.65rem;
font-weight: 800;
text-transform: uppercase;
}
.formatter-check p,
.formatter-check small {
margin: 0;
color: var(--toolbox-muted);
font-size: 0.71rem;
line-height: 1.45;
}
.formatter-check code {
overflow-wrap: anywhere;
font-size: 0.67rem;
}
.formatter-result-note {
padding: 0 0.8rem;
}
.formatter-differences {
margin: 0 0.8rem;
border: 1px solid
color-mix(in srgb, var(--toolbox-danger) 35%, var(--toolbox-border));
border-radius: calc(var(--toolbox-radius) * 0.7);
padding: 0.7rem;
background: color-mix(
in srgb,
var(--toolbox-danger) 6%,
var(--toolbox-surface)
);
}
.formatter-differences h4 {
margin: 0 0 0.45rem;
font-size: 0.82rem;
}
.formatter-differences ul {
display: grid;
gap: 0.35rem;
margin: 0;
padding-left: 1.25rem;
color: var(--toolbox-muted);
font-size: 0.71rem;
line-height: 1.45;
}
.formatter-confirmation {
margin: 0 0.8rem;
border: 1px solid var(--toolbox-border);
border-radius: calc(var(--toolbox-radius) * 0.7);
padding: 0.65rem;
background: var(--toolbox-surface-soft);
color: var(--toolbox-muted);
font-size: 0.74rem;
line-height: 1.4;
}
@media (max-width: 720px) {
.formatter-pattern-grid {
grid-template-columns: 1fr;
}
.formatter-progress {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,245 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { EcmaScriptSyntaxProvider } from "../regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import type {
PatternFormatSnapshotCheck,
PatternFormatValidationResult,
} from "../regex/formatting/formatting.types";
import {
PatternFormatterPanel,
type PatternFormatValidatorClient,
type PatternFormatterPanelProps,
} from "./PatternFormatterPanel";
const provider = new EcmaScriptSyntaxProvider();
function sameCheck(): PatternFormatSnapshotCheck {
return {
comparison: "same",
before: {
status: "complete",
engineIdentity: "Native ECMAScript RegExp · fixture",
message: "Source completed.",
},
after: {
status: "complete",
engineIdentity: "Native ECMAScript RegExp · fixture",
message: "Candidate completed.",
},
summary: "Exact normalized match and replacement output is unchanged.",
mismatchKinds: [],
};
}
function validationResult(
overrides: Partial<PatternFormatValidationResult> = {},
): PatternFormatValidationResult {
return {
status: "equivalent",
canApply: true,
sourceSyntaxAccepted: true,
formattedSyntaxAccepted: true,
captureShapePreserved: true,
current: sameCheck(),
tests: [],
applicableTestCount: 0,
skippedTestCount: 0,
completedTestCount: 0,
differences: [],
elapsedMs: 3,
notices: [
"No enabled unit test has the exact active pattern; the current snapshot was compared.",
],
...overrides,
};
}
function validator(
result: PatternFormatValidationResult = validationResult(),
): PatternFormatValidatorClient {
return {
validate: vi.fn().mockImplementation(async (_input, onProgress) => {
onProgress?.(0, 0);
return result;
}),
cancel: vi.fn(),
dispose: vi.fn(),
};
}
async function renderFormatter(
overrides: Partial<PatternFormatterPanelProps> = {},
result: PatternFormatValidationResult = validationResult(),
) {
const pattern = overrides.pattern ?? "a/b";
const syntax =
overrides.syntax ??
(await provider.parsePattern({
flavour: "ecmascript",
flavourVersion: "2025",
pattern,
flags: [],
options: {},
}));
const currentValidator = validator(result);
const onApply = vi.fn();
const rendered = render(
<PatternFormatterPanel
flavour="ecmascript"
flavourVersion="2025"
pattern={pattern}
flags={[]}
options={{}}
syntax={syntax}
subject="a/b a/b"
replacement="[$&]"
scanAll
timeoutMs={2_000}
tests={[]}
onApply={onApply}
createValidator={() => currentValidator}
{...overrides}
/>,
);
return { ...rendered, currentValidator, onApply, syntax };
}
afterEach(cleanup);
describe("PatternFormatterPanel", () => {
it("shows an exact preview and requires validation plus confirmation before apply", async () => {
const user = userEvent.setup();
const { currentValidator, onApply } = await renderFormatter();
expect(screen.getByTestId("formatted-pattern-preview").textContent).toBe(
"a\\/b",
);
expect(
screen.queryByRole("button", { name: "Apply formatted pattern" }),
).not.toBeInTheDocument();
await user.click(
screen.getByRole("button", { name: "Validate exact snapshots" }),
);
await waitFor(() => {
expect(currentValidator.validate).toHaveBeenCalledTimes(1);
});
expect(
await screen.findByText(
"All completed safety gates retained the same observable behavior.",
),
).toBeVisible();
expect(screen.getByRole("checkbox")).toBeEnabled();
expect(
screen.getByRole("button", { name: "Apply formatted pattern" }),
).toBeDisabled();
await user.click(screen.getByRole("checkbox"));
await user.click(
screen.getByRole("button", { name: "Apply formatted pattern" }),
);
expect(onApply).toHaveBeenCalledWith("a\\/b");
});
it("keeps a behavior-changing candidate blocked", async () => {
const user = userEvent.setup();
await renderFormatter(
{},
validationResult({
status: "different",
canApply: false,
current: {
...sameCheck(),
comparison: "different",
summary: "Match count changed.",
mismatchKinds: ["match-count"],
},
differences: [
{
scope: "current-subject",
code: "current-behavior-changed",
summary: "Match count changed.",
},
],
}),
);
await user.click(
screen.getByRole("button", { name: "Validate exact snapshots" }),
);
expect(await screen.findByText("current-behavior-changed")).toBeVisible();
expect(screen.getByRole("checkbox")).toBeDisabled();
expect(
screen.getByRole("button", { name: "Apply formatted pattern" }),
).toBeDisabled();
});
it("reports PCRE2 and stale syntax snapshots as unavailable", async () => {
const { rerender, syntax } = await renderFormatter({
flavour: "pcre2",
syntax: undefined,
});
expect(
screen.getByText(/currently available only for ECMAScript/),
).toBeVisible();
expect(
screen.queryByRole("button", { name: "Validate exact snapshots" }),
).not.toBeInTheDocument();
rerender(
<PatternFormatterPanel
flavour="ecmascript"
flavourVersion="2025"
pattern="a/c"
flags={[]}
options={{}}
syntax={syntax}
subject=""
replacement=""
scanAll={false}
timeoutMs={2_000}
tests={[]}
onApply={vi.fn()}
/>,
);
expect(
screen.getByText(/accepted, non-recovered syntax-provider snapshot/),
).toBeVisible();
});
it("clears a validation result when an exact input snapshot changes", async () => {
const user = userEvent.setup();
const rendered = await renderFormatter();
await user.click(
screen.getByRole("button", { name: "Validate exact snapshots" }),
);
expect(await screen.findByRole("checkbox")).toBeVisible();
rendered.rerender(
<PatternFormatterPanel
flavour="ecmascript"
flavourVersion="2025"
pattern="a/b"
flags={[]}
options={{}}
syntax={rendered.syntax}
subject="different"
replacement="[$&]"
scanAll
timeoutMs={2_000}
tests={[]}
onApply={rendered.onApply}
createValidator={() => rendered.currentValidator}
/>,
);
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Validate exact snapshots" }),
).toBeEnabled();
});
});

View File

@@ -0,0 +1,617 @@
import { useEffect, useMemo, useRef, useState } from "react";
import type {
RegexEngineOptions,
RegexFlavourId,
} from "../regex/model/flavour";
import type { RegexSyntaxResult } from "../regex/model/syntax";
import type { RegexTestCase } from "../regex/tests/test-case.types";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { formatEcmaScriptPattern } from "../regex/formatting/ecmascript-format";
import { PatternFormatValidator } from "../regex/formatting/PatternFormatValidator";
import type {
PatternFormatPreview,
PatternFormatSnapshotCheck,
PatternFormatValidationInput,
PatternFormatValidationResult,
} from "../regex/formatting/formatting.types";
import "./PatternFormatterPanel.css";
const MAXIMUM_VISIBLE_CHANGES = 200;
const MAXIMUM_VISIBLE_CHECKS = 200;
export interface PatternFormatValidatorClient {
validate(
input: PatternFormatValidationInput,
onProgress?: (completed: number, total: number) => void,
): Promise<PatternFormatValidationResult>;
cancel(): void;
dispose(): void;
}
export interface PatternFormatterPanelProps {
readonly flavour: RegexFlavourId;
readonly flavourVersion?: string;
readonly pattern: string;
readonly flags: readonly string[];
readonly options: RegexEngineOptions;
readonly syntax?: RegexSyntaxResult;
readonly subject: string;
readonly replacement: string;
readonly scanAll: boolean;
readonly timeoutMs: number;
readonly tests: readonly RegexTestCase[];
readonly onApply: (formattedPattern: string) => void;
readonly onClose?: () => void;
readonly createValidator?: () => PatternFormatValidatorClient;
}
type PreviewState =
| { readonly status: "available"; readonly preview: PatternFormatPreview }
| { readonly status: "unavailable"; readonly reason: string };
interface ValidationConfiguration {
readonly flavourVersion?: string;
readonly preview: PatternFormatPreview;
readonly flags: readonly string[];
readonly options: RegexEngineOptions;
readonly subject: string;
readonly replacement: string;
readonly scanAll: boolean;
readonly timeoutMs: number;
readonly tests: readonly RegexTestCase[];
}
interface ValidationRecord {
readonly configuration: ValidationConfiguration;
readonly result: PatternFormatValidationResult;
}
interface ConfirmationRecord {
readonly configuration: ValidationConfiguration;
readonly checked: boolean;
}
function previewState(
flavour: RegexFlavourId,
pattern: string,
syntax: RegexSyntaxResult | undefined,
): PreviewState {
if (flavour !== "ecmascript") {
return {
status: "unavailable",
reason:
"Canonical formatting is currently available only for ECMAScript. PCRE2 and other flavours remain unchanged because no complete grammar-backed formatter is implemented for them.",
};
}
if (!syntax) {
return {
status: "unavailable",
reason:
"Wait for the ECMAScript syntax provider to finish parsing the current pattern.",
};
}
try {
return {
status: "available",
preview: formatEcmaScriptPattern(pattern, syntax),
};
} catch (cause) {
return {
status: "unavailable",
reason:
cause instanceof Error
? cause.message
: "The current syntax snapshot cannot be formatted.",
};
}
}
function formatDuration(milliseconds: number): string {
if (milliseconds < 1) return "<1 ms";
if (milliseconds < 1_000) return `${milliseconds.toFixed(0)} ms`;
return `${(milliseconds / 1_000).toFixed(2)} s`;
}
function observationIdentity(check: PatternFormatSnapshotCheck): string {
const before = check.before.engineIdentity ?? check.before.status;
const after = check.after.engineIdentity ?? check.after.status;
return before === after ? before : `${before}${after}`;
}
function ValidationCheck({
title,
check,
}: {
readonly title: string;
readonly check: PatternFormatSnapshotCheck;
}) {
return (
<article className={`formatter-check is-${check.comparison}`}>
<header>
<strong>{title}</strong>
<span>{check.comparison}</span>
</header>
<p>{check.summary}</p>
<small>{observationIdentity(check)}</small>
{check.mismatchKinds.length > 0 ? (
<code>{check.mismatchKinds.join(" · ")}</code>
) : null}
</article>
);
}
export function PatternFormatterPanel({
flavour,
flavourVersion,
pattern,
flags,
options,
syntax,
subject,
replacement,
scanAll,
timeoutMs,
tests,
onApply,
onClose,
createValidator = () => new PatternFormatValidator(),
}: PatternFormatterPanelProps) {
const preview = useMemo(
() => previewState(flavour, pattern, syntax),
[flavour, pattern, syntax],
);
const configuration = useMemo<ValidationConfiguration | undefined>(
() =>
preview.status === "available"
? {
flavourVersion,
preview: preview.preview,
flags,
options,
subject,
replacement,
scanAll,
timeoutMs,
tests,
}
: undefined,
[
flags,
flavourVersion,
options,
preview,
replacement,
scanAll,
subject,
tests,
timeoutMs,
],
);
const validator = useRef<PatternFormatValidatorClient | undefined>(undefined);
const revision = useRef(0);
const [running, setRunning] = useState(false);
const [progress, setProgress] = useState({ completed: 0, total: 0 });
const [message, setMessage] = useState(
"Review the exact preview before running bounded validation.",
);
const [error, setError] = useState<string>();
const [record, setRecord] = useState<ValidationRecord>();
const [confirmation, setConfirmation] = useState<ConfirmationRecord>();
const currentRecord =
record?.configuration === configuration ? record : undefined;
const confirmed =
confirmation !== undefined &&
confirmation.configuration === configuration &&
confirmation.checked;
useEffect(
() => () => {
revision.current += 1;
validator.current?.dispose();
validator.current = undefined;
},
[],
);
const cancel = () => {
revision.current += 1;
validator.current?.cancel();
setRunning(false);
setMessage("Validation cancelled; formatter workers were terminated.");
};
const runValidation = async () => {
if (
!configuration ||
!configuration.preview.changed ||
!configuration.preview.withinPatternLimit
) {
return;
}
const currentValidator =
validator.current ?? (validator.current = createValidator());
currentValidator.cancel();
revision.current += 1;
const runRevision = revision.current;
setRunning(true);
setConfirmation(undefined);
setError(undefined);
setRecord(undefined);
setProgress({ completed: 0, total: 0 });
setMessage(
"Reparsing, compiling and comparing exact source/candidate snapshots…",
);
try {
const result = await currentValidator.validate(
{
flavour: "ecmascript",
...(configuration.flavourVersion === undefined
? {}
: { flavourVersion: configuration.flavourVersion }),
sourcePattern: configuration.preview.sourcePattern,
formattedPattern: configuration.preview.formattedPattern,
flags: configuration.flags,
options: configuration.options,
subject: configuration.subject,
replacement: configuration.replacement,
scanAll: configuration.scanAll,
timeoutMs: configuration.timeoutMs,
tests: configuration.tests,
},
(completed, total) => {
if (revision.current !== runRevision) return;
setProgress({ completed, total });
setMessage(
total === 0
? "The current subject/replacement snapshot is being compared."
: `Validated ${completed.toLocaleString()} of ${total.toLocaleString()} applicable unit tests.`,
);
},
);
if (revision.current !== runRevision) return;
setRecord({ configuration, result });
setMessage(
result.status === "equivalent"
? "All completed safety gates retained the same observable behavior."
: result.status === "different"
? "Formatting changed at least one checked observation."
: "Validation was inconclusive; the candidate cannot be applied.",
);
} catch (cause) {
if (revision.current !== runRevision) return;
const cancelled =
cause instanceof WorkerRequestError && cause.kind === "cancelled";
setError(
cancelled
? "Validation was cancelled."
: cause instanceof Error
? cause.message
: "Formatter validation failed.",
);
setMessage(
cancelled
? "Validation cancelled; no candidate can be applied."
: "Validation failed; no candidate can be applied.",
);
} finally {
if (revision.current === runRevision) setRunning(false);
}
};
const close = () => {
if (running) cancel();
onClose?.();
};
const result = currentRecord?.result;
const canApply =
result?.canApply === true &&
confirmed &&
configuration?.preview.changed === true;
return (
<section
className="panel formatter-panel"
aria-labelledby="formatter-heading"
>
<header className="panel-heading">
<div>
<p className="eyebrow">
Grammar-backed preview + bounded proof gates
</p>
<h2 id="formatter-heading">Format pattern</h2>
</div>
<div className="panel-heading-actions">
<span className="provenance-badge">ECMAScript only</span>
{onClose ? (
<button
type="button"
className="icon-button"
aria-label="Close pattern formatter"
data-dialog-initial-focus
onClick={close}
>
×
</button>
) : null}
</div>
</header>
{preview.status === "unavailable" ? (
<p className="formatter-unavailable" role="status">
{preview.reason}
</p>
) : (
<div className="formatter-body">
<p className="formatter-advisory">
This deliberately narrow formatter escapes literal slashes and raw
control/line-separator characters. It does not insert insignificant
whitespaceECMAScript patterns generally have no such whitespace.
</p>
<section className="formatter-preview" aria-label="Format preview">
<header>
<div>
<p className="eyebrow">
{preview.preview.formatter.id} · v
{preview.preview.formatter.version}
</p>
<h3>Exact preview</h3>
</div>
<span>
{preview.preview.totalChanges.toLocaleString()} transformation
{preview.preview.totalChanges === 1 ? "" : "s"}
</span>
</header>
<div className="formatter-pattern-grid">
<article>
<strong>Source</strong>
<pre>
<code>{preview.preview.sourcePattern}</code>
</pre>
</article>
<article>
<strong>Formatted candidate</strong>
<pre data-testid="formatted-pattern-preview">
<code>{preview.preview.formattedPattern}</code>
</pre>
</article>
</div>
{preview.preview.changed ? (
<div className="formatter-table-scroll">
<table aria-label="Formatter transformations">
<thead>
<tr>
<th scope="col">UTF-16 range</th>
<th scope="col">Kind</th>
<th scope="col">Before</th>
<th scope="col">After</th>
</tr>
</thead>
<tbody>
{preview.preview.changes
.slice(0, MAXIMUM_VISIBLE_CHANGES)
.map((change) => (
<tr
key={`${change.startUtf16}:${change.endUtf16}:${change.kind}`}
title={change.explanation}
>
<td>
{change.startUtf16}{change.endUtf16}
</td>
<td>{change.kind.replaceAll("-", " ")}</td>
<td>
<code>{JSON.stringify(change.before)}</code>
</td>
<td>
<code>{JSON.stringify(change.after)}</code>
</td>
</tr>
))}
</tbody>
</table>
{preview.preview.totalChanges > MAXIMUM_VISIBLE_CHANGES ? (
<p>
The table shows the first{" "}
{MAXIMUM_VISIBLE_CHANGES.toLocaleString()} transformations.
</p>
) : null}
</div>
) : (
<p className="formatter-empty">
This pattern already uses the formatters canonical literal and
control escapes.
</p>
)}
<ul className="formatter-notices">
{preview.preview.warnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
</section>
{preview.preview.changed ? (
<section className="formatter-validation">
<header>
<div>
<p className="eyebrow">Required before apply</p>
<h3>Safety validation</h3>
</div>
<span>
{result?.status ?? (running ? "running" : "not run")}
</span>
</header>
<div
className={`formatter-status is-${result?.status ?? (error ? "different" : "idle")}`}
role="status"
aria-live="polite"
>
<strong>{message}</strong>
{error ? <p>{error}</p> : null}
</div>
{running ? (
<div className="formatter-progress">
<progress
max={Math.max(1, progress.total)}
value={Math.min(progress.completed, progress.total)}
/>
<span>
{progress.completed.toLocaleString()} /{" "}
{progress.total.toLocaleString()} tests
</span>
</div>
) : null}
<div className="formatter-actions">
<button
type="button"
className="primary-button"
disabled={
running ||
!preview.preview.withinPatternLimit ||
!preview.preview.changed
}
onClick={() => void runValidation()}
>
Validate exact snapshots
</button>
{running ? (
<button
type="button"
className="secondary-button"
onClick={cancel}
>
Cancel
</button>
) : null}
</div>
{result ? (
<div className="formatter-result">
<dl>
<div>
<dt>Syntax</dt>
<dd>
{result.sourceSyntaxAccepted &&
result.formattedSyntaxAccepted
? "both accepted"
: "rejected"}
</dd>
</div>
<div>
<dt>Capture shape</dt>
<dd>
{result.captureShapePreserved ? "preserved" : "changed"}
</dd>
</div>
<div>
<dt>Unit tests</dt>
<dd>
{result.completedTestCount.toLocaleString()} /{" "}
{result.applicableTestCount.toLocaleString()} applicable
</dd>
</div>
<div>
<dt>Unrelated / disabled</dt>
<dd>{result.skippedTestCount.toLocaleString()}</dd>
</div>
<div>
<dt>Elapsed</dt>
<dd>{formatDuration(result.elapsedMs)}</dd>
</div>
</dl>
<div className="formatter-checks">
<ValidationCheck
title="Current subject and replacement"
check={result.current}
/>
{result.tests
.slice(0, MAXIMUM_VISIBLE_CHECKS)
.map((test) => (
<ValidationCheck
key={test.testId}
title={test.name}
check={test}
/>
))}
</div>
{result.tests.length > MAXIMUM_VISIBLE_CHECKS ? (
<p className="formatter-result-note">
The panel shows the first{" "}
{MAXIMUM_VISIBLE_CHECKS.toLocaleString()} completed test
comparisons.
</p>
) : null}
{result.differences.length > 0 ? (
<section
className="formatter-differences"
aria-labelledby="formatter-differences-heading"
>
<h4 id="formatter-differences-heading">
Differences and inconclusive gates
</h4>
<ul>
{result.differences
.slice(0, MAXIMUM_VISIBLE_CHECKS)
.map((difference, index) => (
<li
key={`${difference.code}:${difference.testId ?? ""}:${index}`}
>
<strong>{difference.code}</strong>{" "}
{difference.summary}
</li>
))}
</ul>
</section>
) : null}
<ul className="formatter-notices">
{result.notices.map((notice) => (
<li key={notice}>{notice}</li>
))}
</ul>
<label className="check-control formatter-confirmation">
<input
type="checkbox"
checked={confirmed}
disabled={!result.canApply}
onChange={(event) => {
if (!configuration) return;
setConfirmation({
configuration,
checked: event.target.checked,
});
}}
/>
<span>
I understand that this bounded validation is not a proof
for every possible subject, and I want to apply the exact
candidate shown above.
</span>
</label>
<div className="formatter-actions">
<button
type="button"
className="primary-button"
disabled={!canApply}
onClick={() => {
if (!canApply || !configuration) return;
onApply(configuration.preview.formattedPattern);
}}
>
Apply formatted pattern
</button>
</div>
</div>
) : null}
</section>
) : null}
</div>
)}
</section>
);
}

View File

@@ -0,0 +1,59 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Pcre2TracePanel } from "./Pcre2TracePanel";
function renderPanel(flavour: "ecmascript" | "pcre2") {
return render(
<Pcre2TracePanel
active
flavour={flavour}
pattern="(a+)+$"
flags={["g"]}
options={
flavour === "pcre2"
? {
matchLimit: 1_000_000,
depthLimit: 1_000,
heapLimitKib: 32_768,
}
: {}
}
subject="aaaa!"
timeoutMs={2_000}
onClose={vi.fn()}
onSelectRanges={vi.fn()}
/>,
);
}
describe("Pcre2TracePanel", () => {
it("discloses reported versus derived trace semantics and hard caps", () => {
renderPanel("pcre2");
expect(
screen.getByRole("heading", { name: "Execution trace" }),
).toBeInTheDocument();
expect(
screen.getByText(/actual stream reported by PCRE2_AUTO_CALLOUT/u),
).toBeInTheDocument();
expect(
screen.getByText(/Movement labels are derived only/u),
).toBeInTheDocument();
expect(
screen.getByRole("spinbutton", { name: "Maximum events" }),
).toHaveAttribute("max", "50000");
expect(
screen.getByRole("spinbutton", {
name: "Maximum serialized bytes",
}),
).toHaveAttribute("max", String(10 * 1024 * 1024));
expect(screen.getByRole("button", { name: "Run trace" })).toBeEnabled();
});
it("does not offer an invented ECMAScript trace", () => {
renderPanel("ecmascript");
expect(screen.getByRole("button", { name: "Run trace" })).toBeDisabled();
expect(screen.getByText(/Select PCRE2 to enable/u)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,407 @@
import { useEffect, useRef, useState } from "react";
import { Pcre2TraceSupervisor } from "../regex/execution/Pcre2TraceSupervisor";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
import type {
RegexEngineOptions,
RegexFlavourId,
} from "../regex/model/flavour";
import type { Pcre2TraceEvent, Pcre2TraceResult } from "../regex/model/trace";
import type { SourceRange } from "../regex/model/syntax";
const DEFAULT_TRACE_EVENTS = 5_000;
const DEFAULT_TRACE_BYTES = 2 * 1024 * 1024;
const MAXIMUM_RENDERED_TRACE_EVENTS = 2_000;
type TraceState =
| { readonly status: "idle"; readonly message: string }
| { readonly status: "running"; readonly message: string }
| { readonly status: "ready"; readonly message: string }
| { readonly status: "timeout"; readonly message: string }
| { readonly status: "error"; readonly message: string };
function movementLabel(event: Pcre2TraceEvent): string {
switch (event.movement.classification) {
case "first-event":
return "first";
case "same-position":
return "same position";
case "apparent-backtrack":
return "apparent backtrack";
case "forward":
return "forward";
}
}
export function Pcre2TracePanel({
active,
flavour,
pattern,
flags,
options,
subject,
timeoutMs,
onClose,
onSelectRanges,
}: {
readonly active: boolean;
readonly flavour: RegexFlavourId;
readonly pattern: string;
readonly flags: readonly string[];
readonly options: RegexEngineOptions;
readonly subject: string;
readonly timeoutMs: number;
readonly onClose: () => void;
readonly onSelectRanges: (
patternRange: SourceRange,
subjectRange: SourceRange,
) => void;
}) {
const supervisor = useRef<Pcre2TraceSupervisor | null>(null);
const requestRevision = useRef(0);
const [maximumEvents, setMaximumEvents] = useState(DEFAULT_TRACE_EVENTS);
const [maximumBytes, setMaximumBytes] = useState(DEFAULT_TRACE_BYTES);
const [completed, setCompleted] = useState<{
readonly result: Pcre2TraceResult;
readonly input: {
readonly pattern: string;
readonly subject: string;
readonly flags: string;
readonly options: string;
};
}>();
const [state, setState] = useState<TraceState>({
status: "idle",
message: "Run an isolated PCRE2 automatic-callout trace.",
});
const traceEngine = () => {
supervisor.current ??= new Pcre2TraceSupervisor();
return supervisor.current;
};
useEffect(() => {
return () => supervisor.current?.dispose();
}, []);
useEffect(() => {
if (active) return;
supervisor.current?.cancel();
}, [active]);
const limitsValid =
Number.isSafeInteger(maximumEvents) &&
maximumEvents >= 1 &&
maximumEvents <= DEFAULT_REGEX_LIMITS.maximumTraceEvents &&
Number.isSafeInteger(maximumBytes) &&
maximumBytes >= 32 &&
maximumBytes <= DEFAULT_REGEX_LIMITS.maximumTraceBytes;
const runTrace = async () => {
if (flavour !== "pcre2" || !limitsValid) return;
const revision = (requestRevision.current += 1);
setCompleted(undefined);
setState({
status: "running",
message: "Collecting reported callouts in a dedicated worker…",
});
try {
const next = await traceEngine().trace(
{
flavour,
pattern,
flags,
options,
subject,
maximumTraceEvents: maximumEvents,
maximumTraceBytes: maximumBytes,
},
timeoutMs,
);
if (revision !== requestRevision.current) return;
setCompleted({
result: next,
input: {
pattern,
subject,
flags: flags.join(""),
options: JSON.stringify(options),
},
});
const error = next.diagnostics.find(
(diagnostic) => diagnostic.severity === "error",
);
setState(
next.accepted
? {
status: "ready",
message: next.truncated
? "Bounded trace complete; collection stopped at a configured cap."
: "Bounded trace complete.",
}
: {
status: "error",
message: error?.message ?? "PCRE2 rejected the trace request.",
},
);
} catch (error) {
if (revision !== requestRevision.current) return;
if (error instanceof WorkerRequestError && error.kind === "timeout") {
setState({
status: "timeout",
message: `${error.message}; the trace worker was terminated.`,
});
} else if (
error instanceof WorkerRequestError &&
error.kind === "cancelled"
) {
setState({
status: "idle",
message: "Trace cancelled; its worker was terminated.",
});
} else {
setState({
status: "error",
message:
error instanceof Error
? error.message
: "The PCRE2 trace worker failed.",
});
}
}
};
const resultIsCurrent =
completed?.input.pattern === pattern &&
completed.input.subject === subject &&
completed.input.flags === flags.join("") &&
completed.input.options === JSON.stringify(options);
const visibleResult = resultIsCurrent ? completed.result : undefined;
const visibleState =
resultIsCurrent || state.status === "running"
? state
: {
status: "idle" as const,
message: "Inputs changed; run a new isolated trace.",
};
const renderedEvents =
visibleResult?.events.slice(0, MAXIMUM_RENDERED_TRACE_EVENTS) ?? [];
return (
<section className="panel trace-panel" aria-labelledby="trace-heading">
<header className="panel-heading">
<div>
<p className="eyebrow">PCRE2 10.47 · reported automatic callouts</p>
<h2 id="trace-heading">Execution trace</h2>
</div>
<button
type="button"
className="icon-button"
aria-label="Close execution trace"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
</header>
<p className="trace-disclosure">
This is an actual stream reported by PCRE2_AUTO_CALLOUT for one{" "}
<code>pcre2_match()</code> invocation. Movement labels are derived only
from adjacent reported subject positions; the stream is not a complete
record of every internal engine action.
</p>
<div className="trace-controls">
<label>
<span>Maximum events</span>
<input
type="number"
min={1}
max={DEFAULT_REGEX_LIMITS.maximumTraceEvents}
step={1}
value={maximumEvents}
onChange={(event) => setMaximumEvents(Number(event.target.value))}
/>
</label>
<label>
<span>Maximum serialized bytes</span>
<input
type="number"
min={32}
max={DEFAULT_REGEX_LIMITS.maximumTraceBytes}
step={1024}
value={maximumBytes}
onChange={(event) => setMaximumBytes(Number(event.target.value))}
/>
</label>
<button
type="button"
className="primary-button"
disabled={
flavour !== "pcre2" ||
!limitsValid ||
visibleState.status === "running"
}
onClick={() => void runTrace()}
>
{visibleState.status === "running" ? "Tracing…" : "Run trace"}
</button>
{visibleState.status === "running" ? (
<button
type="button"
className="secondary-button"
onClick={() => {
requestRevision.current += 1;
supervisor.current?.cancel();
setState({
status: "idle",
message: "Trace cancelled; its worker was terminated.",
});
}}
>
Cancel trace
</button>
) : null}
</div>
{flavour !== "pcre2" ? (
<p className="empty-state" role="status">
Select PCRE2 to enable its automatic-callout trace.
</p>
) : null}
<div
className={`trace-status status-${visibleState.status}`}
role="status"
aria-live="polite"
>
<strong>{visibleState.message}</strong>
{visibleResult?.accepted ? (
<small>
{visibleResult.events.length.toLocaleString()} complete retained
events of {visibleResult.totalEventCount.toLocaleString()} callback
event(s) observed before return ·{" "}
{visibleResult.traceBytes.toLocaleString()} serialized bytes · last
complete event{" "}
{visibleResult.lastCompleteEvent?.toLocaleString() ?? "none"} ·{" "}
native status {visibleResult.nativeMatchStatus} ·{" "}
{visibleResult.matched ? "matched" : "not matched or bounded stop"}{" "}
· {visibleResult.elapsedMs.toFixed(2)} ms
</small>
) : null}
</div>
{visibleResult?.diagnostics.length ? (
<ul className="trace-diagnostics">
{visibleResult.diagnostics.map((diagnostic) => (
<li
key={diagnostic.id}
className={`diagnostic-${diagnostic.severity}`}
>
<strong>{diagnostic.severity}</strong> {diagnostic.message}
</li>
))}
</ul>
) : null}
{visibleResult && visibleResult.events.length > renderedEvents.length ? (
<p className="render-limit-banner" role="status">
<strong>Trace viewer limited.</strong> Showing the first{" "}
{renderedEvents.length.toLocaleString()} of{" "}
{visibleResult.events.length.toLocaleString()} complete retained
events. The worker result remains bounded by both configured caps.
</p>
) : null}
{renderedEvents.length > 0 ? (
<div className="trace-table-scroll">
<table className="trace-table">
<thead>
<tr>
<th scope="col">Event</th>
<th scope="col">Callout</th>
<th scope="col">Pattern</th>
<th scope="col">Next item</th>
<th scope="col">Subject</th>
<th scope="col">Captures</th>
<th scope="col">Mark</th>
<th scope="col">Movement</th>
</tr>
</thead>
<tbody>
{renderedEvents.map((event) => (
<tr key={event.eventNumber}>
<td>
<button
type="button"
className="trace-event-select"
aria-label={`Select trace event ${event.eventNumber}`}
onClick={() =>
onSelectRanges(
{
startUtf16:
event.reported.nextPatternItem.startUtf16,
endUtf16: event.reported.nextPatternItem.endUtf16,
},
{
startUtf16: event.reported.subjectPosition.utf16,
endUtf16: event.reported.subjectPosition.utf16,
},
)
}
>
{event.eventNumber}
</button>
</td>
<td>{event.reported.calloutNumber}</td>
<td>
{event.reported.patternPosition.utf16} UTF-16
<small>
{event.reported.patternPosition.nativeByte} byte
</small>
</td>
<td>
<code>
{pattern.slice(
event.reported.nextPatternItem.startUtf16,
event.reported.nextPatternItem.endUtf16,
) || "end"}
</code>
<small>
{event.reported.nextPatternItem.startNativeByte}
{event.reported.nextPatternItem.endNativeByte} bytes
</small>
</td>
<td>
{event.reported.subjectPosition.utf16} UTF-16
<small>
{event.reported.subjectPosition.nativeByte} byte
</small>
</td>
<td>
top {event.reported.captureTop} · last{" "}
{event.reported.captureLast}
</td>
<td>
{event.reported.mark ? (
<code>{event.reported.mark}</code>
) : (
"—"
)}
</td>
<td>
<span
className={`trace-movement trace-${event.movement.classification}`}
title={event.movement.explanation}
>
{movementLabel(event)}
</span>
<small>
{event.movement.subjectDeltaBytes > 0 ? "+" : ""}
{event.movement.subjectDeltaBytes} bytes · derived
</small>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : visibleResult?.accepted ? (
<p className="empty-state">No callout event was retained.</p>
) : null}
</section>
);
}

View File

@@ -65,6 +65,10 @@ export function ProjectPanel({
<p> <p>
<strong>{project.name}</strong> · schema {project.schemaVersion} <strong>{project.name}</strong> · schema {project.schemaVersion}
</p> </p>
<p className="field-help">
Corpus documents, applied outputs and corpus results are always excluded
from project JSON and local saves.
</p>
<label className="check-control"> <label className="check-control">
<input <input
type="checkbox" type="checkbox"

View File

@@ -5,9 +5,9 @@ import { QuickReference } from "./QuickReference";
describe("QuickReference", () => { describe("QuickReference", () => {
it("shows flavour, flags, example and authoritative source before insertion", async () => { it("shows flavour, flags, example and authoritative source before insertion", async () => {
const onInsert = vi.fn(); const onUsePattern = vi.fn();
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuickReference onInsert={onInsert} />); render(<QuickReference onUsePattern={onUsePattern} />);
await user.type( await user.type(
screen.getByLabelText("Filter constructs"), screen.getByLabelText("Filter constructs"),
@@ -20,7 +20,100 @@ describe("QuickReference", () => {
screen.getByRole("link", { name: "ECMAScript Atom grammar" }), screen.getByRole("link", { name: "ECMAScript Atom grammar" }),
).toHaveAttribute("href", expect.stringContaining("tc39.es")); ).toHaveAttribute("href", expect.stringContaining("tc39.es"));
await user.click(screen.getByRole("button", { name: "Insert example" })); await user.click(
expect(onInsert).toHaveBeenCalledWith("a.b"); screen.getByRole("button", { name: "Use example as pattern" }),
);
expect(onUsePattern).toHaveBeenCalledWith("a.b");
});
it("shows an explicit empty state for a filter without matches", async () => {
const user = userEvent.setup();
render(<QuickReference onUsePattern={vi.fn()} />);
await user.type(
screen.getByLabelText("Filter constructs"),
"no such construct",
);
expect(screen.getByRole("status")).toHaveTextContent(
"No reference constructs match",
);
expect(screen.getByText("0 of 12 constructs")).toBeInTheDocument();
});
it("switches to the authoritative PCRE2 construct set", async () => {
const user = userEvent.setup();
render(<QuickReference flavour="pcre2" onUsePattern={vi.fn()} />);
expect(screen.getByText("Original PCRE2 reference")).toBeInTheDocument();
expect(screen.getByText("8 of 8 constructs")).toBeInTheDocument();
await user.type(screen.getByLabelText("Filter constructs"), "branch reset");
expect(screen.getByText("Branch-reset group")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "PCRE2 pattern documentation" }),
).toHaveAttribute("href", expect.stringContaining("pcre.org"));
});
it("shows an original Python 3.14 re reference without borrowing ECMAScript syntax", async () => {
const user = userEvent.setup();
render(<QuickReference flavour="python" onUsePattern={vi.fn()} />);
expect(
screen.getByText("Original Python 3.14 re reference"),
).toBeInTheDocument();
expect(screen.getByText("8 of 8 constructs")).toBeInTheDocument();
await user.type(screen.getByLabelText("Filter constructs"), "absolute end");
expect(screen.getByText("Absolute end anchor")).toBeInTheDocument();
expect(screen.getByText("\\z")).toBeInTheDocument();
expect(screen.getByText("python")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Python 3.14 re documentation" }),
).toHaveAttribute("href", "https://docs.python.org/3.14/library/re.html");
});
it("shows an original Java Pattern reference from the official Java API", async () => {
const user = userEvent.setup();
render(<QuickReference flavour="java" onUsePattern={vi.fn()} />);
expect(
screen.getByText("Original Java Pattern reference"),
).toBeInTheDocument();
expect(screen.getByText("9 of 9 constructs")).toBeInTheDocument();
expect(
screen.getByText(
/bundled TeaVM 0\.15\.0 adapter is a tested compatibility subset/u,
),
).toBeInTheDocument();
await user.type(screen.getByLabelText("Filter constructs"), "intersection");
expect(
screen.getByText("Character-class intersection"),
).toBeInTheDocument();
expect(screen.getByText("[class&&class]")).toBeInTheDocument();
expect(screen.getByText("java")).toBeInTheDocument();
expect(
screen.getByRole("link", {
name: "Java SE 25 Pattern documentation",
}),
).toHaveAttribute(
"href",
"https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/regex/Pattern.html",
);
});
it("does not relabel an unreviewed flavour as ECMAScript", () => {
render(<QuickReference flavour="go" onUsePattern={vi.fn()} />);
expect(
screen.getByText("Original Go regexp reference"),
).toBeInTheDocument();
expect(screen.getByText("0 of 0 constructs")).toBeInTheDocument();
expect(screen.getByRole("status")).toHaveTextContent(
"No reviewed quick reference is available for Go regexp.",
);
expect(screen.queryByText("Any character")).not.toBeInTheDocument();
}); });
}); });

View File

@@ -1,16 +1,29 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { ECMASCRIPT_REFERENCE } from "../regex/reference/token-reference"; import type { RegexFlavourId } from "../regex/model/flavour";
import { REFERENCE_BY_FLAVOUR } from "../regex/reference/token-reference";
export function QuickReference({ export function QuickReference({
onInsert, onUsePattern,
onClose,
flavour = "ecmascript",
}: { }: {
readonly onInsert: (syntax: string) => void; readonly onUsePattern: (syntax: string) => void;
readonly onClose?: () => void;
readonly flavour?: RegexFlavourId;
}) { }) {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const referenceSet = REFERENCE_BY_FLAVOUR[flavour];
const reference = referenceSet.entries;
const entries = useMemo(() => { const entries = useMemo(() => {
const needle = query.toLocaleLowerCase(); const normalizeSearchText = (value: string) =>
value
.toLocaleLowerCase()
.replaceAll(/[^\p{L}\p{N}]+/gu, " ")
.trim();
const needle = normalizeSearchText(query);
return needle return needle
? ECMASCRIPT_REFERENCE.filter((entry) => ? reference.filter((entry) =>
normalizeSearchText(
[ [
entry.construct, entry.construct,
entry.syntax, entry.syntax,
@@ -18,13 +31,11 @@ export function QuickReference({
entry.example, entry.example,
entry.flags.join(" "), entry.flags.join(" "),
entry.source, entry.source,
] ].join(" "),
.join(" ") ).includes(needle),
.toLocaleLowerCase()
.includes(needle),
) )
: ECMASCRIPT_REFERENCE; : reference;
}, [query]); }, [query, reference]);
return ( return (
<section <section
className="panel reference-panel" className="panel reference-panel"
@@ -32,9 +43,25 @@ export function QuickReference({
> >
<header className="panel-heading"> <header className="panel-heading">
<div> <div>
<p className="eyebrow">Original ECMAScript reference</p> <p className="eyebrow">Original {referenceSet.label} reference</p>
<h2 id="reference-heading">Quick reference</h2> <h2 id="reference-heading">Quick reference</h2>
</div> </div>
<div className="panel-heading-actions">
<span>
{entries.length} of {reference.length} constructs
</span>
{onClose ? (
<button
type="button"
className="icon-button"
aria-label="Close quick reference"
data-dialog-initial-focus
onClick={onClose}
>
×
</button>
) : null}
</div>
</header> </header>
<label className="stacked-control"> <label className="stacked-control">
<span>Filter constructs</span> <span>Filter constructs</span>
@@ -44,6 +71,10 @@ export function QuickReference({
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
/> />
</label> </label>
{referenceSet.note ? (
<p className="reference-note">{referenceSet.note}</p>
) : null}
{entries.length > 0 ? (
<ul className="reference-list"> <ul className="reference-list">
{entries.map((entry) => ( {entries.map((entry) => (
<li key={entry.id}> <li key={entry.id}>
@@ -82,13 +113,20 @@ export function QuickReference({
<button <button
type="button" type="button"
className="secondary-button" className="secondary-button"
onClick={() => onInsert(entry.example)} onClick={() => onUsePattern(entry.example)}
> >
Insert example Use example as pattern
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
) : (
<p className="empty-state" role="status">
{reference.length === 0
? `No reviewed quick reference is available for ${referenceSet.label}.`
: `No reference constructs match “${query.trim()}”.`}
</p>
)}
</section> </section>
); );
} }

View File

@@ -51,6 +51,29 @@ function renderResult(result: RegexReplacementResult) {
} }
describe("ReplacementPanel completeness", () => { describe("ReplacementPanel completeness", () => {
it.each([
["python", "Python re"],
["java", "Java Pattern"],
] as const)(
"labels %s templates without relabelling them",
(flavour, label) => {
render(
<ReplacementPanel
flavour={flavour}
replacement=""
onReplacementChange={() => undefined}
onSelectToken={() => undefined}
onSelectContribution={() => undefined}
/>,
);
expect(screen.getByText(`${label} template syntax`)).toBeInTheDocument();
expect(
screen.getByText(`Native matches · bounded ${label} substitution`),
).toBeInTheDocument();
},
);
it("labels a match-limited replacement as partial", () => { it("labels a match-limited replacement as partial", () => {
renderResult({ renderResult({
execution: { ...execution, truncated: true }, execution: { ...execution, truncated: true },

Some files were not shown because too many files have changed in this diff Show More