Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7079cde15f | |||
| 4951c966d4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
release/
|
||||
.engine-build/
|
||||
engines/java/target/
|
||||
coverage/
|
||||
playwright-report/
|
||||
test-results/
|
||||
@@ -9,4 +11,3 @@ test-results/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
public/engines/pcre2/pcre2.mjs
|
||||
public/engines/java/
|
||||
public/engines/python/
|
||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,5 +1,83 @@
|
||||
# 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
|
||||
|
||||
- Added the open-source ECMAScript 2025 syntax provider using regexpp 4.12.2.
|
||||
|
||||
19
LICENSES/Emscripten-MIT.txt
Normal file
19
LICENSES/Emscripten-MIT.txt
Normal 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
104
LICENSES/PCRE2.txt
Normal 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.
|
||||
300
README.md
300
README.md
@@ -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,
|
||||
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
|
||||
grammar; application-owned structural explanations are partial and
|
||||
feature-matrix tested.
|
||||
- Execution engine: the current browser's native ECMAScript `RegExp`.
|
||||
- Native and editor offsets: UTF-16 code units.
|
||||
- ECMAScript native and editor offsets are 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.
|
||||
- Actual execution can be terminated by killing its worker.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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
|
||||
Tools says so and never invents capture history.
|
||||
|
||||
Browser ECMAScript engines do not expose their internal backtracking trace.
|
||||
Version 0.1.0 therefore presents no ECMAScript execution trace and no
|
||||
educational trace disguised as one.
|
||||
Browser ECMAScript exposes no actual engine trace. PCRE2 has a separate bounded
|
||||
automatic-callout viewer. Its event fields are reported by PCRE2, while
|
||||
“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
|
||||
|
||||
- **Match** — live highlighting, extraction tree and bounded capture table.
|
||||
- **Replace** — native ECMAScript `RegExp` matches with bounded,
|
||||
specification-compatible string substitution; output completeness is
|
||||
reported separately from bounded per-match original/result, changed-range and
|
||||
token-contribution previews that synchronize the replacement, pattern,
|
||||
subject and output editors.
|
||||
- **Replace** — engine-native bounded substitution for all four executable
|
||||
flavours;
|
||||
output completeness is reported separately from bounded per-match
|
||||
original/result, changed-range and token-contribution previews that
|
||||
synchronize the replacement, pattern, subject and output editors.
|
||||
- **List** — non-executable typed templates with text, CSV, JSON and NDJSON
|
||||
export; export is disabled when engine collection or bounded rendering is
|
||||
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,
|
||||
capture, replacement, timing and expected-timeout assertions. Capture a
|
||||
complete current result, select only cases to rerun, filter failures, and
|
||||
import/export a standalone validated JSON suite; execution stays in isolated
|
||||
workers.
|
||||
|
||||
Corpus, comparison, debugging, analysis, generation, minimization, formatting
|
||||
and code generation remain roadmap items. Controls for them are not exposed in
|
||||
this release.
|
||||
The auxiliary **PCRE trace**, ECMAScript **Analysis**, **Compare & code**,
|
||||
**Generate cases**, **Minimize** and **Format** workspaces are separate from
|
||||
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
|
||||
|
||||
Pattern and flags are stored separately; delimiters are presentation only.
|
||||
Native `g` and `y` behavior is preserved. The optional **Scan all** action is
|
||||
explicit. When neither `g` nor `y` is present it adds `g` only to that execution
|
||||
request, visibly records the internal flag, and does not change the saved user
|
||||
flags. The `d` flag may likewise be added internally to obtain exact indices
|
||||
without changing matching semantics.
|
||||
ECMAScript preserves native `g` and `y`; PCRE2 offers the application-level
|
||||
`g` iteration flag plus `i`, `m`, `s`, `x`, `U` and `J`. PCRE2 UTF/UCP mode is
|
||||
mandatory because browser strings are Unicode and partial-byte ranges cannot be
|
||||
represented safely in the editor. Python exposes application-level `g` plus
|
||||
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
|
||||
cannot loop forever.
|
||||
@@ -81,29 +155,63 @@ cannot loop forever.
|
||||
|
||||
Important defaults:
|
||||
|
||||
| Limit | Default |
|
||||
| ------------------------------ | -----------------: |
|
||||
| Live parse debounce | 120 ms |
|
||||
| Live execution debounce | 220 ms |
|
||||
| Live execution timeout | 250 ms |
|
||||
| Manual execution timeout | 2 s |
|
||||
| Advanced maximum timeout | 10 s |
|
||||
| Pattern hard limit | 64 Ki UTF-16 units |
|
||||
| Interactive subject hard limit | 16 MiB |
|
||||
| Replacement template | 64 Ki UTF-16 units |
|
||||
| List template | 16 Ki UTF-16 units |
|
||||
| Per-match replacement preview | 200 matches |
|
||||
| Replacement contributions | 2,000 |
|
||||
| Maximum matches | 10,000 |
|
||||
| Maximum capture groups | 1,000 |
|
||||
| Maximum capture rows | 100,000 |
|
||||
| Replacement preview | 64 MiB |
|
||||
| Project JSON document | 32 MiB UTF-8 |
|
||||
| Standalone test-suite JSON | 32 MiB UTF-8 |
|
||||
| In-memory unit-test suite | 32 MiB UTF-8 |
|
||||
| Tests per suite | 1,000 |
|
||||
| Retained message per test | 2 KiB UTF-8 |
|
||||
| Unit-test suite wall time | 60 s |
|
||||
| Limit | Default |
|
||||
| ------------------------------- | -----------------: |
|
||||
| Live parse debounce | 120 ms |
|
||||
| Live execution debounce | 220 ms |
|
||||
| Live execution timeout | 250 ms |
|
||||
| Manual execution timeout | 2 s |
|
||||
| Advanced maximum timeout | 10 s |
|
||||
| Pattern hard limit | 64 Ki UTF-16 units |
|
||||
| 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 |
|
||||
| List template | 16 Ki UTF-16 units |
|
||||
| Per-match replacement preview | 200 matches |
|
||||
| Replacement contributions | 2,000 |
|
||||
| Maximum matches | 10,000 |
|
||||
| Maximum capture groups | 1,000 |
|
||||
| Maximum capture rows | 100,000 |
|
||||
| Replacement preview | 64 MiB |
|
||||
| Project JSON document | 32 MiB UTF-8 |
|
||||
| Standalone test-suite JSON | 32 MiB UTF-8 |
|
||||
| In-memory unit-test suite | 32 MiB UTF-8 |
|
||||
| Tests per suite | 1,000 |
|
||||
| Retained message per test | 2 KiB UTF-8 |
|
||||
| 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
|
||||
“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
|
||||
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
|
||||
32 MiB aggregate bound. Corpus content is not supported or persisted in this
|
||||
release. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8
|
||||
32 MiB aggregate bound. Added generated tests retain their validated generator
|
||||
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
|
||||
read through an `updatedAt` index cursor; loading it does not materialize the
|
||||
complete project store.
|
||||
@@ -154,6 +266,13 @@ npm run test:conformance
|
||||
npm run test:browser
|
||||
npm run engines:build
|
||||
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 toolbox: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; they do not build Regex Tools source.
|
||||
|
||||
Version 0.1.0 has no WebAssembly. Its minimum CSP needs self-hosted scripts and
|
||||
workers, not `wasm-unsafe-eval`. See
|
||||
The PCRE2 and Python workers load only self-hosted JavaScript, WebAssembly and
|
||||
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).
|
||||
|
||||
## Flavour roadmap
|
||||
## Flavour support and boundaries
|
||||
|
||||
The next flavour is official PCRE2 10.47 WebAssembly. A technical spike from the
|
||||
signed official tag has already demonstrated Unicode matching, named captures,
|
||||
substitution, automatic and explicit callouts, and match/depth/heap limits.
|
||||
PCRE2 is not shipped until the application-owned bridge, byte/UTF-16 offsets,
|
||||
trace caps, browser tests and source/licence material meet the acceptance gate.
|
||||
PCRE2's application-owned ABI provides bounded compilation, matching, native
|
||||
substitution and separate automatic-callout tracing, copied caller-owned
|
||||
records, deterministic allocation cleanup and explicit result completeness.
|
||||
The exact source/toolchain build is reproducible offline and the verified pack
|
||||
is bundled with its metadata, checksums and licence.
|
||||
|
||||
Python, Go, Rust, .NET, Java and legacy PCRE follow only through their actual
|
||||
named runtimes. No flavour is emulated through JavaScript and relabelled.
|
||||
The Python slice executes actual CPython 3.14.2 `re` in Pyodide 314.0.3; it does
|
||||
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
|
||||
|
||||
@@ -198,7 +346,7 @@ notices; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) and
|
||||
No regex101 application source, branding, generated explanation prose,
|
||||
reference text or visual design is copied. RegexLib and RGXP.RU fixtures are not
|
||||
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
|
||||
|
||||
@@ -211,7 +359,25 @@ RegexHub fixture is shipped in v0.1.0.
|
||||
error node, not a fabricated multi-character span or partial provider AST.
|
||||
- Native JavaScript compile errors do not consistently expose a machine-readable
|
||||
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
|
||||
pattern’s 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
|
||||
bounded separately.
|
||||
- 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
|
||||
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.
|
||||
- Benchmark, risk analysis, generated cases, minimization and corpus processing
|
||||
are not in this release.
|
||||
- Corpus input accepts recognized UTF-8 text only. It does not auto-detect
|
||||
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
|
||||
[`docs/`](docs/).
|
||||
|
||||
30
SOURCE.md
30
SOURCE.md
@@ -1,9 +1,10 @@
|
||||
# Source identity
|
||||
|
||||
- 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>
|
||||
- Release tag: `v0.1.0`
|
||||
- Previous release tag: `v0.2.0`
|
||||
- Licence: GPL-3.0-or-later
|
||||
- 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,
|
||||
source-identity and attribution documents.
|
||||
|
||||
No PCRE2, Python, Go, Rust, .NET, Java or legacy-PCRE runtime is included in
|
||||
version 0.1.0.
|
||||
The engine-pack inputs and rebuild gates are documented in
|
||||
`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`.
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
# Third-party notices
|
||||
|
||||
Regex Tools original source is GPL-3.0-or-later. The production browser bundle
|
||||
also contains the compatible components below. Exact dependency resolution is
|
||||
recorded in `package-lock.json`.
|
||||
contains the compatible runtime components below.
|
||||
Exact dependency resolution is recorded in `package-lock.json`.
|
||||
|
||||
| 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-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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
| 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-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> |
|
||||
| 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> |
|
||||
| React / React DOM | 19.2.7 | MIT | Runtime | User interface; <https://github.com/facebook/react> |
|
||||
| `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> |
|
||||
| Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; <https://github.com/rolldown/rolldown> |
|
||||
|
||||
`@add-ideas/toolbox-testkit` 0.2.2 and the other test/build dependencies are
|
||||
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
|
||||
not installed or shipped. PCRE2 10.47 was used only in an external feasibility
|
||||
spike and is not present in the v0.1.0 artifact.
|
||||
not installed or shipped. PCRE2 source is not vendored; its generated
|
||||
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
|
||||
release package additionally carries the exact Vite and Rolldown legal files
|
||||
under `LICENSES/build/`, including Vite's bundled-dependency notices for code
|
||||
emitted by the production build.
|
||||
Licence texts and copyright notices for the ordinary bundled dependencies are
|
||||
in `LICENSES/`. Each engine pack carries its exact upstream licence/notice
|
||||
files alongside the runtime. The release package additionally carries the exact
|
||||
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
121
docs/ANALYSIS.md
Normal 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.
|
||||
@@ -5,9 +5,36 @@ boundaries.
|
||||
|
||||
```text
|
||||
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
|
||||
├─ 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
|
||||
└─ 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
|
||||
request ID and worker generation. A supervisor allows one active request,
|
||||
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
|
||||
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
|
||||
presentation and token-evaluation limits.
|
||||
|
||||
Native ranges are retained in the engine's declared unit. Browser editor ranges
|
||||
are half-open UTF-16 code-unit ranges. Reusable converters cover UTF-8 byte and
|
||||
Unicode code-point offsets for later flavours, including invalid-boundary
|
||||
rejection and lone-surrogate detection.
|
||||
Corpus documents are ephemeral React session state, not project state. A
|
||||
dedicated engine supervisor processes one document at a time and is terminated
|
||||
on cancellation, timeout or workspace exit. Batch orchestration retains only
|
||||
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
|
||||
Portal consumes the resulting immutable ZIP and never imports React source.
|
||||
|
||||
124
docs/COMPARISON_AND_CODEGEN.md
Normal file
124
docs/COMPARISON_AND_CODEGEN.md
Normal 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`.
|
||||
@@ -1,28 +1,165 @@
|
||||
# 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
|
||||
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.
|
||||
## Rebuilding PCRE2
|
||||
|
||||
## PCRE2 feasibility record
|
||||
The reproducible build accepts only:
|
||||
|
||||
The next flavour was spiked outside the application repository against official
|
||||
PCRE2 10.47:
|
||||
- PCRE2 tag `pcre2-10.47`, signed tag object
|
||||
`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`;
|
||||
- peeled commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429`;
|
||||
- licence `BSD-3-Clause WITH PCRE2-exception`;
|
||||
- Emscripten 6.0.4;
|
||||
- 8-bit library, Unicode enabled, JIT disabled.
|
||||
The source and compiler checkouts must already exist locally and be completely
|
||||
clean. No command clones, downloads or updates them:
|
||||
|
||||
Node and Chromium tests demonstrated version/config reporting, Unicode,
|
||||
named groups, global substitution, automatic and explicit callouts, and
|
||||
match/depth/heap errors. This was a technical spike only. No spike source,
|
||||
binary or PCRE2 source is included in v0.1.0.
|
||||
```sh
|
||||
npm run engines:pcre2:build -- \
|
||||
--source-dir /absolute/path/to/pcre2 \
|
||||
--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,
|
||||
allocation cleanup, exact source build, UTF-8/UTF-16 mapping, trace/output caps,
|
||||
worker recovery, deterministic assets, licences and browser conformance.
|
||||
Use `--force` only to replace the exact ignored `.engine-build/pcre2` output.
|
||||
The explicit install command first re-verifies that pack and then atomically
|
||||
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.
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
# 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
|
||||
results but not the runtime's internal backtracking events. Regex Tools does
|
||||
not label structural explanations, static hints or adjacent-result inference
|
||||
as an actual engine trace.
|
||||
The bundled CPython `re` and TeaVM `java.util.regex` bridges likewise expose
|
||||
compilation, bounded match/capture and replacement results, not internal engine
|
||||
steps. Regex Tools does not label their partial lexical syntax trees as traces.
|
||||
|
||||
The next PCRE2 slice will use actual automatic/explicit callout events from an
|
||||
application-owned bridge. Event count, serialized bytes, match/depth/heap
|
||||
limits and wall-clock termination must all be enforced. Classifications such
|
||||
as “apparent backtrack” will remain visibly derived from adjacent events.
|
||||
PCRE2 10.47 has a separate actual automatic-callout vertical. ABI version 3
|
||||
compiles the requested pattern with `PCRE2_AUTO_CALLOUT`, registers an
|
||||
application callback, and copies only:
|
||||
|
||||
- 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
|
||||
PCRE2’s 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.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Explanation model
|
||||
|
||||
The community provider converts regexpp nodes into a stable application AST
|
||||
inside the syntax worker. Nodes carry deterministic IDs, UTF-16 ranges, raw
|
||||
source, capture metadata, quantifier bounds, assertion kind, support status,
|
||||
provider identity and provenance.
|
||||
The ECMAScript community provider converts regexpp nodes into a stable
|
||||
application AST inside the syntax worker. Nodes carry deterministic IDs,
|
||||
UTF-16 ranges, raw source, capture metadata, quantifier bounds, assertion kind,
|
||||
support status, provider identity and provenance.
|
||||
|
||||
Explanations are original deterministic text selected by node type. Nullable
|
||||
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
|
||||
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
|
||||
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. PCRE2’s 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.
|
||||
|
||||
@@ -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
|
||||
would consume an incomplete value.
|
||||
|
||||
Repeated ECMAScript captures expose only the final retained capture. The UI
|
||||
labels this limitation and does not invent history. Later .NET support may add
|
||||
actual engine capture-history nodes without degrading them to this model.
|
||||
Repeated ECMAScript, PCRE2, Python and Java captures expose only the final
|
||||
retained capture. The UI labels this limitation and does not invent history.
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -1,20 +1,75 @@
|
||||
# Flavour support
|
||||
|
||||
| Flavour | Syntax | Execution | Replacement | Captures | Trace | 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 |
|
||||
| PCRE2 | Unavailable in release | Technical spike only; not shipped | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes |
|
||||
| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable |
|
||||
| Python `re` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned code points |
|
||||
| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes |
|
||||
| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes |
|
||||
| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Planned UTF-16 |
|
||||
| Java | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-16 |
|
||||
| Flavour | Syntax | Execution | Replacement | Captures | Trace | Analysis | Generated cases | Native offsets |
|
||||
| ---------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------- | -------------- |
|
||||
| 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 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable |
|
||||
| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned bytes |
|
||||
| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned bytes |
|
||||
| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Unavailable | Unavailable | Planned UTF-16 |
|
||||
|
||||
ECMAScript tests cover `g`, `y`, `u`, internal `d`, zero-length iteration,
|
||||
named groups, unmatched/empty/repeated groups, lookbehind, backreferences,
|
||||
Unicode properties, astral input, replacement and result truncation.
|
||||
ECMAScript tests cover its complete exposed flag matrix, internal indices,
|
||||
zero-length iteration, captures, lookbehind, backreferences, Unicode properties
|
||||
and bounded replacement.
|
||||
|
||||
Actual browser identity is displayed because ECMAScript behavior can evolve
|
||||
with the runtime. Syntax acceptance and engine compilation are separate;
|
||||
neither silently rewrites the pattern.
|
||||
PCRE2 tests cover `g`, `i`, `m`, `s`, `x`, `U`, `J`, mandatory UTF/UCP,
|
||||
branch-reset groups, recursion, `\K`, duplicate names, native substitution,
|
||||
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
58
docs/FORMATTING.md
Normal 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
138
docs/GENERATED_CASES.md
Normal 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 generator’s structural
|
||||
coverage claim.
|
||||
|
||||
## Determinism and seed
|
||||
|
||||
The seed is explicit text of 1–128 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
75
docs/MINIMIZATION.md
Normal 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.
|
||||
@@ -7,10 +7,19 @@ community
|
||||
```
|
||||
|
||||
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`
|
||||
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
|
||||
interface.
|
||||
interface. Partial coverage is exposed as metadata and never presented as a
|
||||
complete parser.
|
||||
|
||||
66
docs/PCRE2_NEXT_SLICES.md
Normal file
66
docs/PCRE2_NEXT_SLICES.md
Normal 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).
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
retain the bounded engine value. List export is disabled when a source value is incomplete, so a bounded
|
||||
preview cannot be mistaken for the full value. List generation additionally
|
||||
stops at 250,000 template-token evaluations, a 64 Ki UTF-16 row preview, or an
|
||||
8 Mi-unit aggregate materialization bound. The UI shows 500 list rows while an
|
||||
eligible export still contains every generated row; any incomplete result
|
||||
disables export.
|
||||
retain the bounded engine value. List export is disabled when a source value is
|
||||
incomplete, so a bounded preview cannot be mistaken for the full value. List
|
||||
generation additionally stops at 250,000 template-token evaluations, a 64 Ki
|
||||
UTF-16 row preview, or an 8 Mi-unit aggregate materialization bound. The UI
|
||||
shows 500 list rows while an eligible export still contains every generated
|
||||
row; any incomplete result disables export.
|
||||
Replacement output has a separate 64 MiB bound. Match/capture-limit
|
||||
incompleteness and output-byte truncation are tracked separately; either makes
|
||||
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
|
||||
descending cursor and validates only that record instead of calling `getAll()`.
|
||||
|
||||
Formal cold/warm benchmarks, p95 statistics and growth charts are deferred to
|
||||
the analysis milestone and will run in killable workers.
|
||||
Corpus runs accept at most 256 documents, 16 MiB per document and 256 MiB of
|
||||
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).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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:
|
||||
|
||||
@@ -9,11 +9,23 @@ Required delivery behavior:
|
||||
revalidation/no-cache resources.
|
||||
- Hashed Vite assets may use immutable long-term caching.
|
||||
- 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
|
||||
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/`.
|
||||
|
||||
When PCRE2 ships later, the smallest additional change will be
|
||||
`application/wasm` delivery and `script-src 'self' 'wasm-unsafe-eval'`.
|
||||
The package, manifest, tag, artifact URL and checksum must all use the v0.3.0
|
||||
identity. Historical artifact coordinates remain immutable and must not be
|
||||
overwritten or reused.
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# Reference implementations and sources
|
||||
|
||||
Inspection date: 2026-07-24.
|
||||
Inspection date: 2026-07-27.
|
||||
|
||||
| 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 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
@@ -17,5 +20,5 @@ Inspection date: 2026-07-24.
|
||||
| RegexLib | 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`.
|
||||
|
||||
@@ -18,6 +18,13 @@ on publication failure, replaces an existing exact version only with
|
||||
`--force`, and produces:
|
||||
|
||||
```text
|
||||
release/regex-tools-0.1.0.zip
|
||||
release/regex-tools-0.1.0.zip.sha256
|
||||
release/regex-tools-0.3.0.zip
|
||||
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.
|
||||
|
||||
@@ -9,6 +9,28 @@ untrusted.
|
||||
- A terminated worker is never reused.
|
||||
- Pattern, subject, replacement/list template, match, capture and output sizes
|
||||
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/list token decorations, rows, chips, diagnostics and 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
|
||||
retain at most 2 KiB UTF-8 per case. Oversized replacement output is not
|
||||
copied into an exact current-result draft.
|
||||
- No backend, uploads, telemetry, remote corpus URL, CDN, remote font, `eval`,
|
||||
`Function`, arbitrary command line or unsafe HTML rendering exists.
|
||||
- Corpus file input is local, recognized UTF-8 text only. It is bounded before
|
||||
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
|
||||
default-src 'self';
|
||||
script-src 'self';
|
||||
script-src 'self' 'wasm-unsafe-eval';
|
||||
worker-src 'self' blob:;
|
||||
connect-src 'self';
|
||||
img-src 'self' data:;
|
||||
@@ -44,8 +107,11 @@ base-uri 'none';
|
||||
form-action 'none';
|
||||
```
|
||||
|
||||
The Toolbox shell currently requires inline styles. No `unsafe-eval` or
|
||||
`wasm-unsafe-eval` is required until an actual WebAssembly flavour ships.
|
||||
The Toolbox shell currently requires inline styles. Corpus ZIP generation,
|
||||
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
|
||||
details are posted.
|
||||
|
||||
202
engines/java/LICENSE-TeaVM.txt
Normal file
202
engines/java/LICENSE-TeaVM.txt
Normal 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.
|
||||
11
engines/java/NOTICE-TeaVM.txt
Normal file
11
engines/java/NOTICE-TeaVM.txt
Normal 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
25
engines/java/README.md
Normal 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
84
engines/java/pom.xml
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
engines/pcre2/CMakeLists.txt
Normal file
71
engines/pcre2/CMakeLists.txt
Normal 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
27
engines/pcre2/README.md
Normal 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).
|
||||
910
engines/pcre2/pcre2_bridge.c
Normal file
910
engines/pcre2/pcre2_bridge.c
Normal 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;
|
||||
}
|
||||
236
engines/pcre2/pcre2_bridge.h
Normal file
236
engines/pcre2/pcre2_bridge.h
Normal 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
277
engines/python/bridge.py
Normal 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
|
||||
@@ -12,6 +12,7 @@ export default tseslint.config(
|
||||
"coverage",
|
||||
"test-results",
|
||||
"playwright-report",
|
||||
".engine-build",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
51
package-lock.json
generated
51
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "regex-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "regex-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.2.2",
|
||||
@@ -18,6 +18,7 @@
|
||||
"@codemirror/view": "6.43.6",
|
||||
"@eslint-community/regexpp": "4.12.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"fflate": "0.8.3",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7"
|
||||
},
|
||||
@@ -35,10 +36,10 @@
|
||||
"eslint": "10.7.0",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"eslint-plugin-react-refresh": "0.5.2",
|
||||
"fflate": "0.8.3",
|
||||
"globals": "17.7.0",
|
||||
"jsdom": "29.1.1",
|
||||
"prettier": "3.9.5",
|
||||
"pyodide": "314.0.3",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.64.0",
|
||||
"vite": "8.1.5",
|
||||
@@ -1385,6 +1386,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
@@ -2442,7 +2450,6 @@
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
@@ -3406,6 +3413,20 @@
|
||||
"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": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
@@ -4102,6 +4123,28 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "regex-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Albrecht Degering",
|
||||
@@ -32,6 +32,15 @@
|
||||
"test:conformance": "vitest run tests/conformance",
|
||||
"test:browser": "playwright test",
|
||||
"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",
|
||||
"manifest:generate": "node scripts/generate-toolbox-manifest.mjs",
|
||||
"manifest:check": "node scripts/generate-toolbox-manifest.mjs --check",
|
||||
@@ -51,6 +60,7 @@
|
||||
"@codemirror/view": "6.43.6",
|
||||
"@eslint-community/regexpp": "4.12.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"fflate": "0.8.3",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7"
|
||||
},
|
||||
@@ -68,10 +78,10 @@
|
||||
"eslint": "10.7.0",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"eslint-plugin-react-refresh": "0.5.2",
|
||||
"fflate": "0.8.3",
|
||||
"globals": "17.7.0",
|
||||
"jsdom": "29.1.1",
|
||||
"prettier": "3.9.5",
|
||||
"pyodide": "314.0.3",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.64.0",
|
||||
"vite": "8.1.5",
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# Engine assets
|
||||
|
||||
Regex Tools 0.1.0 executes ECMAScript through the browser's native `RegExp`
|
||||
engine. It therefore ships no external engine binary or WebAssembly pack.
|
||||
Regex Tools executes ECMAScript through the browser's native `RegExp` engine.
|
||||
It also ships three declared, self-hosted runtime packs:
|
||||
|
||||
Future flavour packs will be built from pinned, documented open-source
|
||||
revisions and placed in versioned subdirectories here.
|
||||
- `pcre2/`: official PCRE2 10.47, 8-bit WebAssembly;
|
||||
- `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.
|
||||
|
||||
202
public/engines/java/LICENSE.txt
Normal file
202
public/engines/java/LICENSE.txt
Normal 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.
|
||||
11
public/engines/java/NOTICE.txt
Normal file
11
public/engines/java/NOTICE.txt
Normal 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/).
|
||||
4
public/engines/java/SHA256SUMS
Normal file
4
public/engines/java/SHA256SUMS
Normal file
@@ -0,0 +1,4 @@
|
||||
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE.txt
|
||||
71bbe79ff78d0e5e64096acdd418589abfe886b8f827048dc655df438a57e477 NOTICE.txt
|
||||
719cb99761c8f0964835bf7b954fcf6e0df30ec14d2c06050c07c23a978d6d22 engine-metadata.json
|
||||
fd7974e7bde911ef0682cdc432d28e93c5db9d56f7268482a62e40a2ad86d62f java-regex.mjs
|
||||
118
public/engines/java/engine-metadata.json
Normal file
118
public/engines/java/engine-metadata.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
441
public/engines/java/java-regex.mjs
Normal file
441
public/engines/java/java-regex.mjs
Normal 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};
|
||||
104
public/engines/pcre2/LICENSE.txt
Normal file
104
public/engines/pcre2/LICENSE.txt
Normal 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.
|
||||
4
public/engines/pcre2/SHA256SUMS
Normal file
4
public/engines/pcre2/SHA256SUMS
Normal file
@@ -0,0 +1,4 @@
|
||||
197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811 LICENSE.txt
|
||||
8c7f67ad85893fcc545c40d496abcc82d1009b76c9b7bcc7bd9ab92828852d76 engine-metadata.json
|
||||
d31a5f1e0839955ae8321430eafe65da8cbdef832a1245bb71ccc5bb0d4b6b9c pcre2.mjs
|
||||
cc7c4b2b0038e137c86d2037e73f2302e56432cb09bc082e4f35bba221dd1889 pcre2.wasm
|
||||
90
public/engines/pcre2/engine-metadata.json
Normal file
90
public/engines/pcre2/engine-metadata.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
2
public/engines/pcre2/pcre2.mjs
Normal file
2
public/engines/pcre2/pcre2.mjs
Normal file
File diff suppressed because one or more lines are too long
BIN
public/engines/pcre2/pcre2.wasm
Normal file
BIN
public/engines/pcre2/pcre2.wasm
Normal file
Binary file not shown.
277
public/engines/python/LICENSE.cpython.txt
Normal file
277
public/engines/python/LICENSE.cpython.txt
Normal 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.
|
||||
373
public/engines/python/LICENSE.pyodide.txt
Normal file
373
public/engines/python/LICENSE.pyodide.txt
Normal 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.
|
||||
8
public/engines/python/SHA256SUMS
Normal file
8
public/engines/python/SHA256SUMS
Normal 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
|
||||
99
public/engines/python/engine-metadata.json
Normal file
99
public/engines/python/engine-metadata.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
1
public/engines/python/pyodide-lock.json
Normal file
1
public/engines/python/pyodide-lock.json
Normal file
File diff suppressed because one or more lines are too long
2
public/engines/python/pyodide.asm.mjs
Normal file
2
public/engines/python/pyodide.asm.mjs
Normal file
File diff suppressed because one or more lines are too long
BIN
public/engines/python/pyodide.asm.wasm
Normal file
BIN
public/engines/python/pyodide.asm.wasm
Normal file
Binary file not shown.
3
public/engines/python/pyodide.mjs
Normal file
3
public/engines/python/pyodide.mjs
Normal file
File diff suppressed because one or more lines are too long
BIN
public/engines/python/python_stdlib.zip
Normal file
BIN
public/engines/python/python_stdlib.zip
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.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.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
@@ -36,5 +36,15 @@
|
||||
"label": "Source",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,37 @@
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
buildPcre2Pack,
|
||||
parsePcre2BuildArguments,
|
||||
} from "./pcre2-engine-pack.mjs";
|
||||
|
||||
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(
|
||||
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();
|
||||
|
||||
if (
|
||||
packageJson.version !== "0.1.0" ||
|
||||
entries.length !== 1 ||
|
||||
entries[0] !== "README.md"
|
||||
typeof packageJson.version !== "string" ||
|
||||
entries.length !== 4 ||
|
||||
entries[0] !== "README.md" ||
|
||||
entries[1] !== "java" ||
|
||||
entries[2] !== "pcre2" ||
|
||||
entries[3] !== "python"
|
||||
) {
|
||||
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(
|
||||
"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.",
|
||||
);
|
||||
|
||||
@@ -7,14 +7,23 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sourcePath = join(root, "src", "toolbox", "manifest.source.json");
|
||||
const outputPath = join(root, "public", "toolbox-app.json");
|
||||
const packagePath = join(root, "package.json");
|
||||
const applicationVersionPath = join(root, "src", "version.ts");
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
const source = JSON.parse(await readFile(sourcePath, "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(
|
||||
`Manifest version ${source.version} differs from package version ${packageJson.version}`,
|
||||
`Version drift: manifest ${source.version}, application ${String(applicationVersion)}, package ${packageJson.version}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
16
scripts/install-java-engine.mjs
Normal file
16
scripts/install-java-engine.mjs
Normal 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}.`,
|
||||
);
|
||||
16
scripts/install-pcre2-engine.mjs
Normal file
16
scripts/install-pcre2-engine.mjs
Normal 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}.`,
|
||||
);
|
||||
16
scripts/install-python-engine.mjs
Normal file
16
scripts/install-python-engine.mjs
Normal 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}.`,
|
||||
);
|
||||
603
scripts/java-engine-pack.mjs
Normal file
603
scripts/java-engine-pack.mjs
Normal 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).`,
|
||||
);
|
||||
}
|
||||
@@ -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 gplVersion3TextSha256 =
|
||||
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
|
||||
const expectedApplicationVersion = "0.3.0";
|
||||
const requiredRootFiles = [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
@@ -332,13 +333,27 @@ async function collectReleaseEntries() {
|
||||
await readFile(path.join(root, "package.json"), "utf8"),
|
||||
);
|
||||
if (
|
||||
packageJson.version !== "0.1.0" ||
|
||||
packageJson.version !== expectedApplicationVersion ||
|
||||
packageJson.license !== "GPL-3.0-or-later" ||
|
||||
packageJson.repository?.url !==
|
||||
"git+https://git.add-ideas.de/zemion/regex-tools.git"
|
||||
) {
|
||||
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");
|
||||
if (
|
||||
!licence.includes("GNU GENERAL PUBLIC LICENSE") ||
|
||||
@@ -371,6 +386,28 @@ async function collectReleaseEntries() {
|
||||
"CHANGELOG.md",
|
||||
"SOURCE.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/vite-8.1.5/LICENSE.md",
|
||||
]) {
|
||||
|
||||
110
scripts/pcre2-codegen-golden.test.ts
Normal file
110
scripts/pcre2-codegen-golden.test.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
1018
scripts/pcre2-engine-pack.mjs
Normal file
1018
scripts/pcre2-engine-pack.mjs
Normal file
File diff suppressed because it is too large
Load Diff
89
scripts/pcre2-engine-pack.test.ts
Normal file
89
scripts/pcre2-engine-pack.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
572
scripts/python-engine-pack.mjs
Normal file
572
scripts/python-engine-pack.mjs
Normal 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}.`,
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const mediaTypes = new Map([
|
||||
[".css", "text/css; charset=utf-8"],
|
||||
[".html", "text/html; charset=utf-8"],
|
||||
[".js", "text/javascript; charset=utf-8"],
|
||||
[".mjs", "text/javascript; charset=utf-8"],
|
||||
[".json", "application/json; charset=utf-8"],
|
||||
[".svg", "image/svg+xml"],
|
||||
[".wasm", "application/wasm"],
|
||||
@@ -25,6 +26,14 @@ const testCatalogue = {
|
||||
theme: { mode: "system", brand: "add·ideas" },
|
||||
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) {
|
||||
const decoded = decodeURIComponent(requestPath);
|
||||
@@ -49,6 +58,7 @@ const server = createServer(async (request, response) => {
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
...productionSecurityHeaders,
|
||||
});
|
||||
response.end(JSON.stringify(testCatalogue));
|
||||
return;
|
||||
@@ -65,7 +75,7 @@ const server = createServer(async (request, response) => {
|
||||
"Content-Type":
|
||||
mediaTypes.get(path.extname(file)) ?? "application/octet-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Cross-Origin-Resource-Policy": "same-origin",
|
||||
...productionSecurityHeaders,
|
||||
});
|
||||
response.end(content);
|
||||
} catch {
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
import { lstat, readFile, readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
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 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 details = await lstat(engineDirectory);
|
||||
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,
|
||||
);
|
||||
if (
|
||||
entries.length !== 1 ||
|
||||
entries.length !== 4 ||
|
||||
!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(
|
||||
"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");
|
||||
if (
|
||||
!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.");
|
||||
}
|
||||
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.",
|
||||
);
|
||||
|
||||
521
src/components/AnalysisPanel.css
Normal file
521
src/components/AnalysisPanel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
248
src/components/AnalysisPanel.test.tsx
Normal file
248
src/components/AnalysisPanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
1245
src/components/AnalysisPanel.tsx
Normal file
1245
src/components/AnalysisPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
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 { RegexSyntaxResult } from "../regex/model/syntax";
|
||||
import { CapabilityPanel } from "./CapabilityPanel";
|
||||
@@ -78,11 +79,124 @@ describe("CapabilityPanel", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Fixture engine")).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("fixture")).toBeInTheDocument();
|
||||
|
||||
const replacement = screen.getByText("Replacement").closest("div");
|
||||
expect(replacement).toHaveTextContent("Unavailable");
|
||||
const compilation = screen.getByText("Compilation").closest("div");
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { 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(
|
||||
capabilities: RegexEngineCapabilities | undefined,
|
||||
@@ -35,23 +49,30 @@ function offsetLabel(
|
||||
export function CapabilityPanel({
|
||||
syntax,
|
||||
execution,
|
||||
onClose,
|
||||
}: {
|
||||
readonly syntax?: RegexSyntaxResult;
|
||||
readonly execution?: RegexExecutionResult;
|
||||
readonly onClose?: () => void;
|
||||
}) {
|
||||
const capabilities = execution?.engine.capabilities;
|
||||
const activeFlavour =
|
||||
execution?.engine.flavour ?? syntax?.root.support.flavour;
|
||||
const activeFlavourLabel = activeFlavour
|
||||
? FLAVOUR_LABELS[activeFlavour]
|
||||
: undefined;
|
||||
const rows = [
|
||||
[
|
||||
"Flavour",
|
||||
execution?.engine.flavour ??
|
||||
syntax?.root.support.flavour ??
|
||||
"ECMAScript (awaiting workers)",
|
||||
"Awaiting flavour workers",
|
||||
],
|
||||
[
|
||||
"Syntax provider",
|
||||
syntax
|
||||
? `${syntax.provider.id} ${syntax.provider.version}`
|
||||
: `regexpp ${REGEXPP_VERSION} (awaiting syntax worker)`,
|
||||
: "Awaiting syntax worker result",
|
||||
],
|
||||
["Syntax profile", SYNTAX_PROFILE],
|
||||
[
|
||||
@@ -65,9 +86,20 @@ export function CapabilityPanel({
|
||||
execution?.engine.engineName ?? "Awaiting first engine result",
|
||||
],
|
||||
[
|
||||
"Engine/runtime version",
|
||||
"Engine version",
|
||||
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)],
|
||||
["Compilation", capability(capabilities, "compilation")],
|
||||
["Matching", capability(capabilities, "matching")],
|
||||
@@ -83,13 +115,57 @@ export function CapabilityPanel({
|
||||
],
|
||||
[
|
||||
"Actual execution trace",
|
||||
capability(
|
||||
capabilities,
|
||||
"actualTrace",
|
||||
"the adapter exposes no engine trace",
|
||||
),
|
||||
activeFlavour === "pcre2"
|
||||
? capability(
|
||||
capabilities,
|
||||
"actualTrace",
|
||||
"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")],
|
||||
[
|
||||
"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;
|
||||
return (
|
||||
<section
|
||||
@@ -101,7 +177,20 @@ export function CapabilityPanel({
|
||||
<p className="eyebrow">Current provider and adapter metadata</p>
|
||||
<h2 id="capability-heading">Capabilities</h2>
|
||||
</div>
|
||||
<span className="provenance-badge">Community build</span>
|
||||
<div className="panel-heading-actions">
|
||||
<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>
|
||||
<dl className="capability-grid">
|
||||
{rows.map(([term, value]) => (
|
||||
@@ -112,10 +201,11 @@ export function CapabilityPanel({
|
||||
))}
|
||||
</dl>
|
||||
<p className="capability-roadmap">
|
||||
Next flavour: official PCRE2 10.47 WebAssembly with actual callout
|
||||
traces. Python, Go, Rust, .NET and Java remain unavailable until their
|
||||
named runtimes pass the same worker, offset, conformance and licensing
|
||||
gates.
|
||||
Availability is never inferred by substituting one flavour for another.
|
||||
Static analysis, generated cases and formatting are ECMAScript-only;
|
||||
automatic-callout tracing and reviewed C17 generation are PCRE2-only.
|
||||
Python and Java retain the engine identity and compatibility boundaries
|
||||
reported by their own workers.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
|
||||
331
src/components/ComparisonCodePanel.css
Normal file
331
src/components/ComparisonCodePanel.css
Normal 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));
|
||||
}
|
||||
}
|
||||
82
src/components/ComparisonCodePanel.test.tsx
Normal file
82
src/components/ComparisonCodePanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
884
src/components/ComparisonCodePanel.tsx
Normal file
884
src/components/ComparisonCodePanel.tsx
Normal 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 & 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>
|
||||
);
|
||||
}
|
||||
253
src/components/CorpusPanel.test.tsx
Normal file
253
src/components/CorpusPanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
1024
src/components/CorpusPanel.tsx
Normal file
1024
src/components/CorpusPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
314
src/components/GenerationPanel.css
Normal file
314
src/components/GenerationPanel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
326
src/components/GenerationPanel.test.tsx
Normal file
326
src/components/GenerationPanel.test.tsx
Normal 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 · 0–3/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();
|
||||
});
|
||||
});
|
||||
829
src/components/GenerationPanel.tsx
Normal file
829
src/components/GenerationPanel.tsx
Normal 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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
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";
|
||||
|
||||
const releaseSourceUrl = `https://git.add-ideas.de/zemion/regex-tools/src/tag/v${manifest.version}`;
|
||||
@@ -43,10 +47,19 @@ export function HelpDialog({
|
||||
<section>
|
||||
<h3>What runs where</h3>
|
||||
<p>
|
||||
Syntax parsing and actual ECMAScript execution run in separate,
|
||||
killable browser workers. Patterns, subjects and projects are never
|
||||
uploaded. There is no backend, account, telemetry, CDN dependency,
|
||||
or executable replacement function.
|
||||
Syntax parsing and active-engine execution run in separate, killable
|
||||
browser workers. ECMAScript uses the browser RegExp implementation;
|
||||
PCRE2 uses the bundled 10.47 WebAssembly engine; Python uses CPython
|
||||
3.14 through Pyodide; and Java uses TeaVM'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>
|
||||
</section>
|
||||
<section>
|
||||
@@ -56,8 +69,47 @@ export function HelpDialog({
|
||||
syntax provider. The extraction tree contains actual browser-engine
|
||||
matches and captures. Replacement mode adds a typed token tree and
|
||||
bounded per-match contribution/range mapping over those actual
|
||||
matches. None is an internal V8, SpiderMonkey or JavaScriptCore
|
||||
execution trace.
|
||||
matches. These views are not internal engine traces. The separate
|
||||
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'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>
|
||||
</section>
|
||||
<section>
|
||||
@@ -105,6 +157,78 @@ export function HelpDialog({
|
||||
<dt>Unit-test suite wall time</dt>
|
||||
<dd>60 seconds</dd>
|
||||
</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>
|
||||
<p>
|
||||
A timeout terminates the worker. It means “timed out”, never “no
|
||||
@@ -116,7 +240,15 @@ export function HelpDialog({
|
||||
<p>
|
||||
Ad-hoc test text is excluded from local saves and exports by
|
||||
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>
|
||||
</section>
|
||||
<section>
|
||||
|
||||
241
src/components/MinimizePanel.css
Normal file
241
src/components/MinimizePanel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
184
src/components/MinimizePanel.test.tsx
Normal file
184
src/components/MinimizePanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
870
src/components/MinimizePanel.tsx
Normal file
870
src/components/MinimizePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
71
src/components/ModalDialog.tsx
Normal file
71
src/components/ModalDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
342
src/components/PatternFormatterPanel.css
Normal file
342
src/components/PatternFormatterPanel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
245
src/components/PatternFormatterPanel.test.tsx
Normal file
245
src/components/PatternFormatterPanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
617
src/components/PatternFormatterPanel.tsx
Normal file
617
src/components/PatternFormatterPanel.tsx
Normal 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
|
||||
whitespace—ECMAScript 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 formatter’s 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>
|
||||
);
|
||||
}
|
||||
59
src/components/Pcre2TracePanel.test.tsx
Normal file
59
src/components/Pcre2TracePanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
407
src/components/Pcre2TracePanel.tsx
Normal file
407
src/components/Pcre2TracePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +65,10 @@ export function ProjectPanel({
|
||||
<p>
|
||||
<strong>{project.name}</strong> · schema {project.schemaVersion}
|
||||
</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">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -5,9 +5,9 @@ import { QuickReference } from "./QuickReference";
|
||||
|
||||
describe("QuickReference", () => {
|
||||
it("shows flavour, flags, example and authoritative source before insertion", async () => {
|
||||
const onInsert = vi.fn();
|
||||
const onUsePattern = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<QuickReference onInsert={onInsert} />);
|
||||
render(<QuickReference onUsePattern={onUsePattern} />);
|
||||
|
||||
await user.type(
|
||||
screen.getByLabelText("Filter constructs"),
|
||||
@@ -20,7 +20,100 @@ describe("QuickReference", () => {
|
||||
screen.getByRole("link", { name: "ECMAScript Atom grammar" }),
|
||||
).toHaveAttribute("href", expect.stringContaining("tc39.es"));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Insert example" }));
|
||||
expect(onInsert).toHaveBeenCalledWith("a.b");
|
||||
await user.click(
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
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({
|
||||
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 referenceSet = REFERENCE_BY_FLAVOUR[flavour];
|
||||
const reference = referenceSet.entries;
|
||||
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
|
||||
? ECMASCRIPT_REFERENCE.filter((entry) =>
|
||||
[
|
||||
entry.construct,
|
||||
entry.syntax,
|
||||
entry.explanation,
|
||||
entry.example,
|
||||
entry.flags.join(" "),
|
||||
entry.source,
|
||||
]
|
||||
.join(" ")
|
||||
.toLocaleLowerCase()
|
||||
.includes(needle),
|
||||
? reference.filter((entry) =>
|
||||
normalizeSearchText(
|
||||
[
|
||||
entry.construct,
|
||||
entry.syntax,
|
||||
entry.explanation,
|
||||
entry.example,
|
||||
entry.flags.join(" "),
|
||||
entry.source,
|
||||
].join(" "),
|
||||
).includes(needle),
|
||||
)
|
||||
: ECMASCRIPT_REFERENCE;
|
||||
}, [query]);
|
||||
: reference;
|
||||
}, [query, reference]);
|
||||
return (
|
||||
<section
|
||||
className="panel reference-panel"
|
||||
@@ -32,9 +43,25 @@ export function QuickReference({
|
||||
>
|
||||
<header className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Original ECMAScript reference</p>
|
||||
<p className="eyebrow">Original {referenceSet.label} reference</p>
|
||||
<h2 id="reference-heading">Quick reference</h2>
|
||||
</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>
|
||||
<label className="stacked-control">
|
||||
<span>Filter constructs</span>
|
||||
@@ -44,51 +71,62 @@ export function QuickReference({
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<ul className="reference-list">
|
||||
{entries.map((entry) => (
|
||||
<li key={entry.id}>
|
||||
<div>
|
||||
<strong>{entry.construct}</strong>
|
||||
<code>{entry.syntax}</code>
|
||||
</div>
|
||||
<p>{entry.explanation}</p>
|
||||
<dl className="reference-meta">
|
||||
{referenceSet.note ? (
|
||||
<p className="reference-note">{referenceSet.note}</p>
|
||||
) : null}
|
||||
{entries.length > 0 ? (
|
||||
<ul className="reference-list">
|
||||
{entries.map((entry) => (
|
||||
<li key={entry.id}>
|
||||
<div>
|
||||
<dt>Flavour</dt>
|
||||
<dd>{entry.flavours.join(", ")}</dd>
|
||||
<strong>{entry.construct}</strong>
|
||||
<code>{entry.syntax}</code>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Relevant flags</dt>
|
||||
<dd>
|
||||
{entry.flags.length > 0 ? entry.flags.join(", ") : "None"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Example</dt>
|
||||
<dd>
|
||||
<code>{entry.example}</code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>
|
||||
<a href={entry.sourceUrl} target="_blank" rel="noreferrer">
|
||||
{entry.source}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{entry.caveat ? <small>{entry.caveat}</small> : null}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => onInsert(entry.example)}
|
||||
>
|
||||
Insert example
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{entry.explanation}</p>
|
||||
<dl className="reference-meta">
|
||||
<div>
|
||||
<dt>Flavour</dt>
|
||||
<dd>{entry.flavours.join(", ")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Relevant flags</dt>
|
||||
<dd>
|
||||
{entry.flags.length > 0 ? entry.flags.join(", ") : "None"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Example</dt>
|
||||
<dd>
|
||||
<code>{entry.example}</code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>
|
||||
<a href={entry.sourceUrl} target="_blank" rel="noreferrer">
|
||||
{entry.source}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{entry.caveat ? <small>{entry.caveat}</small> : null}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => onUsePattern(entry.example)}
|
||||
>
|
||||
Use example as pattern
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,29 @@ function renderResult(result: RegexReplacementResult) {
|
||||
}
|
||||
|
||||
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", () => {
|
||||
renderResult({
|
||||
execution: { ...execution, truncated: true },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user