Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7079cde15f | |||
| 4951c966d4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
release/
|
release/
|
||||||
|
.engine-build/
|
||||||
|
engines/java/target/
|
||||||
coverage/
|
coverage/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
@@ -9,4 +11,3 @@ test-results/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
|||||||
3
.prettierignore
Normal file
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
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.3.0 — 2026-07-27
|
||||||
|
|
||||||
|
- Add a self-hosted Pyodide 314.0.3 worker containing CPython 3.14.2 `re`, with
|
||||||
|
runtime identity verification, bounded compilation/matching/replacement and
|
||||||
|
exact code-point-to-editor-UTF-16 range normalization.
|
||||||
|
- Add TeaVM 0.15.0's `java.util.regex` class-library implementation as a
|
||||||
|
verified self-hosted ES module, with bounded `Pattern`/`Matcher` execution,
|
||||||
|
native replacement and exact UTF-16 ranges.
|
||||||
|
- Identify the Java engine as TeaVM class-library code rather than OpenJDK and
|
||||||
|
report its documented `UNICODE_CHARACTER_CLASS`, single-digit replacement
|
||||||
|
and unavailable named-replacement compatibility boundaries.
|
||||||
|
- Add partial application-owned Python and Java lexical syntax and replacement
|
||||||
|
providers while keeping the selected runtime compiler authoritative.
|
||||||
|
- Separate heavyweight engine loading from the per-request execution deadline,
|
||||||
|
retain worker recovery after timeout/crash/cancellation and keep every
|
||||||
|
runtime local to the release.
|
||||||
|
- Add flavour-specific quick reference and capability content for all four
|
||||||
|
executable engines.
|
||||||
|
- Keep comparison, generated cases, code generation, minimization, analysis
|
||||||
|
and formatting behind their explicitly verified ECMAScript/PCRE2 boundaries;
|
||||||
|
Python and Java are never routed through another flavour as a fallback.
|
||||||
|
- Extend deterministic engine-pack provenance, checksums, licence notices,
|
||||||
|
release verification and nested-path delivery checks to the Python and Java
|
||||||
|
runtime assets.
|
||||||
|
|
||||||
|
## 0.2.0 — 2026-07-27
|
||||||
|
|
||||||
|
- Open Quick reference and Capabilities as populated, accessible in-viewport
|
||||||
|
dialogs with keyboard dismissal, focus handling and explicit empty states.
|
||||||
|
- Add flavour-neutral version, flag, option and execution-worker registries
|
||||||
|
and expose only the ECMAScript and fully wired PCRE2 runtimes.
|
||||||
|
- Validate flavour/version/options consistently across workbench requests,
|
||||||
|
projects and test suites, including legacy schema-v1 identities.
|
||||||
|
- Add a production local-first Corpus / Apply workspace with bounded UTF-8
|
||||||
|
multi-file and pasted-text input, sequential killable worker jobs, progress,
|
||||||
|
cancellation, explicit whole-document/independent-line semantics, preserved
|
||||||
|
line separators, bounded capture extraction and replacement previews,
|
||||||
|
per-document exact/partial summaries, content-free JSON/CSV/NDJSON reports
|
||||||
|
and privacy-gated exact-output downloads and bounded ZIP export.
|
||||||
|
- Keep corpus content and results ephemeral: project schema v1 stores only the
|
||||||
|
selected workspace mode and rejects injected corpus payloads.
|
||||||
|
- Add a deterministic, offline PCRE2 10.47 WebAssembly runtime with pinned
|
||||||
|
source/toolchain identities, provenance, licences and checksums.
|
||||||
|
- Add bounded PCRE2 compile, match and native-substitution ABI calls with
|
||||||
|
match/depth/heap caps, copied caller-owned records, cleanup on every exit,
|
||||||
|
UTF-8-to-UTF-16 range normalization and lone-surrogate rejection.
|
||||||
|
- Run PCRE2 in a dedicated killable/recoverable worker, expose its real
|
||||||
|
flavour-specific flags and options, and add lexical syntax/replacement
|
||||||
|
coverage, official quick reference, conformance and browser tests.
|
||||||
|
- Add an ABI-v3 PCRE2 automatic-callout operation with caller-owned event and
|
||||||
|
mark buffers, dual event/byte caps, native truncation stop, exact UTF-8 and
|
||||||
|
UTF-16 positions, a separate killable worker and a bounded viewer that keeps
|
||||||
|
reported fields distinct from derived movement labels.
|
||||||
|
- Add ECMAScript-scoped advisory static risk diagnostics plus cancellable
|
||||||
|
bounded growth probes and cold/warm native-engine benchmarks with median/p95,
|
||||||
|
separate timing/outcome plots, runtime identity and strict
|
||||||
|
input/output/wall limits.
|
||||||
|
- Add an ECMAScript/PCRE2 comparison orchestrator with exact per-side requests,
|
||||||
|
independent worker timeout outcomes, normalized-range match/capture
|
||||||
|
alignment, replacement differences and explicit non-comparable states.
|
||||||
|
- Add a reviewed PCRE2 10.47 8-bit C17 generator with exact UTF-8 byte arrays,
|
||||||
|
native/host limits, error handling, deterministic source identities and
|
||||||
|
compile/execute golden gates against the named toolchain.
|
||||||
|
- Add deterministic, cancellable failing-subject minimization for saved-test
|
||||||
|
failures, capture ranges, exact engine timeouts and ECMAScript/PCRE2
|
||||||
|
mismatches, with real-worker verification, explicit budgets and honest
|
||||||
|
transform-local minimality.
|
||||||
|
- Add deterministic, seed-addressed ECMAScript positive/negative case
|
||||||
|
generation from the normalized AST, with bounded synthesis, actual-engine
|
||||||
|
verification, explicit unsupported coverage, cancellable workers and
|
||||||
|
provenance-preserving unit-test integration.
|
||||||
|
- Add an idempotent grammar-backed ECMAScript literal/control formatter with an
|
||||||
|
exact preview, separate source/candidate syntax and engine workers, capture
|
||||||
|
shape checks, applicable unit-test replay, explicit inconclusive states and a
|
||||||
|
mandatory confirmation before apply.
|
||||||
|
|
||||||
## 0.1.0 — 2026-07-24
|
## 0.1.0 — 2026-07-24
|
||||||
|
|
||||||
- Added the open-source ECMAScript 2025 syntax provider using regexpp 4.12.2.
|
- Added the open-source ECMAScript 2025 syntax provider using regexpp 4.12.2.
|
||||||
|
|||||||
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.
|
||||||
256
README.md
256
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,
|
browser. There is no account, backend, upload, remote execution, telemetry,
|
||||||
runtime CDN, remote font, or cloud-save service.
|
runtime CDN, remote font, or cloud-save service.
|
||||||
|
|
||||||
## Current milestone
|
## Current release — 0.3.0
|
||||||
|
|
||||||
Version 0.1.0 is the first production flavour slice: ECMAScript.
|
The v0.3.0 release contains four executable engine slices:
|
||||||
|
|
||||||
|
- the current browser's native ECMAScript `RegExp`;
|
||||||
|
- official PCRE2 10.47 as a self-hosted 8-bit WebAssembly pack;
|
||||||
|
- CPython 3.14.2 `re` in the self-hosted Pyodide 314.0.3 runtime; and
|
||||||
|
- TeaVM 0.15.0's `java.util.regex` class-library implementation as a
|
||||||
|
self-hosted ES2015 module.
|
||||||
|
|
||||||
|
The Java flavour is TeaVM class-library code compiled to JavaScript. It is not
|
||||||
|
OpenJDK and is not advertised as an OpenJDK compatibility oracle.
|
||||||
|
|
||||||
- Syntax acceptance: `@eslint-community/regexpp` 4.12.2, ECMAScript 2025
|
- Syntax acceptance: `@eslint-community/regexpp` 4.12.2, ECMAScript 2025
|
||||||
grammar; application-owned structural explanations are partial and
|
grammar; application-owned structural explanations are partial and
|
||||||
feature-matrix tested.
|
feature-matrix tested.
|
||||||
- Execution engine: the current browser's native ECMAScript `RegExp`.
|
- ECMAScript native and editor offsets are UTF-16 code units.
|
||||||
- Native and editor offsets: UTF-16 code units.
|
- PCRE2 execution and substitution: pinned official PCRE2 10.47 compiled to a
|
||||||
|
self-hosted 8-bit WebAssembly pack with UTF/UCP always enabled.
|
||||||
|
- PCRE2 native UTF-8 byte ranges are retained and normalized to exact editor
|
||||||
|
UTF-16 ranges; inputs with lone surrogates are rejected instead of rewritten.
|
||||||
|
- PCRE2 match-step, depth and heap limits are configurable inside hard caps.
|
||||||
|
- PCRE2 automatic callouts are copied through a separate ABI operation and
|
||||||
|
displayed from a dedicated killable trace worker under 50,000-event and
|
||||||
|
10 MiB serialized-data hard caps.
|
||||||
|
- Python compilation, matching and replacement use the bundled CPython `re`
|
||||||
|
module. Native code-point ranges are retained and normalized to exact editor
|
||||||
|
UTF-16 ranges.
|
||||||
|
- Java compilation, matching and replacement use TeaVM's
|
||||||
|
`java.util.regex.Pattern` and `Matcher`. Native ranges are UTF-16 code units.
|
||||||
|
- TeaVM 0.15.0 replacement supports its actual single-digit `$n` subset.
|
||||||
|
`$12` is therefore `$1` followed by literal `2`; Java SE `${name}`
|
||||||
|
replacement is reported as unavailable rather than presented as OpenJDK
|
||||||
|
behavior.
|
||||||
|
- Python and Java use deliberately partial application-owned lexical syntax
|
||||||
|
providers; their actual runtime compiler remains authoritative.
|
||||||
- Parsing and execution run in separate workers.
|
- Parsing and execution run in separate workers.
|
||||||
- Actual execution can be terminated by killing its worker.
|
- Actual execution can be terminated by killing its worker.
|
||||||
- The worker is recreated after timeout, crash or cancellation.
|
- The worker is recreated after timeout, crash or cancellation.
|
||||||
- Capture ranges use the engine's `d` indices result.
|
- ECMAScript capture ranges use the engine's `d` indices result; the other
|
||||||
|
flavours return their native range records through bounded bridges.
|
||||||
- Named, numbered, optional, empty and repeated-final captures are represented.
|
- Named, numbered, optional, empty and repeated-final captures are represented.
|
||||||
- Match, replacement, typed list/extraction and unit-test modes are available.
|
- Match, replacement, typed list/extraction, corpus/apply and unit-test modes
|
||||||
|
are available.
|
||||||
|
- ECMAScript-specific advisory static risk findings, bounded dynamic growth
|
||||||
|
probes and cold/warm benchmarks run locally; timing and growth execution is
|
||||||
|
isolated in a separately killable worker.
|
||||||
|
- ECMAScript and PCRE2 can be compared through two exact, independently timed
|
||||||
|
requests without translating patterns, flags or replacements. Incomplete or
|
||||||
|
rejected sides are explicitly not comparable.
|
||||||
|
- The reviewed PCRE2 C17 generator emits exact UTF-8 byte arrays, configured
|
||||||
|
limits and error handling for the PCRE2 10.47 8-bit API.
|
||||||
|
- A cancellable, deterministic subject minimizer verifies saved-test failures,
|
||||||
|
capture-range failures, exact timeouts and ECMAScript/PCRE2 mismatches against
|
||||||
|
the selected real workers under explicit evaluation and wall budgets.
|
||||||
|
- Deterministic ECMAScript test-case generation samples the normalized AST from
|
||||||
|
an explicit seed, reports structural coverage and unsupported constructs, and
|
||||||
|
labels cases only after verification by the selected real engine worker.
|
||||||
- Project JSON import/export and optional IndexedDB save are available.
|
- Project JSON import/export and optional IndexedDB save are available.
|
||||||
- Interactive test text is not persisted by default.
|
- Interactive test text is not persisted by default.
|
||||||
|
|
||||||
@@ -40,39 +83,70 @@ hierarchy uses syntactic capture nesting while preserving actual engine ranges.
|
|||||||
ECMAScript exposes only the final retained capture of a repeated group; Regex
|
ECMAScript exposes only the final retained capture of a repeated group; Regex
|
||||||
Tools says so and never invents capture history.
|
Tools says so and never invents capture history.
|
||||||
|
|
||||||
Browser ECMAScript engines do not expose their internal backtracking trace.
|
Browser ECMAScript exposes no actual engine trace. PCRE2 has a separate bounded
|
||||||
Version 0.1.0 therefore presents no ECMAScript execution trace and no
|
automatic-callout viewer. Its event fields are reported by PCRE2, while
|
||||||
educational trace disguised as one.
|
“forward”, “same position” and “apparent backtrack” are visibly derived only
|
||||||
|
from adjacent reported subject positions. The callout stream is not described
|
||||||
|
as a complete record of every internal engine action.
|
||||||
|
|
||||||
## Workbench modes
|
## Workbench modes
|
||||||
|
|
||||||
- **Match** — live highlighting, extraction tree and bounded capture table.
|
- **Match** — live highlighting, extraction tree and bounded capture table.
|
||||||
- **Replace** — native ECMAScript `RegExp` matches with bounded,
|
- **Replace** — engine-native bounded substitution for all four executable
|
||||||
specification-compatible string substitution; output completeness is
|
flavours;
|
||||||
reported separately from bounded per-match original/result, changed-range and
|
output completeness is reported separately from bounded per-match
|
||||||
token-contribution previews that synchronize the replacement, pattern,
|
original/result, changed-range and token-contribution previews that
|
||||||
subject and output editors.
|
synchronize the replacement, pattern, subject and output editors.
|
||||||
- **List** — non-executable typed templates with text, CSV, JSON and NDJSON
|
- **List** — non-executable typed templates with text, CSV, JSON and NDJSON
|
||||||
export; export is disabled when engine collection or bounded rendering is
|
export; export is disabled when engine collection or bounded rendering is
|
||||||
incomplete.
|
incomplete.
|
||||||
|
- **Corpus / Apply** — bounded multi-file UTF-8 and pasted-text input processed
|
||||||
|
sequentially in killable workers. Whole-document and independent-line
|
||||||
|
semantics are explicit; line apply preserves original separators. Progress,
|
||||||
|
cancellation, bounded capture samples, replacement previews, per-document
|
||||||
|
exact/partial summaries, content-free JSON/CSV/NDJSON reports and
|
||||||
|
privacy-gated exact-output ZIP/downloads are available. Original files are
|
||||||
|
never modified.
|
||||||
- **Unit tests** — editable and cloneable versioned presence, count, full-match,
|
- **Unit tests** — editable and cloneable versioned presence, count, full-match,
|
||||||
capture, replacement, timing and expected-timeout assertions. Capture a
|
capture, replacement, timing and expected-timeout assertions. Capture a
|
||||||
complete current result, select only cases to rerun, filter failures, and
|
complete current result, select only cases to rerun, filter failures, and
|
||||||
import/export a standalone validated JSON suite; execution stays in isolated
|
import/export a standalone validated JSON suite; execution stays in isolated
|
||||||
workers.
|
workers.
|
||||||
|
|
||||||
Corpus, comparison, debugging, analysis, generation, minimization, formatting
|
The auxiliary **PCRE trace**, ECMAScript **Analysis**, **Compare & code**,
|
||||||
and code generation remain roadmap items. Controls for them are not exposed in
|
**Generate cases**, **Minimize** and **Format** workspaces are separate from
|
||||||
this release.
|
normal matching, replacement, tests and corpus execution. Trace collection is
|
||||||
|
never included in benchmark measurements.
|
||||||
|
Comparison retains native ranges while aligning actual results by normalized
|
||||||
|
editor UTF-16 ranges. Agreement is described only as evidence for the current
|
||||||
|
subject, never as proof of pattern equivalence.
|
||||||
|
Case generation is bounded sampling, not a proof of language coverage. Version
|
||||||
|
1 supports only a complete accepted ECMAScript normalized AST; the partial
|
||||||
|
PCRE2 provider is reported as unavailable rather than passed through or
|
||||||
|
relabeled. See
|
||||||
|
[`docs/GENERATED_CASES.md`](docs/GENERATED_CASES.md).
|
||||||
|
Minimization verifies every accepted candidate against the exact selected
|
||||||
|
worker request and claims only local minimality under its documented
|
||||||
|
Unicode-scalar transforms; see
|
||||||
|
[`docs/MINIMIZATION.md`](docs/MINIMIZATION.md).
|
||||||
|
Formatting is currently ECMAScript-only and changes only parser-reported
|
||||||
|
literal slash/control representations. Apply requires independent reparse,
|
||||||
|
actual-engine current-subject/replacement comparison, every applicable exact
|
||||||
|
unit test and explicit confirmation; see
|
||||||
|
[`docs/FORMATTING.md`](docs/FORMATTING.md).
|
||||||
|
|
||||||
## Flags and iteration
|
## Flags and iteration
|
||||||
|
|
||||||
Pattern and flags are stored separately; delimiters are presentation only.
|
Pattern and flags are stored separately; delimiters are presentation only.
|
||||||
Native `g` and `y` behavior is preserved. The optional **Scan all** action is
|
ECMAScript preserves native `g` and `y`; PCRE2 offers the application-level
|
||||||
explicit. When neither `g` nor `y` is present it adds `g` only to that execution
|
`g` iteration flag plus `i`, `m`, `s`, `x`, `U` and `J`. PCRE2 UTF/UCP mode is
|
||||||
request, visibly records the internal flag, and does not change the saved user
|
mandatory because browser strings are Unicode and partial-byte ranges cannot be
|
||||||
flags. The `d` flag may likewise be added internally to obtain exact indices
|
represented safely in the editor. Python exposes application-level `g` plus
|
||||||
without changing matching semantics.
|
CPython `re` flags `a`, `i`, `m`, `s` and `x`. Java exposes application-level
|
||||||
|
`g` plus TeaVM `Pattern` flags `i`, `d`, `m`, `s`, `u`, `x` and the documented
|
||||||
|
`U` compatibility request. The optional **Scan all** action adds only an
|
||||||
|
internal iteration request and does not change saved flags. ECMAScript may add
|
||||||
|
internal `d` to obtain exact indices without changing match semantics.
|
||||||
|
|
||||||
Zero-length global iteration advances using the selected Unicode semantics and
|
Zero-length global iteration advances using the selected Unicode semantics and
|
||||||
cannot loop forever.
|
cannot loop forever.
|
||||||
@@ -82,7 +156,7 @@ cannot loop forever.
|
|||||||
Important defaults:
|
Important defaults:
|
||||||
|
|
||||||
| Limit | Default |
|
| Limit | Default |
|
||||||
| ------------------------------ | -----------------: |
|
| ------------------------------- | -----------------: |
|
||||||
| Live parse debounce | 120 ms |
|
| Live parse debounce | 120 ms |
|
||||||
| Live execution debounce | 220 ms |
|
| Live execution debounce | 220 ms |
|
||||||
| Live execution timeout | 250 ms |
|
| Live execution timeout | 250 ms |
|
||||||
@@ -90,6 +164,16 @@ Important defaults:
|
|||||||
| Advanced maximum timeout | 10 s |
|
| Advanced maximum timeout | 10 s |
|
||||||
| Pattern hard limit | 64 Ki UTF-16 units |
|
| Pattern hard limit | 64 Ki UTF-16 units |
|
||||||
| Interactive subject hard limit | 16 MiB |
|
| Interactive subject hard limit | 16 MiB |
|
||||||
|
| Corpus documents | 256 |
|
||||||
|
| Per corpus document | 16 MiB |
|
||||||
|
| Aggregate corpus input | 256 MiB |
|
||||||
|
| Independent corpus lines | 100,000 |
|
||||||
|
| Aggregate corpus matches | 100,000 |
|
||||||
|
| Capture summaries per document | 32 |
|
||||||
|
| Samples per capture | 3 |
|
||||||
|
| Retained applied output | 64 MiB |
|
||||||
|
| Applied ZIP | 68 MiB |
|
||||||
|
| Corpus batch wall time | 5 min |
|
||||||
| Replacement template | 64 Ki UTF-16 units |
|
| Replacement template | 64 Ki UTF-16 units |
|
||||||
| List template | 16 Ki UTF-16 units |
|
| List template | 16 Ki UTF-16 units |
|
||||||
| Per-match replacement preview | 200 matches |
|
| Per-match replacement preview | 200 matches |
|
||||||
@@ -104,6 +188,30 @@ Important defaults:
|
|||||||
| Tests per suite | 1,000 |
|
| Tests per suite | 1,000 |
|
||||||
| Retained message per test | 2 KiB UTF-8 |
|
| Retained message per test | 2 KiB UTF-8 |
|
||||||
| Unit-test suite wall time | 60 s |
|
| Unit-test suite wall time | 60 s |
|
||||||
|
| PCRE2 trace events | 50,000 |
|
||||||
|
| PCRE2 serialized trace | 10 MiB |
|
||||||
|
| Comparison retained differences | 5,000 |
|
||||||
|
| Comparison retained alignments | 2,000 |
|
||||||
|
| Static analysis traversal | 20,000 nodes |
|
||||||
|
| Static analysis findings | 100 |
|
||||||
|
| Growth probe steps | 24 |
|
||||||
|
| Analysis aggregate wall time | 30 s |
|
||||||
|
| Generated verified cases | 48 |
|
||||||
|
| Generated candidate attempts | 192 |
|
||||||
|
| Generation aggregate wall time | 15 s |
|
||||||
|
| Generation aggregate candidates | 1 MiB |
|
||||||
|
| Minimizer subject | 1 MiB UTF-8 |
|
||||||
|
| Minimizer candidate evaluations | 2,000 |
|
||||||
|
| Minimizer aggregate wall time | 60 s |
|
||||||
|
| Formatter validation tests | 1,000 |
|
||||||
|
| Formatter test wall time | 60 s |
|
||||||
|
| Retained minimizer history | 200 |
|
||||||
|
|
||||||
|
Analysis defaults to three warm-up plus 15 measured samples (caps: 100 and
|
||||||
|
1,000), with a 2-second per-sample worker limit that can be raised to 10
|
||||||
|
seconds. Growth generation defaults to 1 MiB, preflights the central 16 MiB
|
||||||
|
subject cap and rejects more than 10,000,000 repetitions. Replacement timing
|
||||||
|
is skipped above an 8 Mi UTF-16-unit output estimate.
|
||||||
|
|
||||||
A timeout terminates actual execution. It is reported as **timed out**, never as
|
A timeout terminates actual execution. It is reported as **timed out**, never as
|
||||||
“no match”. The next request creates a fresh worker.
|
“no match”. The next request creates a fresh worker.
|
||||||
@@ -128,8 +236,12 @@ require separate explicit opt-ins for JSON export and IndexedDB save.
|
|||||||
Standalone test-suite JSON likewise omits subjects by default, is schema- and
|
Standalone test-suite JSON likewise omits subjects by default, is schema- and
|
||||||
field-validated on import, and appends at most 1,000 total tests. Add, update,
|
field-validated on import, and appends at most 1,000 total tests. Add, update,
|
||||||
clone and append-import also preflight the complete in-memory suite against a
|
clone and append-import also preflight the complete in-memory suite against a
|
||||||
32 MiB aggregate bound. Corpus content is not supported or persisted in this
|
32 MiB aggregate bound. Added generated tests retain their validated generator
|
||||||
release. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8
|
version, seed, candidate ID and intended outcome; editing one removes that
|
||||||
|
provenance. Schema v1 can remember that the corpus workspace was selected, but
|
||||||
|
corpus documents, contents, outputs and results are always ephemeral and
|
||||||
|
excluded from JSON and IndexedDB. Imports that try to inject those fields are
|
||||||
|
rejected. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8
|
||||||
JSON-document limit in addition to per-field limits. The latest local save is
|
JSON-document limit in addition to per-field limits. The latest local save is
|
||||||
read through an `updatedAt` index cursor; loading it does not materialize the
|
read through an `updatedAt` index cursor; loading it does not materialize the
|
||||||
complete project store.
|
complete project store.
|
||||||
@@ -154,6 +266,13 @@ npm run test:conformance
|
|||||||
npm run test:browser
|
npm run test:browser
|
||||||
npm run engines:build
|
npm run engines:build
|
||||||
npm run engines:verify
|
npm run engines:verify
|
||||||
|
npm run engines:pcre2:build -- --source-dir /path/to/pcre2 --emcc /path/to/emcc
|
||||||
|
npm run engines:pcre2:verify
|
||||||
|
npm run engines:pcre2:install
|
||||||
|
node scripts/python-engine-pack.mjs node_modules/pyodide .engine-build/python
|
||||||
|
node scripts/install-python-engine.mjs .engine-build/python
|
||||||
|
node scripts/java-engine-pack.mjs
|
||||||
|
node scripts/install-java-engine.mjs
|
||||||
npm run build
|
npm run build
|
||||||
npm run toolbox:check
|
npm run toolbox:check
|
||||||
npm run check
|
npm run check
|
||||||
@@ -173,20 +292,49 @@ The deterministic release ZIP can be consumed by Toolbox Portal using an exact
|
|||||||
artifact URL, manifest ID, version and SHA-256. Portal builds consume the
|
artifact URL, manifest ID, version and SHA-256. Portal builds consume the
|
||||||
artifact; they do not build Regex Tools source.
|
artifact; they do not build Regex Tools source.
|
||||||
|
|
||||||
Version 0.1.0 has no WebAssembly. Its minimum CSP needs self-hosted scripts and
|
The PCRE2 and Python workers load only self-hosted JavaScript, WebAssembly and
|
||||||
workers, not `wasm-unsafe-eval`. See
|
Python-standard-library assets; Java loads its self-hosted ES module.
|
||||||
|
Deployment must serve `.wasm` as `application/wasm` and allow WebAssembly
|
||||||
|
compilation in `script-src`; see
|
||||||
[`docs/PORTAL_REQUIREMENTS.md`](docs/PORTAL_REQUIREMENTS.md).
|
[`docs/PORTAL_REQUIREMENTS.md`](docs/PORTAL_REQUIREMENTS.md).
|
||||||
|
|
||||||
## Flavour roadmap
|
## Flavour support and boundaries
|
||||||
|
|
||||||
The next flavour is official PCRE2 10.47 WebAssembly. A technical spike from the
|
PCRE2's application-owned ABI provides bounded compilation, matching, native
|
||||||
signed official tag has already demonstrated Unicode matching, named captures,
|
substitution and separate automatic-callout tracing, copied caller-owned
|
||||||
substitution, automatic and explicit callouts, and match/depth/heap limits.
|
records, deterministic allocation cleanup and explicit result completeness.
|
||||||
PCRE2 is not shipped until the application-owned bridge, byte/UTF-16 offsets,
|
The exact source/toolchain build is reproducible offline and the verified pack
|
||||||
trace caps, browser tests and source/licence material meet the acceptance gate.
|
is bundled with its metadata, checksums and licence.
|
||||||
|
|
||||||
Python, Go, Rust, .NET, Java and legacy PCRE follow only through their actual
|
The Python slice executes actual CPython 3.14.2 `re` in Pyodide 314.0.3; it does
|
||||||
named runtimes. No flavour is emulated through JavaScript and relabelled.
|
not translate a Python pattern into ECMAScript. Pyodide startup is a separate
|
||||||
|
worker-load operation and is excluded from the normal execution deadline.
|
||||||
|
|
||||||
|
The Java slice executes TeaVM 0.15.0's own `java.util.regex` class library. It
|
||||||
|
does not translate a Java pattern into ECMAScript, but it is also not OpenJDK:
|
||||||
|
in particular, TeaVM 0.15.0 does not implement
|
||||||
|
`Pattern.UNICODE_CHARACTER_CLASS`. The UI reports the `U` flag as a
|
||||||
|
compatibility request and preserves TeaVM's predefined-class and word-boundary
|
||||||
|
behaviour.
|
||||||
|
|
||||||
|
Generated-case version 1 is ECMAScript-only. It traverses the complete
|
||||||
|
application-owned ECMAScript AST under node, depth, attempt, byte, repetition
|
||||||
|
and wall-time caps, then verifies every retained label through the actual
|
||||||
|
browser `RegExp` adapter. PCRE2 generation remains unavailable until its syntax
|
||||||
|
provider can expose a complete normalized tree.
|
||||||
|
|
||||||
|
Pattern formatting is likewise ECMAScript-only. It consumes the complete
|
||||||
|
regexpp token model and does not reinterpret the partial PCRE2, Python or Java
|
||||||
|
structures. Those flavours remain unavailable until complete grammar-backed
|
||||||
|
implementations pass equivalent idempotence, reparse, engine and test gates.
|
||||||
|
|
||||||
|
The first advertised generated-code target is the PCRE2 10.47 8-bit C API.
|
||||||
|
Its deterministic match and replacement fixtures compile and execute against
|
||||||
|
the exact official release. See
|
||||||
|
[`docs/COMPARISON_AND_CODEGEN.md`](docs/COMPARISON_AND_CODEGEN.md).
|
||||||
|
|
||||||
|
Go, Rust, .NET and legacy PCRE follow only through their actual named runtimes.
|
||||||
|
No flavour is emulated through JavaScript and relabelled.
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
@@ -198,7 +346,7 @@ notices; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) and
|
|||||||
No regex101 application source, branding, generated explanation prose,
|
No regex101 application source, branding, generated explanation prose,
|
||||||
reference text or visual design is copied. RegexLib and RGXP.RU fixtures are not
|
reference text or visual design is copied. RegexLib and RGXP.RU fixtures are not
|
||||||
redistributed because a suitable fixture licence was not established. No
|
redistributed because a suitable fixture licence was not established. No
|
||||||
RegexHub fixture is shipped in v0.1.0.
|
RegexHub fixture is shipped in v0.3.0.
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
@@ -211,7 +359,25 @@ RegexHub fixture is shipped in v0.1.0.
|
|||||||
error node, not a fabricated multi-character span or partial provider AST.
|
error node, not a fabricated multi-character span or partial provider AST.
|
||||||
- Native JavaScript compile errors do not consistently expose a machine-readable
|
- Native JavaScript compile errors do not consistently expose a machine-readable
|
||||||
source offset; the provider diagnostic supplies the range.
|
source offset; the provider diagnostic supplies the range.
|
||||||
- Capture history and actual ECMAScript execution tracing are unavailable.
|
- Capture history is unavailable. Browser ECMAScript exposes no engine trace;
|
||||||
|
PCRE2 exposes bounded automatic callouts, not a complete internal trace.
|
||||||
|
Movement classifications are derived and labelled as such.
|
||||||
|
- Static risk heuristics and generated-input observations apply only to
|
||||||
|
ECMAScript 2025 and are advisory. They are never proof of safety or of a
|
||||||
|
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
|
- Capture-table rendering is paged at 200 rows while aggregate collection is
|
||||||
bounded separately.
|
bounded separately.
|
||||||
- Explanation/extraction trees and editor decorations show bounded 2,000-node
|
- Explanation/extraction trees and editor decorations show bounded 2,000-node
|
||||||
@@ -235,8 +401,20 @@ RegexHub fixture is shipped in v0.1.0.
|
|||||||
- Unit-test failure messages retain at most 2 KiB UTF-8 per case, so 1,000
|
- Unit-test failure messages retain at most 2 KiB UTF-8 per case, so 1,000
|
||||||
results stay below the 2 MiB log budget. Oversized exact replacement results
|
results stay below the 2 MiB log budget. Oversized exact replacement results
|
||||||
offer a lightweight `should-match` draft when one is valid and fits the suite.
|
offer a lightweight `should-match` draft when one is valid and fits the suite.
|
||||||
- Benchmark, risk analysis, generated cases, minimization and corpus processing
|
- Corpus input accepts recognized UTF-8 text only. It does not auto-detect
|
||||||
are not in this release.
|
legacy encodings or recursively traverse directories.
|
||||||
|
- Corpus capture extraction is a bounded per-document summary: full match plus
|
||||||
|
at most 31 capture groups, three 160-unit samples each. Content-free reports
|
||||||
|
include counts and clipping metadata, never the samples themselves.
|
||||||
|
- Generated cases deliberately sample only ECMAScript and report
|
||||||
|
backreferences, lookaround, boundaries, complex Unicode sets and other
|
||||||
|
unsupported constraints instead of claiming exhaustive coverage.
|
||||||
|
- Pattern formatting is intentionally narrow and ECMAScript-only. Its bounded
|
||||||
|
source/candidate and unit-test validation is evidence for the checked
|
||||||
|
snapshots, not a proof of equivalence over all subjects.
|
||||||
|
- Cross-flavour comparison, PCRE2 C generation and subject minimization remain
|
||||||
|
scoped to their explicitly supported ECMAScript/PCRE2 paths; selecting
|
||||||
|
Python or Java does not silently substitute one of those engines.
|
||||||
|
|
||||||
Architecture, security, performance, provenance and flavour details live in
|
Architecture, security, performance, provenance and flavour details live in
|
||||||
[`docs/`](docs/).
|
[`docs/`](docs/).
|
||||||
|
|||||||
30
SOURCE.md
30
SOURCE.md
@@ -1,9 +1,10 @@
|
|||||||
# Source identity
|
# Source identity
|
||||||
|
|
||||||
- Project: Regex Tools
|
- Project: Regex Tools
|
||||||
- Version: 0.1.0
|
- Release version: `0.3.0`
|
||||||
|
- Release tag: `v0.3.0`
|
||||||
- Repository: <https://git.add-ideas.de/zemion/regex-tools>
|
- Repository: <https://git.add-ideas.de/zemion/regex-tools>
|
||||||
- Release tag: `v0.1.0`
|
- Previous release tag: `v0.2.0`
|
||||||
- Licence: GPL-3.0-or-later
|
- Licence: GPL-3.0-or-later
|
||||||
- Copyright: © 2026 Albrecht Degering
|
- Copyright: © 2026 Albrecht Degering
|
||||||
|
|
||||||
@@ -19,5 +20,26 @@ runtime or build-time CDN fetch. Complete preferred-form source is the tagged
|
|||||||
repository. The release archive contains compiled static assets plus legal,
|
repository. The release archive contains compiled static assets plus legal,
|
||||||
source-identity and attribution documents.
|
source-identity and attribution documents.
|
||||||
|
|
||||||
No PCRE2, Python, Go, Rust, .NET, Java or legacy-PCRE runtime is included in
|
The engine-pack inputs and rebuild gates are documented in
|
||||||
version 0.1.0.
|
`docs/ENGINE_BUILDS.md`.
|
||||||
|
|
||||||
|
- PCRE2 source is not vendored: the gate accepts only the pinned, clean
|
||||||
|
official 10.47 checkout and Emscripten 6.0.4 compiler.
|
||||||
|
- The Python pack is copied from the exact pinned Pyodide 314.0.3 npm package
|
||||||
|
and contains its CPython 3.14.2 runtime, Python standard library, loader,
|
||||||
|
WebAssembly module, metadata, checksums and licence material.
|
||||||
|
- The Java bridge source is under `engines/java/`. Its bundled module is
|
||||||
|
compiled against TeaVM 0.15.0's `java.util.regex` class library and is
|
||||||
|
identified as TeaVM rather than OpenJDK.
|
||||||
|
|
||||||
|
Generated staging is ignored. Only verified runtime packs with deterministic
|
||||||
|
metadata, closed file sets, checksums and exact licence/notice material are
|
||||||
|
installed under `public/engines/` and included in the application source and
|
||||||
|
build.
|
||||||
|
|
||||||
|
The v0.3.0 release includes native browser ECMAScript, PCRE2 10.47, CPython
|
||||||
|
3.14.2 `re` through Pyodide 314.0.3 and TeaVM 0.15.0 `java.util.regex`. Go,
|
||||||
|
Rust, .NET and legacy PCRE remain unimplemented. Earlier releases remain
|
||||||
|
available under their historical tags and artifact coordinates. A future
|
||||||
|
public artifact must bump package, manifest, tag and source identity together;
|
||||||
|
it must not reuse `v0.3.0`.
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
# Third-party notices
|
# Third-party notices
|
||||||
|
|
||||||
Regex Tools original source is GPL-3.0-or-later. The production browser bundle
|
Regex Tools original source is GPL-3.0-or-later. The production browser bundle
|
||||||
also contains the compatible components below. Exact dependency resolution is
|
contains the compatible runtime components below.
|
||||||
recorded in `package-lock.json`.
|
Exact dependency resolution is recorded in `package-lock.json`.
|
||||||
|
|
||||||
| Component | Version | Licence | Shipped | Role and source |
|
| Component | Version | Licence | Shipped | Role and source |
|
||||||
| -------------------------------- | ------------------------------------------------------------------------ | ---------- | ----------------- | ------------------------------------------------------------------------ |
|
| -------------------------------- | ------------------------------------------------------------------------ | --------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
| `@add-ideas/toolbox-contract` | 0.2.2 | Apache-2.0 | Runtime | Manifest contract; <https://git.add-ideas.de/zemion/toolbox-sdk> |
|
| `@add-ideas/toolbox-contract` | 0.2.2 | Apache-2.0 | Runtime | Manifest contract; <https://git.add-ideas.de/zemion/toolbox-sdk> |
|
||||||
| `@add-ideas/toolbox-shell-react` | 0.2.2 | Apache-2.0 | Runtime | Shared shell; same source |
|
| `@add-ideas/toolbox-shell-react` | 0.2.2 | Apache-2.0 | Runtime | Shared shell; same source |
|
||||||
| `@eslint-community/regexpp` | 4.12.2 | MIT | Syntax worker | ECMAScript parser; <https://github.com/eslint-community/regexpp> |
|
| `@eslint-community/regexpp` | 4.12.2 | MIT | Syntax worker | ECMAScript parser; <https://github.com/eslint-community/regexpp> |
|
||||||
| CodeMirror packages | state 6.7.1; view 6.43.6; language 6.12.4; commands 6.10.4; search 6.7.1 | MIT | Runtime | Editors; <https://github.com/codemirror> |
|
| CodeMirror packages | state 6.7.1; view 6.43.6; language 6.12.4; commands 6.10.4; search 6.7.1 | MIT | Runtime | Editors; <https://github.com/codemirror> |
|
||||||
| `@lezer/highlight` | 1.2.3 | MIT | Runtime | Editor highlighting support; <https://github.com/lezer-parser/highlight> |
|
| `@lezer/highlight` | 1.2.3 | MIT | Runtime | Editor highlighting support; <https://github.com/lezer-parser/highlight> |
|
||||||
| React / React DOM | 19.2.7 | MIT | Runtime | User interface; <https://github.com/facebook/react> |
|
| React / React DOM | 19.2.7 | MIT | Runtime | User interface; <https://github.com/facebook/react> |
|
||||||
| `fflate` | 0.8.3 | MIT | Packaging utility | Deterministic release ZIP; <https://github.com/101arrowz/fflate> |
|
| `fflate` | 0.8.3 | MIT | Runtime and build dependency | Local corpus-output ZIP and deterministic release ZIP; <https://github.com/101arrowz/fflate> |
|
||||||
|
| Emscripten generated runtime | 6.0.4 | MIT | Generated WebAssembly glue | PCRE2 module loader/runtime; <https://github.com/emscripten-core/emscripten> |
|
||||||
|
| PCRE2 | 10.47 | BSD-3-Clause WITH PCRE2-exception | WebAssembly runtime | Pinned official 8-bit engine; <https://github.com/PCRE2Project/pcre2> |
|
||||||
|
| Pyodide | 314.0.3 | MPL-2.0 | Python runtime pack | Self-hosted CPython browser runtime; <https://github.com/pyodide/pyodide> |
|
||||||
|
| CPython | 3.14.2 | PSF-2.0 | Python runtime/stdlib | `re` execution and bundled standard library; <https://github.com/python/cpython> |
|
||||||
|
| TeaVM | 0.15.0 | Apache-2.0 | Generated Java engine module | `java.util.regex` class-library implementation; <https://github.com/konsoletyper/teavm> |
|
||||||
| Vite | 8.1.5 | MIT | Generated helpers | Production build; <https://github.com/vitejs/vite> |
|
| Vite | 8.1.5 | MIT | Generated helpers | Production build; <https://github.com/vitejs/vite> |
|
||||||
| Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; <https://github.com/rolldown/rolldown> |
|
| Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; <https://github.com/rolldown/rolldown> |
|
||||||
|
|
||||||
@@ -20,10 +25,14 @@ recorded in `package-lock.json`.
|
|||||||
development-only and are not part of the static runtime bundle.
|
development-only and are not part of the static runtime bundle.
|
||||||
|
|
||||||
`recheck` 4.5.0 and `regexp-ast-analysis` 0.7.1 were inspected but deliberately
|
`recheck` 4.5.0 and `regexp-ast-analysis` 0.7.1 were inspected but deliberately
|
||||||
not installed or shipped. PCRE2 10.47 was used only in an external feasibility
|
not installed or shipped. PCRE2 source is not vendored; its generated
|
||||||
spike and is not present in the v0.1.0 artifact.
|
WebAssembly pack is shipped with exact metadata, checksums and licence. Pyodide
|
||||||
|
is pinned as an npm build input and its reviewed self-hosted runtime files are
|
||||||
|
copied into the Python pack. The TeaVM module contains its compiled class
|
||||||
|
library and is explicitly identified as TeaVM, not OpenJDK.
|
||||||
|
|
||||||
Corresponding licence texts and copyright notices are in `LICENSES/`. The
|
Licence texts and copyright notices for the ordinary bundled dependencies are
|
||||||
release package additionally carries the exact Vite and Rolldown legal files
|
in `LICENSES/`. Each engine pack carries its exact upstream licence/notice
|
||||||
under `LICENSES/build/`, including Vite's bundled-dependency notices for code
|
files alongside the runtime. The release package additionally carries the exact
|
||||||
emitted by the production build.
|
Vite and Rolldown legal files under `LICENSES/build/`, including Vite's
|
||||||
|
bundled-dependency notices for code emitted by the production build.
|
||||||
|
|||||||
121
docs/ANALYSIS.md
Normal file
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
|
```text
|
||||||
React workbench
|
React workbench
|
||||||
├─ Pattern SyntaxSupervisor → syntax.worker → Regexpp provider → normalized AST
|
├─ Pattern SyntaxSupervisor → syntax.worker
|
||||||
|
│ ├─ Regexpp ECMAScript provider → normalized AST
|
||||||
|
│ ├─ PCRE2 lexical provider → partial normalized structure
|
||||||
|
│ ├─ Python lexical provider → partial normalized structure
|
||||||
|
│ └─ Java lexical provider → partial normalized structure
|
||||||
├─ Replacement SyntaxSupervisor → syntax.worker → typed replacement tokens
|
├─ Replacement SyntaxSupervisor → syntax.worker → typed replacement tokens
|
||||||
├─ EngineSupervisor → ecmascript.worker → native RegExp → match DTOs
|
├─ EngineSupervisor
|
||||||
|
│ ├─ ecmascript.worker → native RegExp → match DTOs
|
||||||
|
│ ├─ pcre2.worker → PCRE2 10.47 WASM ABI → match DTOs
|
||||||
|
│ ├─ python.worker → Pyodide 314.0.3 → CPython 3.14.2 re → match DTOs
|
||||||
|
│ └─ java.worker → TeaVM 0.15.0 java.util.regex module → match DTOs
|
||||||
|
├─ Pcre2TraceSupervisor → pcre2-trace.worker
|
||||||
|
│ └─ separate ABI-v3 PCRE2_AUTO_CALLOUT operation → bounded trace DTOs
|
||||||
|
├─ AnalysisPanel
|
||||||
|
│ ├─ normalized-AST ECMAScript static heuristics
|
||||||
|
│ └─ AnalysisSupervisor → analysis.worker → native RegExp benchmark/growth probes
|
||||||
|
├─ ComparisonOrchestrator
|
||||||
|
│ ├─ two exact syntax snapshots + independently killable engine workers
|
||||||
|
│ └─ normalized UTF-16 match/capture alignment + explicit incomplete states
|
||||||
|
├─ PCRE2 C17 generator → validated snapshot → exact UTF-8 byte-array source
|
||||||
|
├─ SubjectMinimizer
|
||||||
|
│ ├─ fixed syntax snapshots + exact real-engine failure or comparison oracle
|
||||||
|
│ └─ deterministic Unicode-scalar reducer → bounded local-minimal result
|
||||||
|
├─ CaseGenerationOrchestrator
|
||||||
|
│ ├─ generation.worker → deterministic bounded ECMAScript AST candidates
|
||||||
|
│ └─ EngineSupervisor → selected actual-engine label verification
|
||||||
|
├─ ECMAScript literal/control formatter
|
||||||
|
│ ├─ exact regexpp literal-token ranges → idempotent candidate preview
|
||||||
|
│ └─ PatternFormatValidator → paired syntax + actual-engine workers and exact tests
|
||||||
|
├─ Corpus EngineSupervisor → sequential bounded document jobs → summaries/outputs
|
||||||
├─ Test SyntaxSupervisor + EngineSupervisor → isolated cancellable test runs
|
├─ Test SyntaxSupervisor + EngineSupervisor → isolated cancellable test runs
|
||||||
└─ project validator / serializer / IndexedDB persistence
|
└─ project validator / serializer / IndexedDB persistence
|
||||||
```
|
```
|
||||||
@@ -19,7 +46,72 @@ provenance. React never imports or traverses regexpp's cyclic AST.
|
|||||||
Every engine request carries a protocol version, monotonically increasing
|
Every engine request carries a protocol version, monotonically increasing
|
||||||
request ID and worker generation. A supervisor allows one active request,
|
request ID and worker generation. A supervisor allows one active request,
|
||||||
rejects stale responses, terminates on timeout/crash/cancel, and lazily creates
|
rejects stale responses, terminates on timeout/crash/cancel, and lazily creates
|
||||||
a new generation. Timeout is a distinct error state.
|
a new generation. Timeout is a distinct error state. A separate idempotent
|
||||||
|
load operation verifies a worker's runtime identity before execution; its
|
||||||
|
flavour-specific startup deadline is not charged to the 250 ms live or selected
|
||||||
|
manual execution deadline. A killed generation must load and verify again.
|
||||||
|
|
||||||
|
The Python worker imports only the self-hosted Pyodide loader and pack, then
|
||||||
|
evaluates the application-owned bounded bridge in CPython. The bridge returns
|
||||||
|
code-point ranges, which are retained as native values and converted only at
|
||||||
|
valid boundaries to editor UTF-16. The Java worker imports a verified TeaVM
|
||||||
|
ES2015 module. That module executes TeaVM's own `java.util.regex.Pattern` and
|
||||||
|
`Matcher` class-library implementation and returns native UTF-16 ranges; it is
|
||||||
|
not OpenJDK.
|
||||||
|
|
||||||
|
PCRE2 tracing is not an `EngineSupervisor` match or replacement operation. Its
|
||||||
|
own worker compiles with `PCRE2_AUTO_CALLOUT`, copies only complete fixed
|
||||||
|
records and bounded mark bytes, and stops natively at either trace cap.
|
||||||
|
Reported callout fields retain byte positions; movement classifications are a
|
||||||
|
separate derived UI layer. Normal execution, corpus, tests and benchmarks never
|
||||||
|
run through the trace operation.
|
||||||
|
|
||||||
|
Static analysis traverses the normalized ECMAScript tree on the UI side under
|
||||||
|
node/finding caps. Timing and generated-input growth probes use the independent
|
||||||
|
analysis worker. Timeout, cancellation or crash discards that worker; no trace
|
||||||
|
mode is benchmarked.
|
||||||
|
|
||||||
|
Comparison is a separate orchestration layer, not an `EngineSupervisor`
|
||||||
|
operation. It runs explicit ECMAScript and PCRE2 syntax/execution snapshots,
|
||||||
|
retains each engine identity and native range model, and aligns only normalized
|
||||||
|
editor ranges. Timeout, cancellation, compile rejection and truncation remain
|
||||||
|
distinct non-comparable outcomes. Shared patterns are sent unchanged;
|
||||||
|
per-flavour ports are user-owned variants.
|
||||||
|
|
||||||
|
The PCRE2 C generator is application-owned and never implements browser
|
||||||
|
execution. It validates one exact PCRE2 snapshot, emits user strings as
|
||||||
|
independent UTF-8 byte arrays, and states the host-level mappings that the
|
||||||
|
PCRE2 API cannot express directly. Its advertised C17 target is protected by
|
||||||
|
deterministic golden identities and an exact PCRE2 10.47 compile/execute gate.
|
||||||
|
|
||||||
|
Subject minimization is a main-thread bounded reducer whose expensive predicate
|
||||||
|
checks stay in independently terminable ECMAScript or PCRE2 workers. Fixed
|
||||||
|
pattern syntax is parsed once for capture metadata. Python and Java are blocked
|
||||||
|
at this feature boundary rather than routed to a different engine. The baseline
|
||||||
|
and every accepted candidate retain the exact target and engine identity.
|
||||||
|
Timeout, crash, cancellation, worker failure and incomplete output are separate
|
||||||
|
outcomes. Deterministic ddmin deletion is followed by a fixed-point local sweep
|
||||||
|
over Unicode-scalar deletion and lower-rank canonical replacement; only
|
||||||
|
completion of that sweep permits a transform-local minimality claim.
|
||||||
|
|
||||||
|
Generated-case synthesis and verification are separate trust boundaries. The
|
||||||
|
dedicated generation worker receives only a validated, complete ECMAScript
|
||||||
|
normalized tree plus explicit settings and seed. The main-thread orchestrator
|
||||||
|
then sends each bounded candidate to `EngineSupervisor`; only the selected real
|
||||||
|
adapter can establish a `should-match` or `should-not-match` label. Worker
|
||||||
|
timeout, crash, cancellation, compile rejection, wrong outcome and ordinary
|
||||||
|
failure stay distinct. Settings/results are ephemeral, while explicitly added
|
||||||
|
unit tests retain field-validated generator provenance.
|
||||||
|
|
||||||
|
Formatting is not an engine operation and never rewrites from heuristic text
|
||||||
|
alone. The ECMAScript formatter consumes an exact accepted regexpp snapshot and
|
||||||
|
changes only parser-reported literal/control token ranges. Its validator
|
||||||
|
reparses source and candidate separately, preserves capture shape, runs both in
|
||||||
|
separate actual-engine workers against the current replacement snapshot, then
|
||||||
|
replays every enabled test tied to the exact active pattern/version/flags/options.
|
||||||
|
Any difference, incomplete output, one-sided timeout, identity change or
|
||||||
|
inconclusive worker outcome blocks apply. The preview and observations are
|
||||||
|
ephemeral, and user confirmation is required even after all bounded gates pass.
|
||||||
|
|
||||||
Pattern and replacement syntax use independent supervisors. A pattern snapshot
|
Pattern and replacement syntax use independent supervisors. A pattern snapshot
|
||||||
is bound to its exact pattern, ordered flags and revision; execution cannot use
|
is bound to its exact pattern, ordered flags and revision; execution cannot use
|
||||||
@@ -32,10 +124,28 @@ over those actual matches, the complete replacement-token model and the bounded
|
|||||||
output. They execute no replacement callback or user code and have separate
|
output. They execute no replacement callback or user code and have separate
|
||||||
presentation and token-evaluation limits.
|
presentation and token-evaluation limits.
|
||||||
|
|
||||||
Native ranges are retained in the engine's declared unit. Browser editor ranges
|
Corpus documents are ephemeral React session state, not project state. A
|
||||||
are half-open UTF-16 code-unit ranges. Reusable converters cover UTF-8 byte and
|
dedicated engine supervisor processes one document at a time and is terminated
|
||||||
Unicode code-point offsets for later flavours, including invalid-boundary
|
on cancellation, timeout or workspace exit. Batch orchestration retains only
|
||||||
rejection and lone-surrogate detection.
|
per-document summaries and, for apply runs, bounded outputs. It enforces
|
||||||
|
document-count, per-document byte, aggregate input byte, aggregate match,
|
||||||
|
aggregate output and batch wall-time limits. Exact-output export is unavailable
|
||||||
|
if any document is partial, failed, cancelled or not run.
|
||||||
|
|
||||||
|
Whole-document mode sends each document to the engine once. Independent-line
|
||||||
|
mode segments CRLF, CR, LF, U+2028 and U+2029 without discarding them, executes
|
||||||
|
each logical line as its own subject, and reattaches the exact original
|
||||||
|
separator during apply. This makes anchor and cross-line behavior deliberately
|
||||||
|
different rather than silently rewriting the pattern. Match DTOs are reduced to
|
||||||
|
bounded per-group participation counts and samples before the next document.
|
||||||
|
|
||||||
|
Native ranges are retained in the engine's declared unit. ECMAScript and Java
|
||||||
|
use UTF-16 code units. PCRE2 always uses UTF/UCP and exposes UTF-8 byte ranges;
|
||||||
|
its ABI copies scalar records and names out of WebAssembly before returning and
|
||||||
|
retains no code pointer. CPython `re` exposes Unicode code-point indices.
|
||||||
|
Browser editor ranges are half-open UTF-16 code-unit ranges. Reusable
|
||||||
|
converters cover UTF-8 byte and Unicode code-point offsets, including
|
||||||
|
invalid-boundary rejection and lone-surrogate detection.
|
||||||
|
|
||||||
The release boundary is `dist/` plus checked-in legal/source documents. The
|
The release boundary is `dist/` plus checked-in legal/source documents. The
|
||||||
Portal consumes the resulting immutable ZIP and never imports React source.
|
Portal consumes the resulting immutable ZIP and never imports React source.
|
||||||
|
|||||||
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
|
# Engine builds
|
||||||
|
|
||||||
Version 0.1.0 ships no WebAssembly engine.
|
The production application uses the browser's native `RegExp` for ECMAScript
|
||||||
|
and bundles verified PCRE2, Python and Java runtime packs. Normal application
|
||||||
|
builds never download or compile an engine: reviewed files under
|
||||||
|
`public/engines/` are copied into `dist/` and verified there.
|
||||||
|
|
||||||
`npm run engines:build` verifies that this release has no external pack to
|
## Rebuilding PCRE2
|
||||||
build. After the production build, `npm run engines:verify` rejects any
|
|
||||||
undeclared engine asset and confirms the packaged ECMAScript-only notice.
|
|
||||||
Neither command accesses the network.
|
|
||||||
|
|
||||||
## PCRE2 feasibility record
|
The reproducible build accepts only:
|
||||||
|
|
||||||
The next flavour was spiked outside the application repository against official
|
- PCRE2 tag `pcre2-10.47`, signed tag object
|
||||||
PCRE2 10.47:
|
`cd007b4466798f66d479d1442a407099e7c40050`, peeled commit
|
||||||
|
`f454e231fe5006dd7ff8f4693fd2b8eb94333429` and tree
|
||||||
|
`81a83a3552bd68d0ea7b7004f8bb6e7892f583ba`;
|
||||||
|
- Emscripten 6.0.4, compiler revision
|
||||||
|
`fe5be6afdff43ad58860d821fcc8572a23f92d19`, from emsdk commit
|
||||||
|
`224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f`;
|
||||||
|
- CMake 4.3.4 and Ninja 1.13.2;
|
||||||
|
- 8-bit PCRE2 with Unicode enabled and JIT, threads and filesystem disabled.
|
||||||
|
|
||||||
- signed tag object `cd007b4466798f66d479d1442a407099e7c40050`;
|
The source and compiler checkouts must already exist locally and be completely
|
||||||
- peeled commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429`;
|
clean. No command clones, downloads or updates them:
|
||||||
- licence `BSD-3-Clause WITH PCRE2-exception`;
|
|
||||||
- Emscripten 6.0.4;
|
|
||||||
- 8-bit library, Unicode enabled, JIT disabled.
|
|
||||||
|
|
||||||
Node and Chromium tests demonstrated version/config reporting, Unicode,
|
```sh
|
||||||
named groups, global substitution, automatic and explicit callouts, and
|
npm run engines:pcre2:build -- \
|
||||||
match/depth/heap errors. This was a technical spike only. No spike source,
|
--source-dir /absolute/path/to/pcre2 \
|
||||||
binary or PCRE2 source is included in v0.1.0.
|
--emcc /absolute/path/to/emscripten/emcc
|
||||||
|
npm run engines:pcre2:verify
|
||||||
|
npm run engines:pcre2:install
|
||||||
|
```
|
||||||
|
|
||||||
Production PCRE2 support remains gated on a reviewed bridge, copied DTOs,
|
Use `--force` only to replace the exact ignored `.engine-build/pcre2` output.
|
||||||
allocation cleanup, exact source build, UTF-8/UTF-16 mapping, trace/output caps,
|
The explicit install command first re-verifies that pack and then atomically
|
||||||
worker recovery, deterministic assets, licences and browser conformance.
|
replaces only `public/engines/pcre2`.
|
||||||
|
|
||||||
|
The builder verifies Git objects and critical source/compiler hashes,
|
||||||
|
sanitizes build flags, sets `SOURCE_DATE_EPOCH=1760997684`, builds serially and
|
||||||
|
emits exactly:
|
||||||
|
|
||||||
|
- `pcre2.mjs` and `pcre2.wasm`;
|
||||||
|
- deterministic `engine-metadata.json`;
|
||||||
|
- the exact upstream `LICENSE.txt`;
|
||||||
|
- `SHA256SUMS`.
|
||||||
|
|
||||||
|
The verifier checks the closed file set, metadata, source bridge hashes and all
|
||||||
|
checksums. It validates and instantiates WebAssembly, checks ABI/engine/config
|
||||||
|
identity, then executes native Unicode compile, bounded match and bounded
|
||||||
|
substitution smoke cases plus normal and cap-stopped automatic-callout traces.
|
||||||
|
|
||||||
|
The immutable signed-tag object is pinned. OpenPGP trust validation remains a
|
||||||
|
release-operator step because the offline builder does not install or trust a
|
||||||
|
key.
|
||||||
|
|
||||||
|
## Installing the Python pack
|
||||||
|
|
||||||
|
The Python pack is derived from the exact `pyodide@314.0.3` npm package locked
|
||||||
|
with integrity:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==
|
||||||
|
```
|
||||||
|
|
||||||
|
That Pyodide release is tag/commit
|
||||||
|
`ac57031be7564f864d061cb37c5c152e59f83ad4`. Direct executable identity
|
||||||
|
verification reports CPython 3.14.2; the shipped `pyodide-lock.json`
|
||||||
|
independently reports `info.python` as 3.14.0, and the verifier records rather
|
||||||
|
than conflates those two upstream identities. The pack copies only the reviewed
|
||||||
|
loader, WebAssembly runtime, Python standard library and lock metadata required
|
||||||
|
for a local CPython `re` worker. It adds deterministic engine metadata, a
|
||||||
|
closed-file-set checksum manifest and exact Pyodide/CPython licence material.
|
||||||
|
|
||||||
|
The install operation reads from the already installed pinned npm package. It
|
||||||
|
does not contact a CDN or package index. It verifies source package identity,
|
||||||
|
file hashes, runtime metadata and licence inputs before atomically replacing
|
||||||
|
only `public/engines/python/`. The distribution verifier checks the same closed
|
||||||
|
set under `dist/`, validates WebAssembly magic and confirms that all runtime
|
||||||
|
references remain relative and self-hosted.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/python-engine-pack.mjs \
|
||||||
|
node_modules/pyodide \
|
||||||
|
.engine-build/python
|
||||||
|
node scripts/install-python-engine.mjs .engine-build/python
|
||||||
|
```
|
||||||
|
|
||||||
|
The installed closed set is:
|
||||||
|
|
||||||
|
- `pyodide.mjs`, `pyodide.asm.mjs` and `pyodide.asm.wasm`;
|
||||||
|
- `python_stdlib.zip` and `pyodide-lock.json`;
|
||||||
|
- `engine-metadata.json` and `SHA256SUMS`; and
|
||||||
|
- `LICENSE.pyodide.txt` and `LICENSE.cpython.txt`.
|
||||||
|
|
||||||
|
## Building the Java pack
|
||||||
|
|
||||||
|
The Java bridge under `engines/java/` wraps TeaVM 0.15.0
|
||||||
|
`java.util.regex.Pattern` and `Matcher`. TeaVM tag/commit
|
||||||
|
`ee91b03e616c4b45401cd11fb0cd7eb0daf6649b` is pinned through the Maven build.
|
||||||
|
The build produces a minified ES2015 module without source maps, source files,
|
||||||
|
remote imports or an OpenJDK runtime.
|
||||||
|
|
||||||
|
The pack gate verifies the Maven coordinates, bridge source identity, TeaVM
|
||||||
|
engine identity and self-test, generated module, deterministic metadata,
|
||||||
|
checksums, Apache-2.0 licence and TeaVM NOTICE. Only the verified pack is
|
||||||
|
installed under `public/engines/java/`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/java-engine-pack.mjs
|
||||||
|
node scripts/install-java-engine.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
The pack command performs two clean offline Maven builds and requires identical
|
||||||
|
output. The installed closed set is `java-regex.mjs`,
|
||||||
|
`engine-metadata.json`, `SHA256SUMS`, `LICENSE.txt` and `NOTICE.txt`.
|
||||||
|
|
||||||
|
TeaVM supplies its own Apache Harmony-derived class-library implementation.
|
||||||
|
The resulting engine is therefore identified as
|
||||||
|
`TeaVM java.util.regex 0.15.0`, not OpenJDK. TeaVM 0.15.0 does not implement
|
||||||
|
OpenJDK's `Pattern.UNICODE_CHARACTER_CLASS`; the bridge treats the application
|
||||||
|
`U` flag as a compatibility request, implies Unicode case folding and reports
|
||||||
|
that predefined-character classes and word boundaries retain TeaVM behaviour.
|
||||||
|
|
||||||
|
## Runtime boundaries
|
||||||
|
|
||||||
|
### PCRE2
|
||||||
|
|
||||||
|
ABI version 3 has no retained native handles. Each call compiles, obtains
|
||||||
|
capture names, matches/substitutes or traces and releases its code, match data
|
||||||
|
and match context before returning. The caller supplies fixed-capacity
|
||||||
|
result/name/output/event/mark buffers. Hard maxima are 1 MiB pattern, 16 MiB
|
||||||
|
subject, 1,000 captures, 10,000 matches, 100,000 capture rows, 50,000 trace
|
||||||
|
events, 10 MiB serialized trace data and 128 MiB PCRE2 heap limit; the
|
||||||
|
workbench uses stricter pattern and replacement input limits where applicable.
|
||||||
|
|
||||||
|
PCRE2 always runs with UTF and UCP. Native UTF-8 byte offsets are retained in
|
||||||
|
DTOs and converted only at valid code-point boundaries to half-open editor
|
||||||
|
UTF-16 ranges. Lone UTF-16 surrogates are rejected before encoding.
|
||||||
|
|
||||||
|
Normal execution lives only in `pcre2.worker`. Instrumented
|
||||||
|
`PCRE2_AUTO_CALLOUT` collection is a separate ABI operation loaded only by
|
||||||
|
`pcre2-trace.worker`; normal match, replacement, tests, corpus and benchmarks
|
||||||
|
never call it. Each supervisor terminates its worker on timeout, crash,
|
||||||
|
cancellation or supersession and lazily creates a clean generation for the
|
||||||
|
next request.
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
The Python worker loads the self-hosted Pyodide pack and verifies Pyodide,
|
||||||
|
CPython and `re` identities before accepting work. The application bridge is
|
||||||
|
evaluated inside that interpreter and exposes bounded compile, match and native
|
||||||
|
replacement operations. It reports CPython's Unicode code-point positions;
|
||||||
|
the TypeScript adapter retains them and converts only valid boundaries to
|
||||||
|
half-open editor UTF-16 ranges. Runtime load has a separate startup deadline.
|
||||||
|
Execution timeout, cancellation or crash terminates the worker and its complete
|
||||||
|
Python/WebAssembly heap.
|
||||||
|
|
||||||
|
### Java
|
||||||
|
|
||||||
|
The Java worker imports the self-hosted TeaVM ES module and checks the bridge
|
||||||
|
ABI, declared engine identity and compiled self-test before accepting work.
|
||||||
|
Every request compiles a `Pattern`, obtains bounded `Matcher` results or native
|
||||||
|
replacement output, and returns native half-open UTF-16 ranges. No Java virtual
|
||||||
|
machine, OpenJDK classes, remote service or ECMAScript regex fallback is
|
||||||
|
involved. Runtime load has a separate startup deadline; termination discards
|
||||||
|
the complete worker module state.
|
||||||
|
|||||||
@@ -1,13 +1,43 @@
|
|||||||
# Execution traces
|
# Execution traces
|
||||||
|
|
||||||
Version 0.1.0 exposes no execution trace.
|
Browser ECMAScript APIs expose compilation, match, capture and replacement
|
||||||
|
results, but no V8, SpiderMonkey or JavaScriptCore execution trace. Regex Tools
|
||||||
|
does not relabel structural explanations, static findings or timing
|
||||||
|
observations as an ECMAScript engine trace.
|
||||||
|
|
||||||
Browser ECMAScript APIs return compilation, match, capture and replacement
|
The bundled CPython `re` and TeaVM `java.util.regex` bridges likewise expose
|
||||||
results but not the runtime's internal backtracking events. Regex Tools does
|
compilation, bounded match/capture and replacement results, not internal engine
|
||||||
not label structural explanations, static hints or adjacent-result inference
|
steps. Regex Tools does not label their partial lexical syntax trees as traces.
|
||||||
as an actual engine trace.
|
|
||||||
|
|
||||||
The next PCRE2 slice will use actual automatic/explicit callout events from an
|
PCRE2 10.47 has a separate actual automatic-callout vertical. ABI version 3
|
||||||
application-owned bridge. Event count, serialized bytes, match/depth/heap
|
compiles the requested pattern with `PCRE2_AUTO_CALLOUT`, registers an
|
||||||
limits and wall-clock termination must all be enforced. Classifications such
|
application callback, and copies only:
|
||||||
as “apparent backtrack” will remain visibly derived from adjacent events.
|
|
||||||
|
- callout number;
|
||||||
|
- pattern byte position and next-item byte length;
|
||||||
|
- subject byte position;
|
||||||
|
- capture-top and capture-last numbers;
|
||||||
|
- bounded current mark text.
|
||||||
|
|
||||||
|
These fields have `reported` provenance. Pattern and subject byte boundaries
|
||||||
|
are validated and normalized to exact editor UTF-16 positions while the native
|
||||||
|
values remain visible.
|
||||||
|
|
||||||
|
Collection runs in `pcre2-trace.worker`, not the normal execution worker. It
|
||||||
|
uses the selected PCRE2 match, depth and heap limits plus the supervisor wall
|
||||||
|
clock. The callback retains only complete 32-byte records and mark slices under
|
||||||
|
both a 50,000-event and 10 MiB serialized-data hard cap. At a cap it returns
|
||||||
|
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
|
# Explanation model
|
||||||
|
|
||||||
The community provider converts regexpp nodes into a stable application AST
|
The ECMAScript community provider converts regexpp nodes into a stable
|
||||||
inside the syntax worker. Nodes carry deterministic IDs, UTF-16 ranges, raw
|
application AST inside the syntax worker. Nodes carry deterministic IDs,
|
||||||
source, capture metadata, quantifier bounds, assertion kind, support status,
|
UTF-16 ranges, raw source, capture metadata, quantifier bounds, assertion kind,
|
||||||
provider identity and provenance.
|
support status, provider identity and provenance.
|
||||||
|
|
||||||
Explanations are original deterministic text selected by node type. Nullable
|
Explanations are original deterministic text selected by node type. Nullable
|
||||||
and minimum/maximum consumed-length properties are derived recursively where
|
and minimum/maximum consumed-length properties are derived recursively where
|
||||||
@@ -15,6 +15,21 @@ regexpp 4.12.2 does not expose tolerant recovery. A malformed pattern therefore
|
|||||||
gets the provider's exact diagnostic plus an application-owned error node. No
|
gets the provider's exact diagnostic plus an application-owned error node. No
|
||||||
partial regexpp AST is claimed.
|
partial regexpp AST is claimed.
|
||||||
|
|
||||||
|
The PCRE2 provider currently emits a deliberately partial lexical tree for
|
||||||
|
common capture forms. It does not infer branch-reset numbering or claim full
|
||||||
|
nesting; authoritative compilation, capture counts, names and result ranges
|
||||||
|
come from PCRE2 itself.
|
||||||
|
|
||||||
|
The Python and Java providers likewise emit deliberately partial lexical trees.
|
||||||
|
They recognize their documented capture and replacement forms for navigation
|
||||||
|
and display, but do not claim full grammar acceptance. CPython `re` and TeaVM
|
||||||
|
`Pattern` remain authoritative for compilation. Java capture names displayed
|
||||||
|
with native numbered spans come from the bounded lexical metadata; they are not
|
||||||
|
invented as a TeaVM-reported name table.
|
||||||
|
|
||||||
The explanation tree is structural. It is never called an actual execution
|
The explanation tree is structural. It is never called an actual execution
|
||||||
trace. Selecting a node selects and scrolls its exact editor range; selecting
|
trace. Selecting a node selects and scrolls its exact editor range; selecting
|
||||||
pattern text chooses the smallest enclosing node.
|
pattern text chooses the smallest enclosing node. 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
|
preview as complete. In particular, list export is disabled if its template
|
||||||
would consume an incomplete value.
|
would consume an incomplete value.
|
||||||
|
|
||||||
Repeated ECMAScript captures expose only the final retained capture. The UI
|
Repeated ECMAScript, PCRE2, Python and Java captures expose only the final
|
||||||
labels this limitation and does not invent history. Later .NET support may add
|
retained capture. The UI labels this limitation and does not invent history.
|
||||||
actual engine capture-history nodes without degrading them to this model.
|
Later .NET support may add actual engine capture-history nodes without
|
||||||
|
degrading them to this model.
|
||||||
|
|
||||||
The syntactic hierarchy remains useful even if actual spans overlap or fall
|
The syntactic hierarchy remains useful even if actual spans overlap or fall
|
||||||
outside a parent's geometry; native and normalized ranges are preserved rather
|
outside a parent's geometry; native and normalized ranges are preserved rather
|
||||||
than “fixed”.
|
than “fixed”. ECMAScript and Java native offsets are UTF-16 code units, PCRE2
|
||||||
|
native offsets are UTF-8 bytes and Python native offsets are Unicode code
|
||||||
|
points. All editor ranges are normalized to UTF-16 without discarding the
|
||||||
|
native unit.
|
||||||
|
|||||||
@@ -1,20 +1,75 @@
|
|||||||
# Flavour support
|
# Flavour support
|
||||||
|
|
||||||
| Flavour | Syntax | Execution | Replacement | Captures | Trace | Native offsets |
|
| Flavour | Syntax | Execution | Replacement | Captures | Trace | Analysis | Generated cases | Native offsets |
|
||||||
| ------------ | ------------------------------------------------------------------ | ----------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- | ----------- | ------------------- |
|
| ---------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------- | -------------- |
|
||||||
| ECMAScript | regexpp 4.12.2 grammar; explanations partial/feature-matrix tested | Current browser `RegExp`; feature-matrix tested | Native matches; bounded ECMAScript `GetSubstitution` | Named/numbered; final repeated capture; no history | Unavailable | UTF-16 |
|
| ECMAScript | regexpp 4.12.2; explanations partial/feature-matrix tested | Current browser `RegExp`; conformance tested | Bounded ECMAScript `GetSubstitution` | Named/numbered; final repeated capture; no history | Unavailable | Experimental static heuristics + bounded native probes | Bounded normalized-AST sampling; actual-engine verified | UTF-16 |
|
||||||
| PCRE2 | Unavailable in release | Technical spike only; not shipped | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes |
|
| PCRE2 10.47 | Application lexical provider partial; compiler authoritative | Pinned 8-bit WASM; bounded/killable/conformance tested | Native bounded `pcre2_substitute()` loop | Native named/numbered records; no capture history | Bounded reported automatic callouts; derived movement labels | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | UTF-8 bytes |
|
||||||
| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable |
|
| Python `re` | Application lexical provider partial; compiler authoritative | CPython 3.14.2 in self-hosted Pyodide 314.0.3 | Native bounded `re` expansion | Native named/numbered spans; no capture history | Unavailable | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | Code points |
|
||||||
| Python `re` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned code points |
|
| Java `java.util.regex` | Application lexical provider partial; compiler authoritative | TeaVM 0.15.0 class library; bounded/killable; not OpenJDK | TeaVM single-digit `$n`; no `${name}` | Native numbered ranges; lexical names; no history | Unavailable | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | UTF-16 |
|
||||||
| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes |
|
| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable |
|
||||||
| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes |
|
| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned bytes |
|
||||||
| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Planned UTF-16 |
|
| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned bytes |
|
||||||
| Java | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-16 |
|
| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Unavailable | Unavailable | Planned UTF-16 |
|
||||||
|
|
||||||
ECMAScript tests cover `g`, `y`, `u`, internal `d`, zero-length iteration,
|
ECMAScript tests cover its complete exposed flag matrix, internal indices,
|
||||||
named groups, unmatched/empty/repeated groups, lookbehind, backreferences,
|
zero-length iteration, captures, lookbehind, backreferences, Unicode properties
|
||||||
Unicode properties, astral input, replacement and result truncation.
|
and bounded replacement.
|
||||||
|
|
||||||
Actual browser identity is displayed because ECMAScript behavior can evolve
|
PCRE2 tests cover `g`, `i`, `m`, `s`, `x`, `U`, `J`, mandatory UTF/UCP,
|
||||||
with the runtime. Syntax acceptance and engine compilation are separate;
|
branch-reset groups, recursion, `\K`, duplicate names, native substitution,
|
||||||
neither silently rewrites the pattern.
|
astral byte/UTF-16 ranges, zero-length iteration, compile errors, match limits
|
||||||
|
and result/output truncation. Trace tests cover reported automatic callouts,
|
||||||
|
bounded mark copying, native stop at the complete-event cap, compile ranges,
|
||||||
|
worker loading and browser rendering.
|
||||||
|
|
||||||
|
Python tests cover application-level `g`, CPython flags `a`, `i`, `m`, `s` and
|
||||||
|
`x`, Python-only named-group/replacement syntax, native `re` substitution,
|
||||||
|
astral code-point/UTF-16 normalization, zero-length iteration, compile errors,
|
||||||
|
result limits, runtime identity and worker recovery.
|
||||||
|
|
||||||
|
Java tests cover application-level `g`, TeaVM `Pattern` flags, Java named-group
|
||||||
|
syntax, TeaVM's native single-digit `$n` `Matcher` replacement, explicit
|
||||||
|
rejection of unsupported `${name}`, astral UTF-16 ranges, zero-length
|
||||||
|
iteration, compile errors, result limits, module identity and worker recovery.
|
||||||
|
The runtime and UI explicitly report that TeaVM 0.15.0 is not OpenJDK and does
|
||||||
|
not implement OpenJDK `Pattern.UNICODE_CHARACTER_CLASS`; `U` is a compatibility
|
||||||
|
request rather than an exact implementation of that flag.
|
||||||
|
|
||||||
|
ECMAScript static findings and generated-input timing observations are
|
||||||
|
advisory. They describe only the selected ECMAScript 2025 pattern, browser
|
||||||
|
runtime and bounded inputs; they are not proofs of safety or general
|
||||||
|
complexity.
|
||||||
|
|
||||||
|
ECMAScript generated-case version 1 uses only a complete accepted normalized
|
||||||
|
AST, an explicit deterministic seed and bounded sampling strategies. Every
|
||||||
|
retained positive or negative label is confirmed by the actual selected
|
||||||
|
browser engine. PCRE2, Python and Java generation are unavailable because
|
||||||
|
their current lexical providers are intentionally partial; ECMAScript behavior
|
||||||
|
is never relabelled as another flavour.
|
||||||
|
|
||||||
|
The PCRE2, Python and Java explanation providers deliberately do not invent
|
||||||
|
full structural coverage. PCRE2 branch-reset numbering is omitted from the
|
||||||
|
syntax tree, while actual capture numbers, names and ranges still come from the
|
||||||
|
selected runtime result.
|
||||||
|
|
||||||
|
ECMAScript and PCRE2 support exact-request comparison for syntax acceptance,
|
||||||
|
engine compilation, matches, captures and engine-native replacement. Results
|
||||||
|
align by normalized UTF-16 ranges while retaining each native offset model.
|
||||||
|
Timeout, cancellation, rejection and truncation are never presented as
|
||||||
|
equivalent results.
|
||||||
|
|
||||||
|
Pattern formatting is available only for an exact accepted ECMAScript regexpp
|
||||||
|
snapshot. It canonicalizes literal slashes and raw control/line-separator
|
||||||
|
representations, then requires paired actual-engine and applicable exact-test
|
||||||
|
validation before apply. PCRE2, Python and Java formatting is unavailable
|
||||||
|
because those providers are deliberately partial; their syntax is never
|
||||||
|
treated as ECMAScript.
|
||||||
|
|
||||||
|
Subject minimization accepts only the verified ECMAScript and PCRE2 paths.
|
||||||
|
Python and Java selections are disabled rather than being minimized through a
|
||||||
|
different flavour. Semantic comparison is likewise an explicit
|
||||||
|
ECMAScript/PCRE2 operation; it is not a generic four-way equivalence checker.
|
||||||
|
|
||||||
|
PCRE2 is the only current code-generation target. The reviewed C17 generator is
|
||||||
|
pinned to the PCRE2 10.47 8-bit API; other language generators remain
|
||||||
|
unavailable until they pass equivalent escaping and named-toolchain gates.
|
||||||
|
|||||||
58
docs/FORMATTING.md
Normal file
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
|
It uses only reviewed open-source dependencies and builds without private
|
||||||
registry credentials. Version 0.1.0 parses ECMAScript with regexpp 4.12.2.
|
registry credentials. ECMAScript uses regexpp 4.12.2. PCRE2 uses an
|
||||||
|
application-owned lexical provider for partial explanations and the actual
|
||||||
|
bundled PCRE2 compiler for authoritative acceptance. Python and Java use
|
||||||
|
application-owned lexical providers for partial explanations and
|
||||||
|
flavour-native replacement tokens; bundled CPython `re` and TeaVM
|
||||||
|
`java.util.regex` respectively remain authoritative for compilation.
|
||||||
|
The Java provider follows the bundled TeaVM 0.15.0 replacement subset:
|
||||||
|
single-digit `$n` is executable, `$12` is `$1` followed by literal `2`, and
|
||||||
|
Java SE `${name}` references are surfaced as a compatibility limitation.
|
||||||
|
|
||||||
The product deliberately does not implement or reference an `@r101/parser`
|
The product deliberately does not implement or reference an `@r101/parser`
|
||||||
profile. No commercial package alias, import, tarball, credential, licence key
|
profile. No commercial package alias, import, tarball, credential, licence key
|
||||||
or private CI path exists. Additional flavour syntax will be implemented
|
or private CI path exists. Additional flavour syntax is implemented
|
||||||
incrementally as open-source providers behind the same application-owned
|
incrementally as open-source providers behind the same application-owned
|
||||||
interface.
|
interface. Partial coverage is exposed as metadata and never presented as a
|
||||||
|
complete parser.
|
||||||
|
|||||||
66
docs/PCRE2_NEXT_SLICES.md
Normal file
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
|
elapsed time and exact runtime identity but makes no universal throughput or
|
||||||
safety claim.
|
safety claim.
|
||||||
|
|
||||||
|
PCRE2 cold worker startup includes loading and instantiating the self-hosted
|
||||||
|
WebAssembly module; warm requests reuse that instance.
|
||||||
|
Every request still compiles and frees its pattern so no unbounded compiled-code
|
||||||
|
cache can accumulate. Native match-step, depth and heap limits complement the
|
||||||
|
supervisor wall clock, and cancellation discards the complete worker heap.
|
||||||
|
|
||||||
|
Python has a materially larger cold start because its worker loads the
|
||||||
|
self-hosted Pyodide JavaScript/WebAssembly runtime and Python standard library
|
||||||
|
before verifying CPython and `re`. Java imports and self-tests its bundled
|
||||||
|
TeaVM ES module. Engine loading is an explicit operation with a
|
||||||
|
flavour-specific startup deadline; it is not charged to the 250 ms live or
|
||||||
|
selected manual execution timeout. Warm requests reuse the verified worker.
|
||||||
|
Killing a worker for timeout, crash or cancellation discards its complete
|
||||||
|
runtime, so the next request pays the cold-load cost and verifies identity
|
||||||
|
again.
|
||||||
|
|
||||||
|
Automatic-callout tracing is deliberately excluded from every benchmark. It
|
||||||
|
compiles an independently instrumented PCRE2 pattern in a separate worker and
|
||||||
|
can materially change runtime work. The trace path retains at most 50,000
|
||||||
|
complete 32-byte event records and 10 MiB total serialized event/mark bytes;
|
||||||
|
the viewer renders at most 2,000 retained events.
|
||||||
|
|
||||||
The production bundle separates syntax and execution workers. Interactive
|
The production bundle separates syntax and execution workers. Interactive
|
||||||
syntax/extraction trees and editor decoration sets render at most 2,000 items
|
syntax/extraction trees and editor decoration sets render at most 2,000 items
|
||||||
each and report both the rendered and actual totals. These are presentation
|
each and report both the rendered and actual totals. These are presentation
|
||||||
@@ -32,12 +54,12 @@ pages of 200. Individual result-value previews are capped at 64 Ki UTF-16 units
|
|||||||
and share an 8 Mi-unit aggregate engine-result budget. The extraction tree
|
and share an 8 Mi-unit aggregate engine-result budget. The extraction tree
|
||||||
shows 100 UTF-16 units per value and capture-table cells show 512, with explicit
|
shows 100 UTF-16 units per value and capture-table cells show 512, with explicit
|
||||||
range lengths and separate engine-prefix labels; copy and eligible JSON export
|
range lengths and separate engine-prefix labels; copy and eligible JSON export
|
||||||
retain the bounded engine value. List export is disabled when a source value is incomplete, so a bounded
|
retain the bounded engine value. List export is disabled when a source value is
|
||||||
preview cannot be mistaken for the full value. List generation additionally
|
incomplete, so a bounded preview cannot be mistaken for the full value. List
|
||||||
stops at 250,000 template-token evaluations, a 64 Ki UTF-16 row preview, or an
|
generation additionally stops at 250,000 template-token evaluations, a 64 Ki
|
||||||
8 Mi-unit aggregate materialization bound. The UI shows 500 list rows while an
|
UTF-16 row preview, or an 8 Mi-unit aggregate materialization bound. The UI
|
||||||
eligible export still contains every generated row; any incomplete result
|
shows 500 list rows while an eligible export still contains every generated
|
||||||
disables export.
|
row; any incomplete result disables export.
|
||||||
Replacement output has a separate 64 MiB bound. Match/capture-limit
|
Replacement output has a separate 64 MiB bound. Match/capture-limit
|
||||||
incompleteness and output-byte truncation are tracked separately; either makes
|
incompleteness and output-byte truncation are tracked separately; either makes
|
||||||
the aggregate replacement result incomplete.
|
the aggregate replacement result incomplete.
|
||||||
@@ -62,5 +84,57 @@ representation against a byte budget before materializing or writing it.
|
|||||||
IndexedDB schema version 2 indexes `updatedAt`; latest-project loading opens one
|
IndexedDB schema version 2 indexes `updatedAt`; latest-project loading opens one
|
||||||
descending cursor and validates only that record instead of calling `getAll()`.
|
descending cursor and validates only that record instead of calling `getAll()`.
|
||||||
|
|
||||||
Formal cold/warm benchmarks, p95 statistics and growth charts are deferred to
|
Corpus runs accept at most 256 documents, 16 MiB per document and 256 MiB of
|
||||||
the analysis milestone and will run in killable workers.
|
aggregate UTF-8 input. Jobs run sequentially so only one engine request is
|
||||||
|
active at a time. A batch retains at most 100,000 matches and 64 MiB of applied
|
||||||
|
output, and stops after five minutes of aggregate wall time. Each document also
|
||||||
|
uses the selected manual timeout. Progress is reported after every document;
|
||||||
|
cancellation kills the current worker and labels all remaining documents.
|
||||||
|
Result tables contain summaries, not match/capture objects. ZIP export is
|
||||||
|
available only for an all-exact apply batch and is created asynchronously.
|
||||||
|
|
||||||
|
Independent-line mode preflights at most 100,000 logical lines and processes
|
||||||
|
each as an isolated engine subject while preserving line separators for exact
|
||||||
|
apply output. Per document, corpus extraction retains at most 32 group summaries
|
||||||
|
(including the full match), three 160-UTF-16-unit samples per group and 100
|
||||||
|
unique diagnostics. Output previews show 2,000 units. Content-free summary
|
||||||
|
exports are capped at 4 MiB; the applied archive is capped at 68 MiB in addition
|
||||||
|
to the 64 MiB uncompressed-output budget.
|
||||||
|
|
||||||
|
ECMAScript analysis reports cold compilation separately from warm native
|
||||||
|
execution. The default benchmark uses three warm-up and 15 measured samples;
|
||||||
|
user caps are 100 warm-ups and 1,000 samples. It reports median and
|
||||||
|
nearest-rank p95 values plus separate timing and outcome/growth plots with the
|
||||||
|
exact runtime identity. Each sample defaults to a 2-second killable-worker
|
||||||
|
limit (10-second cap), and the aggregate analysis wall time is 30 seconds.
|
||||||
|
|
||||||
|
Growth probes retain at most 24 steps, generate at most 1 MiB by default under
|
||||||
|
the central 16 MiB subject cap, and reject more than 10,000,000 repetitions
|
||||||
|
before string allocation. Replacement timing is skipped when match collection
|
||||||
|
is truncated or the estimated output exceeds 8 Mi UTF-16 units. Results are
|
||||||
|
observations for the chosen runtime and generated inputs, never a general
|
||||||
|
complexity proof. See [`ANALYSIS.md`](ANALYSIS.md).
|
||||||
|
|
||||||
|
Generated-case synthesis defaults to 48 retained cases from at most 192
|
||||||
|
candidates, 16 seeded-random variants, 64 KiB per candidate, 1 MiB aggregate
|
||||||
|
candidate bytes and 15 seconds of aggregate wall time. Every candidate then
|
||||||
|
uses a separate selected-engine request with a 500 ms default timeout.
|
||||||
|
Generation preflights UTF-8 size and repetition bounds, caps normalized-AST
|
||||||
|
traversal at 20,000 nodes / 512 levels and retains only bounded diagnostics.
|
||||||
|
Hard caps are documented in [`GENERATED_CASES.md`](GENERATED_CASES.md).
|
||||||
|
|
||||||
|
Subject minimization accepts at most 1 MiB UTF-8, performs at most 2,000 real
|
||||||
|
engine candidate evaluations and stops after at most 60 seconds including
|
||||||
|
syntax setup. One fixed worker timeout of at most 10 seconds applies to every
|
||||||
|
candidate. A candidate starts only when that complete deadline fits in the
|
||||||
|
remaining aggregate budget. Accepted-transform history retains 200 entries;
|
||||||
|
evaluation and accepted totals remain exact. See
|
||||||
|
[`MINIMIZATION.md`](MINIMIZATION.md).
|
||||||
|
|
||||||
|
Formatter validation uses two syntax supervisors and two actual-engine
|
||||||
|
supervisors so source/candidate outcomes cannot share compiled state. The
|
||||||
|
current subject/replacement snapshot always runs; at most 1,000 enabled tests
|
||||||
|
with the exact active identity are replayed. Test validation has a 60-second
|
||||||
|
aggregate wall budget, and a test starts only when its complete configured
|
||||||
|
worker timeout still fits. Preview details retain at most 10,000 transforms and
|
||||||
|
the UI renders the first 200. See [`FORMATTING.md`](FORMATTING.md).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Portal requirements
|
# Portal requirements
|
||||||
|
|
||||||
Regex Tools 0.1.0 is a static nested-path-safe application.
|
Regex Tools is a static nested-path-safe application.
|
||||||
|
|
||||||
Required delivery behavior:
|
Required delivery behavior:
|
||||||
|
|
||||||
@@ -9,11 +9,23 @@ Required delivery behavior:
|
|||||||
revalidation/no-cache resources.
|
revalidation/no-cache resources.
|
||||||
- Hashed Vite assets may use immutable long-term caching.
|
- Hashed Vite assets may use immutable long-term caching.
|
||||||
- CSP must allow `worker-src 'self' blob:`.
|
- CSP must allow `worker-src 'self' blob:`.
|
||||||
- No WebAssembly MIME rule or `wasm-unsafe-eval` is required by v0.1.0.
|
- `engines/pcre2/pcre2.wasm` and
|
||||||
|
`engines/python/pyodide.asm.wasm` must use `application/wasm`.
|
||||||
|
- Engine `.mjs` files must use a JavaScript MIME type;
|
||||||
|
`engines/python/python_stdlib.zip`, JSON, licence, notice and checksum files
|
||||||
|
must be delivered without content transformation.
|
||||||
|
- `script-src` must permit self-hosted WebAssembly compilation, normally with
|
||||||
|
`'wasm-unsafe-eval'`; broad `'unsafe-eval'` is neither needed nor recommended.
|
||||||
|
- `connect-src 'self'` must allow the Pyodide worker to fetch its own
|
||||||
|
same-origin WebAssembly, standard-library and lock files.
|
||||||
|
- Every file below `engines/pcre2/`, `engines/python/` and `engines/java/`
|
||||||
|
should use revalidation or a release-scoped immutable cache. These are not
|
||||||
|
content-hashed filenames and must never drift under one release identity.
|
||||||
|
|
||||||
The Portal must consume the immutable release ZIP, verify its SHA-256, verify
|
The Portal must consume the immutable release ZIP, verify its SHA-256, verify
|
||||||
manifest ID `de.add-ideas.regex-tools` and version `0.1.0`, and mount it at a
|
manifest ID `de.add-ideas.regex-tools` and version `0.3.0`, and mount it at a
|
||||||
relative target such as `apps/regex/`.
|
relative target such as `apps/regex/`.
|
||||||
|
|
||||||
When PCRE2 ships later, the smallest additional change will be
|
The package, manifest, tag, artifact URL and checksum must all use the v0.3.0
|
||||||
`application/wasm` delivery and `script-src 'self' 'wasm-unsafe-eval'`.
|
identity. Historical artifact coordinates remain immutable and must not be
|
||||||
|
overwritten or reused.
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
# Reference implementations and sources
|
# Reference implementations and sources
|
||||||
|
|
||||||
Inspection date: 2026-07-24.
|
Inspection date: 2026-07-27.
|
||||||
|
|
||||||
| Reference | Exact revision/version | Licence | Use and copying status |
|
| Reference | Exact revision/version | Licence | Use and copying status |
|
||||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| Regex Tools | Unborn repository at initial inspection; release identity `v0.1.0` | GPL-3.0-or-later | Application source written for this project. |
|
| Regex Tools | Release identity `v0.3.0` | GPL-3.0-or-later | Application source written for this project. |
|
||||||
| Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. |
|
| Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. |
|
||||||
| Toolbox Portal | `a9c31c8986c40a0097966318e925083302e91e13` / 0.5.0 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
|
| Toolbox Portal | `a9c31c8986c40a0097966318e925083302e91e13` / 0.5.0 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
|
||||||
| regexpp | 4.12.2; npm integrity `sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==` | MIT | Pinned ECMAScript syntax provider. Provider AST is normalized in a worker. |
|
| regexpp | 4.12.2; npm integrity `sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==` | MIT | Pinned ECMAScript syntax provider. Provider AST is normalized in a worker. |
|
||||||
| PCRE2 | signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | External technical spike only; not shipped or copied. |
|
| PCRE2 | signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | Pinned offline build; generated verified WebAssembly pack is shipped, source is not vendored. |
|
||||||
|
| Pyodide | tag 314.0.3; commit `ac57031be7564f864d061cb37c5c152e59f83ad4`; pinned npm integrity | MPL-2.0 | Reviewed self-hosted runtime pack; no CDN use. |
|
||||||
|
| CPython | executable runtime identity 3.14.2 in Pyodide 314.0.3 | PSF-2.0 | Actual `re` compiler, matcher and replacement runtime; Python source is not vendored. |
|
||||||
|
| TeaVM | tag/commit `ee91b03e616c4b45401cd11fb0cd7eb0daf6649b` / 0.15.0 | Apache-2.0 | `java.util.regex` class library compiled into the verified module; not OpenJDK. |
|
||||||
| ECMAScript specification | <https://tc39.es/ecma262/>; living specification inspected 2026-07-24 | ECMA terms | Semantics reference; no prose copied. |
|
| ECMAScript specification | <https://tc39.es/ecma262/>; living specification inspected 2026-07-24 | ECMA terms | Semantics reference; no prose copied. |
|
||||||
| regex101 | <https://regex101.com/> and public parser documentation | Product/commercial terms | Product reference only. No source, branding, explanations, reference prose or visual design copied. |
|
| regex101 | <https://regex101.com/> and public parser documentation | Product/commercial terms | Product reference only. No source, branding, explanations, reference prose or visual design copied. |
|
||||||
| `recheck` | 4.5.0 | MIT | Metadata inspected; not installed or shipped. |
|
| `recheck` | 4.5.0 | MIT | Metadata inspected; not installed or shipped. |
|
||||||
@@ -17,5 +20,5 @@ Inspection date: 2026-07-24.
|
|||||||
| RegexLib | Website inspected | Redistribution licence not established | No fixture copied. |
|
| RegexLib | Website inspected | Redistribution licence not established | No fixture copied. |
|
||||||
| RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. |
|
| RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. |
|
||||||
|
|
||||||
All v0.1.0 conformance cases are project-authored. See
|
All v0.1.0, v0.2.0 and v0.3.0 conformance cases are project-authored. See
|
||||||
`tests/fixtures/README.md`.
|
`tests/fixtures/README.md`.
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ on publication failure, replaces an existing exact version only with
|
|||||||
`--force`, and produces:
|
`--force`, and produces:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
release/regex-tools-0.1.0.zip
|
release/regex-tools-0.3.0.zip
|
||||||
release/regex-tools-0.1.0.zip.sha256
|
release/regex-tools-0.3.0.zip.sha256
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Before packaging, the engine verifier checks the closed PCRE2, Python and Java
|
||||||
|
file sets in `dist/`, including checksums, metadata, licences/notices, module
|
||||||
|
and WebAssembly identities and self-hosted relative asset references.
|
||||||
|
|
||||||
|
The existing v0.1.0 and v0.2.0 ZIPs and matching sidecars are historical
|
||||||
|
immutable artifacts. Creating v0.3.0 must not replace, rename or remove them.
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ untrusted.
|
|||||||
- A terminated worker is never reused.
|
- A terminated worker is never reused.
|
||||||
- Pattern, subject, replacement/list template, match, capture and output sizes
|
- Pattern, subject, replacement/list template, match, capture and output sizes
|
||||||
are bounded.
|
are bounded.
|
||||||
|
- PCRE2 adds native match-step, depth and heap caps. Its C ABI returns only
|
||||||
|
caller-owned bounded records and releases code, match data and match context
|
||||||
|
on every exit path.
|
||||||
|
- PCRE2 automatic-callout tracing is a separate ABI and worker path. It copies
|
||||||
|
only complete fixed records and bounded mark text, stops matching at the
|
||||||
|
50,000-event or 10 MiB serialized trace cap, and is killed on timeout,
|
||||||
|
cancellation or crash.
|
||||||
|
- PCRE2 runs only in mandatory UTF/UCP mode. Exact UTF-8 byte boundaries are
|
||||||
|
normalized to editor UTF-16 offsets; lone surrogates are rejected instead of
|
||||||
|
being silently replaced by browser encoding.
|
||||||
|
- Python starts only after the worker verifies Pyodide, CPython and `re`
|
||||||
|
identities. Pattern, subject, flags and replacement remain string data passed
|
||||||
|
to the fixed application bridge; no user-supplied Python source is evaluated.
|
||||||
|
Native code-point boundaries are checked before conversion to editor UTF-16.
|
||||||
|
- Java starts only after the worker verifies the TeaVM bridge ABI, engine
|
||||||
|
identity and compiled self-test. It exposes only fixed bounded
|
||||||
|
`Pattern`/`Matcher` entry points; no JVM, Java class loading or user-supplied
|
||||||
|
JavaScript is available. The runtime is TeaVM, not OpenJDK.
|
||||||
|
- Every bundled engine directory has a closed file set, deterministic
|
||||||
|
metadata/checksums and licence material verified both before installation and
|
||||||
|
in `dist/`. Runtime imports and fetches resolve only against the same-origin
|
||||||
|
release path.
|
||||||
- Replacement templates are strings; executable callbacks are unavailable.
|
- Replacement templates are strings; executable callbacks are unavailable.
|
||||||
- Replacement/list token decorations, rows, chips, diagnostics and list
|
- Replacement/list token decorations, rows, chips, diagnostics and list
|
||||||
evaluation work have separate presentation/operation caps; incomplete list
|
evaluation work have separate presentation/operation caps; incomplete list
|
||||||
@@ -25,14 +47,55 @@ untrusted.
|
|||||||
- Unit-test assertion diagnostics never stringify complete large values and
|
- Unit-test assertion diagnostics never stringify complete large values and
|
||||||
retain at most 2 KiB UTF-8 per case. Oversized replacement output is not
|
retain at most 2 KiB UTF-8 per case. Oversized replacement output is not
|
||||||
copied into an exact current-result draft.
|
copied into an exact current-result draft.
|
||||||
- No backend, uploads, telemetry, remote corpus URL, CDN, remote font, `eval`,
|
- Corpus file input is local, recognized UTF-8 text only. It is bounded before
|
||||||
`Function`, arbitrary command line or unsafe HTML rendering exists.
|
decoding, rejects NUL/binary input, runs sequentially in a dedicated
|
||||||
|
cancellable supervisor and never modifies source files.
|
||||||
|
- Corpus JSON/CSV/NDJSON summaries never contain source, capture-sample or
|
||||||
|
applied-output content. CSV cells are formula-injection protected. Local UI
|
||||||
|
previews are explicitly bounded. Applied files require an explicit
|
||||||
|
content-export acknowledgement; names are normalized against archive path
|
||||||
|
traversal and collisions. Incomplete applied output cannot be downloaded or
|
||||||
|
included in the all-document ZIP, whose bytes have a separate hard limit.
|
||||||
|
- Corpus documents, contents, outputs and results are never placed in project
|
||||||
|
JSON or IndexedDB. Schema-v1 imports that attempt to inject corpus payloads
|
||||||
|
are rejected.
|
||||||
|
- ECMAScript growth analysis preflights repetition count and UTF-8 bytes before
|
||||||
|
allocating a generated string. The analysis worker is terminated on timeout,
|
||||||
|
cancellation or crash; replacement timing also preflights its output bound.
|
||||||
|
Analysis settings and results are neither persisted nor uploaded.
|
||||||
|
- Generated-case synthesis runs in its own killable worker under AST, depth,
|
||||||
|
attempt, repetition and UTF-8 byte caps. Labels come only from bounded
|
||||||
|
requests to the selected real engine worker; a wrong outcome, timeout, crash,
|
||||||
|
cancellation or compile rejection is never converted into a verified case.
|
||||||
|
Settings, discarded candidates and unselected results are ephemeral and
|
||||||
|
never uploaded.
|
||||||
|
- Imported generated-test provenance is accepted only for the exact supported
|
||||||
|
generator id/version and must agree with the test assertion outcome. Unknown
|
||||||
|
provenance fields and future versions are rejected; editing a case removes
|
||||||
|
its provenance.
|
||||||
|
- ECMAScript formatting consumes only exact accepted regexpp literal-token
|
||||||
|
ranges and never treats PCRE2 as ECMAScript. Source/candidate syntax and
|
||||||
|
execution use separate killable workers; compile failure, truncation,
|
||||||
|
timeout asymmetry, engine-identity drift, incomplete tests or any semantic
|
||||||
|
difference fails closed. Preview/results are ephemeral and apply requires an
|
||||||
|
explicit confirmation.
|
||||||
|
- Subject minimization rejects lone surrogates, caps input at 1 MiB UTF-8,
|
||||||
|
evaluation count at 2,000 and aggregate wall time at 60 seconds. Each
|
||||||
|
predicate check runs in the selected killable engine worker. Timeout, crash,
|
||||||
|
cancellation and worker error remain distinct; truncated results fail
|
||||||
|
closed. Subjects and results are ephemeral and never uploaded or persisted.
|
||||||
|
- No backend, uploads, telemetry, remote corpus URL, CDN, remote font,
|
||||||
|
arbitrary command line or unsafe HTML rendering exists. Application code
|
||||||
|
does not evaluate user-controlled JavaScript or Python. The generic generated
|
||||||
|
Pyodide loader contains an Emscripten dynamic-link branch that uses
|
||||||
|
JavaScript `eval`, but the shipped self-contained CPython/stdlib path does not
|
||||||
|
exercise it and loads under the CSP below without broad `'unsafe-eval'`.
|
||||||
|
|
||||||
Version 0.1.0 needs a CSP equivalent to:
|
The engine-enabled build needs a CSP equivalent to:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
script-src 'self';
|
script-src 'self' 'wasm-unsafe-eval';
|
||||||
worker-src 'self' blob:;
|
worker-src 'self' blob:;
|
||||||
connect-src 'self';
|
connect-src 'self';
|
||||||
img-src 'self' data:;
|
img-src 'self' data:;
|
||||||
@@ -44,8 +107,11 @@ base-uri 'none';
|
|||||||
form-action 'none';
|
form-action 'none';
|
||||||
```
|
```
|
||||||
|
|
||||||
The Toolbox shell currently requires inline styles. No `unsafe-eval` or
|
The Toolbox shell currently requires inline styles. Corpus ZIP generation,
|
||||||
`wasm-unsafe-eval` is required until an actual WebAssembly flavour ships.
|
PCRE2, Pyodide and TeaVM use self-hosted modules/assets and local workers under
|
||||||
|
the existing `worker-src` and `connect-src` policy. Broad `'unsafe-eval'` is not
|
||||||
|
required; `'wasm-unsafe-eval'` remains required for the two WebAssembly
|
||||||
|
runtimes.
|
||||||
|
|
||||||
Report vulnerabilities privately to the repository owner before public issue
|
Report vulnerabilities privately to the repository owner before public issue
|
||||||
details are posted.
|
details are posted.
|
||||||
|
|||||||
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",
|
"coverage",
|
||||||
"test-results",
|
"test-results",
|
||||||
"playwright-report",
|
"playwright-report",
|
||||||
|
".engine-build",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
51
package-lock.json
generated
51
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.2",
|
"@add-ideas/toolbox-contract": "0.2.2",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@codemirror/view": "6.43.6",
|
"@codemirror/view": "6.43.6",
|
||||||
"@eslint-community/regexpp": "4.12.2",
|
"@eslint-community/regexpp": "4.12.2",
|
||||||
"@lezer/highlight": "1.2.3",
|
"@lezer/highlight": "1.2.3",
|
||||||
|
"fflate": "0.8.3",
|
||||||
"react": "19.2.7",
|
"react": "19.2.7",
|
||||||
"react-dom": "19.2.7"
|
"react-dom": "19.2.7"
|
||||||
},
|
},
|
||||||
@@ -35,10 +36,10 @@
|
|||||||
"eslint": "10.7.0",
|
"eslint": "10.7.0",
|
||||||
"eslint-plugin-react-hooks": "7.1.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"eslint-plugin-react-refresh": "0.5.2",
|
"eslint-plugin-react-refresh": "0.5.2",
|
||||||
"fflate": "0.8.3",
|
|
||||||
"globals": "17.7.0",
|
"globals": "17.7.0",
|
||||||
"jsdom": "29.1.1",
|
"jsdom": "29.1.1",
|
||||||
"prettier": "3.9.5",
|
"prettier": "3.9.5",
|
||||||
|
"pyodide": "314.0.3",
|
||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.64.0",
|
"typescript-eslint": "8.64.0",
|
||||||
"vite": "8.1.5",
|
"vite": "8.1.5",
|
||||||
@@ -1385,6 +1386,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/emscripten": {
|
||||||
|
"version": "1.41.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
||||||
|
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/esrecurse": {
|
"node_modules/@types/esrecurse": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||||
@@ -2442,7 +2450,6 @@
|
|||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
@@ -3406,6 +3413,20 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pyodide": {
|
||||||
|
"version": "314.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-314.0.3.tgz",
|
||||||
|
"integrity": "sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/emscripten": "^1.41.4",
|
||||||
|
"ws": "^8.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||||
@@ -4102,6 +4123,28 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||||
|
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Albrecht Degering",
|
"author": "Albrecht Degering",
|
||||||
@@ -32,6 +32,15 @@
|
|||||||
"test:conformance": "vitest run tests/conformance",
|
"test:conformance": "vitest run tests/conformance",
|
||||||
"test:browser": "playwright test",
|
"test:browser": "playwright test",
|
||||||
"engines:build": "node scripts/build-engines.mjs",
|
"engines:build": "node scripts/build-engines.mjs",
|
||||||
|
"engines:pcre2:build": "node scripts/build-engines.mjs --pcre2",
|
||||||
|
"engines:pcre2:install": "node scripts/install-pcre2-engine.mjs",
|
||||||
|
"engines:pcre2:verify": "node scripts/verify-engine-assets.mjs --pcre2-pack .engine-build/pcre2",
|
||||||
|
"engines:python:build": "node scripts/python-engine-pack.mjs",
|
||||||
|
"engines:python:install": "node scripts/install-python-engine.mjs",
|
||||||
|
"engines:python:verify": "node scripts/verify-engine-assets.mjs --python-pack public/engines/python",
|
||||||
|
"engines:java:build": "node scripts/java-engine-pack.mjs",
|
||||||
|
"engines:java:install": "node scripts/install-java-engine.mjs",
|
||||||
|
"engines:java:verify": "node scripts/verify-engine-assets.mjs --java-pack public/engines/java",
|
||||||
"engines:verify": "node scripts/verify-engine-assets.mjs",
|
"engines:verify": "node scripts/verify-engine-assets.mjs",
|
||||||
"manifest:generate": "node scripts/generate-toolbox-manifest.mjs",
|
"manifest:generate": "node scripts/generate-toolbox-manifest.mjs",
|
||||||
"manifest:check": "node scripts/generate-toolbox-manifest.mjs --check",
|
"manifest:check": "node scripts/generate-toolbox-manifest.mjs --check",
|
||||||
@@ -51,6 +60,7 @@
|
|||||||
"@codemirror/view": "6.43.6",
|
"@codemirror/view": "6.43.6",
|
||||||
"@eslint-community/regexpp": "4.12.2",
|
"@eslint-community/regexpp": "4.12.2",
|
||||||
"@lezer/highlight": "1.2.3",
|
"@lezer/highlight": "1.2.3",
|
||||||
|
"fflate": "0.8.3",
|
||||||
"react": "19.2.7",
|
"react": "19.2.7",
|
||||||
"react-dom": "19.2.7"
|
"react-dom": "19.2.7"
|
||||||
},
|
},
|
||||||
@@ -68,10 +78,10 @@
|
|||||||
"eslint": "10.7.0",
|
"eslint": "10.7.0",
|
||||||
"eslint-plugin-react-hooks": "7.1.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"eslint-plugin-react-refresh": "0.5.2",
|
"eslint-plugin-react-refresh": "0.5.2",
|
||||||
"fflate": "0.8.3",
|
|
||||||
"globals": "17.7.0",
|
"globals": "17.7.0",
|
||||||
"jsdom": "29.1.1",
|
"jsdom": "29.1.1",
|
||||||
"prettier": "3.9.5",
|
"prettier": "3.9.5",
|
||||||
|
"pyodide": "314.0.3",
|
||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.64.0",
|
"typescript-eslint": "8.64.0",
|
||||||
"vite": "8.1.5",
|
"vite": "8.1.5",
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# Engine assets
|
# Engine assets
|
||||||
|
|
||||||
Regex Tools 0.1.0 executes ECMAScript through the browser's native `RegExp`
|
Regex Tools executes ECMAScript through the browser's native `RegExp` engine.
|
||||||
engine. It therefore ships no external engine binary or WebAssembly pack.
|
It also ships three declared, self-hosted runtime packs:
|
||||||
|
|
||||||
Future flavour packs will be built from pinned, documented open-source
|
- `pcre2/`: official PCRE2 10.47, 8-bit WebAssembly;
|
||||||
revisions and placed in versioned subdirectories here.
|
- `python/`: Pyodide 314.0.3 with CPython 3.14.2 `re`; and
|
||||||
|
- `java/`: TeaVM 0.15.0's `java.util.regex` class library compiled to an
|
||||||
|
ES2015 module. This module is not OpenJDK.
|
||||||
|
|
||||||
|
The packs come from pinned, documented open-source inputs. Runtime files travel
|
||||||
|
with deterministic metadata, closed-set checksums and the applicable upstream
|
||||||
|
licence/notice material, all of which are verified by the release gate.
|
||||||
|
|||||||
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,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.regex-tools",
|
"id": "de.add-ideas.regex-tools",
|
||||||
"name": "Regex Tools",
|
"name": "Regex Tools",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -36,5 +36,15 @@
|
|||||||
"label": "Source",
|
"label": "Source",
|
||||||
"url": "https://git.add-ideas.de/zemion/regex-tools"
|
"url": "https://git.add-ideas.de/zemion/regex-tools"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"assets": [
|
||||||
|
"./engines/pcre2/pcre2.mjs",
|
||||||
|
"./engines/pcre2/pcre2.wasm",
|
||||||
|
"./engines/python/pyodide.mjs",
|
||||||
|
"./engines/python/pyodide.asm.mjs",
|
||||||
|
"./engines/python/pyodide.asm.wasm",
|
||||||
|
"./engines/python/pyodide-lock.json",
|
||||||
|
"./engines/python/python_stdlib.zip",
|
||||||
|
"./engines/java/java-regex.mjs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,37 @@
|
|||||||
import { readFile, readdir } from "node:fs/promises";
|
import { readFile, readdir } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import {
|
||||||
|
buildPcre2Pack,
|
||||||
|
parsePcre2BuildArguments,
|
||||||
|
} from "./pcre2-engine-pack.mjs";
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const options = parsePcre2BuildArguments(process.argv.slice(2));
|
||||||
|
|
||||||
|
if (options.mode === "help") {
|
||||||
|
console.log(`Usage:
|
||||||
|
node scripts/build-engines.mjs
|
||||||
|
node scripts/build-engines.mjs --pcre2 --source-dir PATH --emcc PATH [--force]
|
||||||
|
|
||||||
|
The PCRE2 command is an explicit offline build gate. It verifies exact local
|
||||||
|
source and compiler identities, writes .engine-build/pcre2, and never
|
||||||
|
downloads or publishes an engine pack. Install the verified local pack with
|
||||||
|
npm run engines:pcre2:install.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.mode === "pcre2") {
|
||||||
|
const result = await buildPcre2Pack(options, root);
|
||||||
|
console.log(
|
||||||
|
`Built and verified staged PCRE2 ${result.metadata.engineVersion} pack at ${result.output}.`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"The verified pack remains in private staging until the explicit local install command is run.",
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
await readFile(path.join(root, "package.json"), "utf8"),
|
await readFile(path.join(root, "package.json"), "utf8"),
|
||||||
);
|
);
|
||||||
@@ -10,15 +39,18 @@ const engineDirectory = path.join(root, "public", "engines");
|
|||||||
const entries = (await readdir(engineDirectory)).sort();
|
const entries = (await readdir(engineDirectory)).sort();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
packageJson.version !== "0.1.0" ||
|
typeof packageJson.version !== "string" ||
|
||||||
entries.length !== 1 ||
|
entries.length !== 4 ||
|
||||||
entries[0] !== "README.md"
|
entries[0] !== "README.md" ||
|
||||||
|
entries[1] !== "java" ||
|
||||||
|
entries[2] !== "pcre2" ||
|
||||||
|
entries[3] !== "python"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"The ECMAScript release must contain only the documented native-browser engine placeholder.",
|
"The production build requires the documented Java, PCRE2 and Python runtime packs.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Regex Tools 0.1.0 uses the native browser RegExp engine; no external engine pack needs building.",
|
"The checked-in Java, PCRE2 and Python runtime packs are present; no release-time download or engine compilation is needed.",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,14 +7,23 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|||||||
const sourcePath = join(root, "src", "toolbox", "manifest.source.json");
|
const sourcePath = join(root, "src", "toolbox", "manifest.source.json");
|
||||||
const outputPath = join(root, "public", "toolbox-app.json");
|
const outputPath = join(root, "public", "toolbox-app.json");
|
||||||
const packagePath = join(root, "package.json");
|
const packagePath = join(root, "package.json");
|
||||||
|
const applicationVersionPath = join(root, "src", "version.ts");
|
||||||
const checkOnly = process.argv.includes("--check");
|
const checkOnly = process.argv.includes("--check");
|
||||||
|
|
||||||
const source = JSON.parse(await readFile(sourcePath, "utf8"));
|
const source = JSON.parse(await readFile(sourcePath, "utf8"));
|
||||||
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
||||||
|
const applicationVersionSource = await readFile(applicationVersionPath, "utf8");
|
||||||
|
const applicationVersion =
|
||||||
|
/^export const APPLICATION_VERSION = "([^"]+)";$/mu.exec(
|
||||||
|
applicationVersionSource,
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
if (source.version !== packageJson.version) {
|
if (
|
||||||
|
source.version !== packageJson.version ||
|
||||||
|
applicationVersion !== packageJson.version
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Manifest version ${source.version} differs from package version ${packageJson.version}`,
|
`Version drift: manifest ${source.version}, application ${String(applicationVersion)}, package ${packageJson.version}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 zipEpoch = new Date(1980, 0, 1, 0, 0, 0);
|
||||||
const gplVersion3TextSha256 =
|
const gplVersion3TextSha256 =
|
||||||
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
|
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
|
||||||
|
const expectedApplicationVersion = "0.3.0";
|
||||||
const requiredRootFiles = [
|
const requiredRootFiles = [
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
@@ -332,13 +333,27 @@ async function collectReleaseEntries() {
|
|||||||
await readFile(path.join(root, "package.json"), "utf8"),
|
await readFile(path.join(root, "package.json"), "utf8"),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
packageJson.version !== "0.1.0" ||
|
packageJson.version !== expectedApplicationVersion ||
|
||||||
packageJson.license !== "GPL-3.0-or-later" ||
|
packageJson.license !== "GPL-3.0-or-later" ||
|
||||||
packageJson.repository?.url !==
|
packageJson.repository?.url !==
|
||||||
"git+https://git.add-ideas.de/zemion/regex-tools.git"
|
"git+https://git.add-ideas.de/zemion/regex-tools.git"
|
||||||
) {
|
) {
|
||||||
throw new Error("Package version, licence or source identity drifted.");
|
throw new Error("Package version, licence or source identity drifted.");
|
||||||
}
|
}
|
||||||
|
const sourceIdentity = await readFile(path.join(root, "SOURCE.md"), "utf8");
|
||||||
|
if (
|
||||||
|
!sourceIdentity.includes(
|
||||||
|
`- Release version: \`${expectedApplicationVersion}\``,
|
||||||
|
) ||
|
||||||
|
!sourceIdentity.includes(
|
||||||
|
`- Release tag: \`v${expectedApplicationVersion}\``,
|
||||||
|
) ||
|
||||||
|
!sourceIdentity.includes(
|
||||||
|
"- Repository: <https://git.add-ideas.de/zemion/regex-tools>",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("SOURCE.md release identity drifted.");
|
||||||
|
}
|
||||||
const licence = await readFile(path.join(root, "LICENSE"), "utf8");
|
const licence = await readFile(path.join(root, "LICENSE"), "utf8");
|
||||||
if (
|
if (
|
||||||
!licence.includes("GNU GENERAL PUBLIC LICENSE") ||
|
!licence.includes("GNU GENERAL PUBLIC LICENSE") ||
|
||||||
@@ -371,6 +386,28 @@ async function collectReleaseEntries() {
|
|||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"SOURCE.md",
|
"SOURCE.md",
|
||||||
"THIRD_PARTY_NOTICES.md",
|
"THIRD_PARTY_NOTICES.md",
|
||||||
|
"engines/README.md",
|
||||||
|
"engines/java/LICENSE.txt",
|
||||||
|
"engines/java/NOTICE.txt",
|
||||||
|
"engines/java/SHA256SUMS",
|
||||||
|
"engines/java/engine-metadata.json",
|
||||||
|
"engines/java/java-regex.mjs",
|
||||||
|
"engines/pcre2/LICENSE.txt",
|
||||||
|
"engines/pcre2/SHA256SUMS",
|
||||||
|
"engines/pcre2/engine-metadata.json",
|
||||||
|
"engines/pcre2/pcre2.mjs",
|
||||||
|
"engines/pcre2/pcre2.wasm",
|
||||||
|
"engines/python/LICENSE.cpython.txt",
|
||||||
|
"engines/python/LICENSE.pyodide.txt",
|
||||||
|
"engines/python/SHA256SUMS",
|
||||||
|
"engines/python/engine-metadata.json",
|
||||||
|
"engines/python/pyodide-lock.json",
|
||||||
|
"engines/python/pyodide.asm.mjs",
|
||||||
|
"engines/python/pyodide.asm.wasm",
|
||||||
|
"engines/python/pyodide.mjs",
|
||||||
|
"engines/python/python_stdlib.zip",
|
||||||
|
"LICENSES/Emscripten-MIT.txt",
|
||||||
|
"LICENSES/PCRE2.txt",
|
||||||
"LICENSES/build/rolldown-1.1.5/LICENSE",
|
"LICENSES/build/rolldown-1.1.5/LICENSE",
|
||||||
"LICENSES/build/vite-8.1.5/LICENSE.md",
|
"LICENSES/build/vite-8.1.5/LICENSE.md",
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
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"],
|
[".css", "text/css; charset=utf-8"],
|
||||||
[".html", "text/html; charset=utf-8"],
|
[".html", "text/html; charset=utf-8"],
|
||||||
[".js", "text/javascript; charset=utf-8"],
|
[".js", "text/javascript; charset=utf-8"],
|
||||||
|
[".mjs", "text/javascript; charset=utf-8"],
|
||||||
[".json", "application/json; charset=utf-8"],
|
[".json", "application/json; charset=utf-8"],
|
||||||
[".svg", "image/svg+xml"],
|
[".svg", "image/svg+xml"],
|
||||||
[".wasm", "application/wasm"],
|
[".wasm", "application/wasm"],
|
||||||
@@ -25,6 +26,14 @@ const testCatalogue = {
|
|||||||
theme: { mode: "system", brand: "add·ideas" },
|
theme: { mode: "system", brand: "add·ideas" },
|
||||||
apps: [{ manifest: "./toolbox-app.json", enabled: true }],
|
apps: [{ manifest: "./toolbox-app.json", enabled: true }],
|
||||||
};
|
};
|
||||||
|
const productionSecurityHeaders = {
|
||||||
|
"Content-Security-Policy":
|
||||||
|
"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'",
|
||||||
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||||
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
|
"Cross-Origin-Resource-Policy": "same-origin",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
};
|
||||||
|
|
||||||
function safeFile(requestPath) {
|
function safeFile(requestPath) {
|
||||||
const decoded = decodeURIComponent(requestPath);
|
const decoded = decodeURIComponent(requestPath);
|
||||||
@@ -49,6 +58,7 @@ const server = createServer(async (request, response) => {
|
|||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
...productionSecurityHeaders,
|
||||||
});
|
});
|
||||||
response.end(JSON.stringify(testCatalogue));
|
response.end(JSON.stringify(testCatalogue));
|
||||||
return;
|
return;
|
||||||
@@ -65,7 +75,7 @@ const server = createServer(async (request, response) => {
|
|||||||
"Content-Type":
|
"Content-Type":
|
||||||
mediaTypes.get(path.extname(file)) ?? "application/octet-stream",
|
mediaTypes.get(path.extname(file)) ?? "application/octet-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Cross-Origin-Resource-Policy": "same-origin",
|
...productionSecurityHeaders,
|
||||||
});
|
});
|
||||||
response.end(content);
|
response.end(content);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
import { lstat, readFile, readdir } from "node:fs/promises";
|
import { lstat, readFile, readdir } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { verifyJavaPack } from "./java-engine-pack.mjs";
|
||||||
|
import { verifyPcre2Pack } from "./pcre2-engine-pack.mjs";
|
||||||
|
import { verifyPythonPack } from "./python-engine-pack.mjs";
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const arguments_ = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (arguments_.length > 0) {
|
||||||
|
const verifiers = new Map([
|
||||||
|
["--java-pack", verifyJavaPack],
|
||||||
|
["--pcre2-pack", verifyPcre2Pack],
|
||||||
|
["--python-pack", verifyPythonPack],
|
||||||
|
]);
|
||||||
|
const verifier = verifiers.get(arguments_[0]);
|
||||||
|
if (arguments_.length !== 2 || !verifier) {
|
||||||
|
throw new Error(
|
||||||
|
"Usage: node scripts/verify-engine-assets.mjs [--java-pack|--pcre2-pack|--python-pack PATH]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const pack = path.resolve(root, arguments_[1]);
|
||||||
|
const metadata = await verifier(pack, root);
|
||||||
|
console.log(`Verified staged ${metadata.engineVersion} engine pack.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
const engineDirectory = path.join(root, "dist", "engines");
|
const engineDirectory = path.join(root, "dist", "engines");
|
||||||
const details = await lstat(engineDirectory);
|
const details = await lstat(engineDirectory);
|
||||||
if (details.isSymbolicLink() || !details.isDirectory()) {
|
if (details.isSymbolicLink() || !details.isDirectory()) {
|
||||||
@@ -14,21 +37,34 @@ const entries = (await readdir(engineDirectory, { withFileTypes: true })).sort(
|
|||||||
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
entries.length !== 1 ||
|
entries.length !== 4 ||
|
||||||
!entries[0]?.isFile() ||
|
!entries[0]?.isFile() ||
|
||||||
entries[0].name !== "README.md"
|
entries[0].name !== "README.md" ||
|
||||||
|
!entries[1]?.isDirectory() ||
|
||||||
|
entries[1].name !== "java" ||
|
||||||
|
!entries[2]?.isDirectory() ||
|
||||||
|
entries[2].name !== "pcre2" ||
|
||||||
|
!entries[3]?.isDirectory() ||
|
||||||
|
entries[3].name !== "python"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Regex Tools 0.1.0 must not ship an undeclared external engine pack.",
|
"The production build must contain only README.md and the declared Java, PCRE2 and Python packs.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const notice = await readFile(path.join(engineDirectory, "README.md"), "utf8");
|
const notice = await readFile(path.join(engineDirectory, "README.md"), "utf8");
|
||||||
if (
|
if (
|
||||||
!notice.includes("native `RegExp`") ||
|
!notice.includes("native `RegExp`") ||
|
||||||
!notice.includes("no external engine")
|
!notice.includes("PCRE2 10.47") ||
|
||||||
|
!notice.includes("Pyodide 314.0.3") ||
|
||||||
|
!notice.includes("TeaVM 0.15.0")
|
||||||
) {
|
) {
|
||||||
throw new Error("The engine asset notice does not describe this release.");
|
throw new Error("The engine asset notice does not describe this release.");
|
||||||
}
|
}
|
||||||
|
await verifyPcre2Pack(path.join(engineDirectory, "pcre2"), root);
|
||||||
|
await verifyPythonPack(path.join(engineDirectory, "python"), root);
|
||||||
|
await verifyJavaPack(path.join(engineDirectory, "java"), root);
|
||||||
|
|
||||||
console.log("Verified ECMAScript-only engine assets (no WebAssembly pack).");
|
console.log(
|
||||||
|
"Verified native ECMAScript and bundled PCRE2, CPython/Pyodide and TeaVM Java engine assets.",
|
||||||
|
);
|
||||||
|
|||||||
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 { render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { RegexExecutionResult } from "../regex/model/match";
|
import type { RegexExecutionResult } from "../regex/model/match";
|
||||||
import type { RegexSyntaxResult } from "../regex/model/syntax";
|
import type { RegexSyntaxResult } from "../regex/model/syntax";
|
||||||
import { CapabilityPanel } from "./CapabilityPanel";
|
import { CapabilityPanel } from "./CapabilityPanel";
|
||||||
@@ -78,11 +79,124 @@ describe("CapabilityPanel", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText("Fixture engine")).toBeInTheDocument();
|
expect(screen.getByText("Fixture engine")).toBeInTheDocument();
|
||||||
expect(screen.getByText("123")).toBeInTheDocument();
|
expect(screen.getByText("123")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Not separately reported by this adapter"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("1", { selector: "dd" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("UTF-8 bytes")).toBeInTheDocument();
|
expect(screen.getByText("UTF-8 bytes")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("fixture")).toBeInTheDocument();
|
||||||
|
|
||||||
const replacement = screen.getByText("Replacement").closest("div");
|
const replacement = screen.getByText("Replacement").closest("div");
|
||||||
expect(replacement).toHaveTextContent("Unavailable");
|
expect(replacement).toHaveTextContent("Unavailable");
|
||||||
const compilation = screen.getByText("Compilation").closest("div");
|
const compilation = screen.getByText("Compilation").closest("div");
|
||||||
expect(compilation).toHaveTextContent("Available");
|
expect(compilation).toHaveTextContent("Available");
|
||||||
|
const generatedCases = screen.getByText("Generated cases").closest("div");
|
||||||
|
expect(generatedCases).toHaveTextContent(
|
||||||
|
"deterministic AST candidates, retained only after actual-engine verification",
|
||||||
|
);
|
||||||
|
const formatting = screen.getByText("Pattern formatting").closest("div");
|
||||||
|
expect(formatting).toHaveTextContent(
|
||||||
|
"grammar-backed literal/control escaping with mandatory exact-snapshot validation",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps PCRE2-only trace and code generation separate from ECMAScript-only tools", () => {
|
||||||
|
render(
|
||||||
|
<CapabilityPanel
|
||||||
|
syntax={syntax}
|
||||||
|
execution={{
|
||||||
|
...execution,
|
||||||
|
engine: {
|
||||||
|
...execution.engine,
|
||||||
|
flavour: "pcre2",
|
||||||
|
engineName: "PCRE2",
|
||||||
|
capabilities: {
|
||||||
|
...execution.engine.capabilities,
|
||||||
|
actualTrace: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const trace = screen.getByText("Actual execution trace").closest("div");
|
||||||
|
expect(trace).toHaveTextContent("Available");
|
||||||
|
const generatedCases = screen.getByText("Generated cases").closest("div");
|
||||||
|
expect(generatedCases).toHaveTextContent(
|
||||||
|
"Unavailable — the ECMAScript case generator is not applied to PCRE2",
|
||||||
|
);
|
||||||
|
const formatting = screen.getByText("Pattern formatting").closest("div");
|
||||||
|
expect(formatting).toHaveTextContent(
|
||||||
|
"Unavailable — the ECMAScript formatter is not applied to PCRE2",
|
||||||
|
);
|
||||||
|
const codeGeneration = screen.getByText("Code generation").closest("div");
|
||||||
|
expect(codeGeneration).toHaveTextContent(
|
||||||
|
"Available — reviewed PCRE2 10.47 8-bit C17 target",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["python", "Python re"],
|
||||||
|
["java", "Java Pattern"],
|
||||||
|
] as const)(
|
||||||
|
"does not borrow ECMAScript or PCRE2 feature semantics for %s",
|
||||||
|
(flavour, label) => {
|
||||||
|
render(
|
||||||
|
<CapabilityPanel
|
||||||
|
syntax={syntax}
|
||||||
|
execution={{
|
||||||
|
...execution,
|
||||||
|
engine: {
|
||||||
|
...execution.engine,
|
||||||
|
flavour,
|
||||||
|
engineName: `${label} fixture`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("Actual execution trace").closest("div"),
|
||||||
|
).toHaveTextContent(
|
||||||
|
"Unavailable — automatic-callout tracing is implemented only for PCRE2",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Static risk analysis").closest("div"),
|
||||||
|
).toHaveTextContent(
|
||||||
|
`Unavailable — ECMAScript heuristics are not applied to ${label}`,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Generated cases").closest("div"),
|
||||||
|
).toHaveTextContent(
|
||||||
|
`Unavailable — the ECMAScript case generator is not applied to ${label}`,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Pattern formatting").closest("div"),
|
||||||
|
).toHaveTextContent(
|
||||||
|
`Unavailable — the ECMAScript formatter is not applied to ${label}`,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Code generation").closest("div"),
|
||||||
|
).toHaveTextContent(
|
||||||
|
"Unavailable — C17 code generation is implemented only for PCRE2",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("offers an accessible close control when used as a modal panel", async () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<CapabilityPanel
|
||||||
|
syntax={syntax}
|
||||||
|
execution={execution}
|
||||||
|
onClose={onClose}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", { name: "Close capabilities" }),
|
||||||
|
);
|
||||||
|
expect(onClose).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import { REGEXPP_VERSION, SYNTAX_PROFILE } from "../version";
|
import { SYNTAX_PROFILE } from "../version";
|
||||||
import type { RegexExecutionResult } from "../regex/model/match";
|
import type { RegexExecutionResult } from "../regex/model/match";
|
||||||
import type { RegexSyntaxResult } from "../regex/model/syntax";
|
import type { RegexSyntaxResult } from "../regex/model/syntax";
|
||||||
import type { RegexEngineCapabilities } from "../regex/model/flavour";
|
import type {
|
||||||
|
RegexEngineCapabilities,
|
||||||
|
RegexFlavourId,
|
||||||
|
} from "../regex/model/flavour";
|
||||||
|
|
||||||
|
const FLAVOUR_LABELS: Readonly<Record<RegexFlavourId, string>> = {
|
||||||
|
ecmascript: "ECMAScript",
|
||||||
|
pcre2: "PCRE2",
|
||||||
|
pcre: "PCRE",
|
||||||
|
python: "Python re",
|
||||||
|
go: "Go regexp",
|
||||||
|
rust: "Rust regex",
|
||||||
|
dotnet: ".NET Regex",
|
||||||
|
java: "Java Pattern",
|
||||||
|
};
|
||||||
|
|
||||||
function capability(
|
function capability(
|
||||||
capabilities: RegexEngineCapabilities | undefined,
|
capabilities: RegexEngineCapabilities | undefined,
|
||||||
@@ -35,23 +49,30 @@ function offsetLabel(
|
|||||||
export function CapabilityPanel({
|
export function CapabilityPanel({
|
||||||
syntax,
|
syntax,
|
||||||
execution,
|
execution,
|
||||||
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
readonly syntax?: RegexSyntaxResult;
|
readonly syntax?: RegexSyntaxResult;
|
||||||
readonly execution?: RegexExecutionResult;
|
readonly execution?: RegexExecutionResult;
|
||||||
|
readonly onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const capabilities = execution?.engine.capabilities;
|
const capabilities = execution?.engine.capabilities;
|
||||||
|
const activeFlavour =
|
||||||
|
execution?.engine.flavour ?? syntax?.root.support.flavour;
|
||||||
|
const activeFlavourLabel = activeFlavour
|
||||||
|
? FLAVOUR_LABELS[activeFlavour]
|
||||||
|
: undefined;
|
||||||
const rows = [
|
const rows = [
|
||||||
[
|
[
|
||||||
"Flavour",
|
"Flavour",
|
||||||
execution?.engine.flavour ??
|
execution?.engine.flavour ??
|
||||||
syntax?.root.support.flavour ??
|
syntax?.root.support.flavour ??
|
||||||
"ECMAScript (awaiting workers)",
|
"Awaiting flavour workers",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Syntax provider",
|
"Syntax provider",
|
||||||
syntax
|
syntax
|
||||||
? `${syntax.provider.id} ${syntax.provider.version}`
|
? `${syntax.provider.id} ${syntax.provider.version}`
|
||||||
: `regexpp ${REGEXPP_VERSION} (awaiting syntax worker)`,
|
: "Awaiting syntax worker result",
|
||||||
],
|
],
|
||||||
["Syntax profile", SYNTAX_PROFILE],
|
["Syntax profile", SYNTAX_PROFILE],
|
||||||
[
|
[
|
||||||
@@ -65,9 +86,20 @@ export function CapabilityPanel({
|
|||||||
execution?.engine.engineName ?? "Awaiting first engine result",
|
execution?.engine.engineName ?? "Awaiting first engine result",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Engine/runtime version",
|
"Engine version",
|
||||||
execution?.engine.engineVersion ?? "Shown after first execution",
|
execution?.engine.engineVersion ?? "Shown after first execution",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"Runtime version",
|
||||||
|
execution?.engine.runtimeVersion ??
|
||||||
|
(execution
|
||||||
|
? "Not separately reported by this adapter"
|
||||||
|
: "Shown after first execution"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Adapter version",
|
||||||
|
execution?.engine.adapterVersion ?? "Shown after first execution",
|
||||||
|
],
|
||||||
["Native offsets", offsetLabel(execution?.engine.offsetUnit)],
|
["Native offsets", offsetLabel(execution?.engine.offsetUnit)],
|
||||||
["Compilation", capability(capabilities, "compilation")],
|
["Compilation", capability(capabilities, "compilation")],
|
||||||
["Matching", capability(capabilities, "matching")],
|
["Matching", capability(capabilities, "matching")],
|
||||||
@@ -83,13 +115,57 @@ export function CapabilityPanel({
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Actual execution trace",
|
"Actual execution trace",
|
||||||
capability(
|
activeFlavour === "pcre2"
|
||||||
|
? capability(
|
||||||
capabilities,
|
capabilities,
|
||||||
"actualTrace",
|
"actualTrace",
|
||||||
"the adapter exposes no engine trace",
|
"this PCRE2 adapter exposes no bounded automatic-callout trace",
|
||||||
),
|
)
|
||||||
|
: activeFlavour
|
||||||
|
? "Unavailable — automatic-callout tracing is implemented only for PCRE2"
|
||||||
|
: "Awaiting flavour worker result",
|
||||||
],
|
],
|
||||||
["Benchmark", capability(capabilities, "benchmark")],
|
["Benchmark", capability(capabilities, "benchmark")],
|
||||||
|
[
|
||||||
|
"Static risk analysis",
|
||||||
|
activeFlavour === "ecmascript"
|
||||||
|
? "Available — advisory ECMAScript 2025 heuristics"
|
||||||
|
: activeFlavourLabel
|
||||||
|
? `Unavailable — ECMAScript heuristics are not applied to ${activeFlavourLabel}`
|
||||||
|
: "Awaiting flavour worker result",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Generated cases",
|
||||||
|
activeFlavour === "ecmascript"
|
||||||
|
? "Available — deterministic AST candidates, retained only after actual-engine verification"
|
||||||
|
: activeFlavourLabel
|
||||||
|
? `Unavailable — the ECMAScript case generator is not applied to ${activeFlavourLabel}`
|
||||||
|
: "Awaiting flavour worker result",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Pattern formatting",
|
||||||
|
activeFlavour === "ecmascript"
|
||||||
|
? "Available — grammar-backed literal/control escaping with mandatory exact-snapshot validation"
|
||||||
|
: activeFlavourLabel
|
||||||
|
? `Unavailable — the ECMAScript formatter is not applied to ${activeFlavourLabel}`
|
||||||
|
: "Awaiting flavour worker result",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Code generation",
|
||||||
|
activeFlavour === "pcre2"
|
||||||
|
? "Available — reviewed PCRE2 10.47 8-bit C17 target"
|
||||||
|
: activeFlavour
|
||||||
|
? "Unavailable — C17 code generation is implemented only for PCRE2"
|
||||||
|
: "Awaiting flavour worker result",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Known syntax gaps",
|
||||||
|
syntax
|
||||||
|
? syntax.coverage.unsupportedConstructs.length > 0
|
||||||
|
? syntax.coverage.unsupportedConstructs.join("; ")
|
||||||
|
: "None reported by the active provider"
|
||||||
|
: "Awaiting syntax worker result",
|
||||||
|
],
|
||||||
] as const;
|
] as const;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -101,7 +177,20 @@ export function CapabilityPanel({
|
|||||||
<p className="eyebrow">Current provider and adapter metadata</p>
|
<p className="eyebrow">Current provider and adapter metadata</p>
|
||||||
<h2 id="capability-heading">Capabilities</h2>
|
<h2 id="capability-heading">Capabilities</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="panel-heading-actions">
|
||||||
<span className="provenance-badge">Community build</span>
|
<span className="provenance-badge">Community build</span>
|
||||||
|
{onClose ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
aria-label="Close capabilities"
|
||||||
|
data-dialog-initial-focus
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<dl className="capability-grid">
|
<dl className="capability-grid">
|
||||||
{rows.map(([term, value]) => (
|
{rows.map(([term, value]) => (
|
||||||
@@ -112,10 +201,11 @@ export function CapabilityPanel({
|
|||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
<p className="capability-roadmap">
|
<p className="capability-roadmap">
|
||||||
Next flavour: official PCRE2 10.47 WebAssembly with actual callout
|
Availability is never inferred by substituting one flavour for another.
|
||||||
traces. Python, Go, Rust, .NET and Java remain unavailable until their
|
Static analysis, generated cases and formatting are ECMAScript-only;
|
||||||
named runtimes pass the same worker, offset, conformance and licensing
|
automatic-callout tracing and reviewed C17 generation are PCRE2-only.
|
||||||
gates.
|
Python and Java retain the engine identity and compatibility boundaries
|
||||||
|
reported by their own workers.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
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 { useEffect, useRef } from "react";
|
||||||
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
|
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
|
||||||
|
import {
|
||||||
|
DEFAULT_GENERATION_SETTINGS,
|
||||||
|
GENERATION_LIMITS,
|
||||||
|
} from "../regex/generation/generation-limits";
|
||||||
import { manifest } from "../toolbox/manifest";
|
import { manifest } from "../toolbox/manifest";
|
||||||
|
|
||||||
const releaseSourceUrl = `https://git.add-ideas.de/zemion/regex-tools/src/tag/v${manifest.version}`;
|
const releaseSourceUrl = `https://git.add-ideas.de/zemion/regex-tools/src/tag/v${manifest.version}`;
|
||||||
@@ -43,10 +47,19 @@ export function HelpDialog({
|
|||||||
<section>
|
<section>
|
||||||
<h3>What runs where</h3>
|
<h3>What runs where</h3>
|
||||||
<p>
|
<p>
|
||||||
Syntax parsing and actual ECMAScript execution run in separate,
|
Syntax parsing and active-engine execution run in separate, killable
|
||||||
killable browser workers. Patterns, subjects and projects are never
|
browser workers. ECMAScript uses the browser RegExp implementation;
|
||||||
uploaded. There is no backend, account, telemetry, CDN dependency,
|
PCRE2 uses the bundled 10.47 WebAssembly engine; Python uses CPython
|
||||||
or executable replacement function.
|
3.14 through Pyodide; and Java uses TeaVM's java.util.regex
|
||||||
|
compatibility implementation. The result metadata names the runtime
|
||||||
|
actually used. Patterns, subjects and projects are never uploaded.
|
||||||
|
There is no backend, account, telemetry, CDN dependency, or
|
||||||
|
executable replacement function.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
TeaVM 0.15.0 is not OpenJDK. Its replacement implementation accepts
|
||||||
|
single-digit $n references, so $12 means $1 followed by literal 2.
|
||||||
|
Java SE ${"{name}"} references are reported as unavailable.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
@@ -56,8 +69,47 @@ export function HelpDialog({
|
|||||||
syntax provider. The extraction tree contains actual browser-engine
|
syntax provider. The extraction tree contains actual browser-engine
|
||||||
matches and captures. Replacement mode adds a typed token tree and
|
matches and captures. Replacement mode adds a typed token tree and
|
||||||
bounded per-match contribution/range mapping over those actual
|
bounded per-match contribution/range mapping over those actual
|
||||||
matches. None is an internal V8, SpiderMonkey or JavaScriptCore
|
matches. These views are not internal engine traces. The separate
|
||||||
execution trace.
|
PCRE trace viewer shows actual bounded automatic callouts reported
|
||||||
|
by PCRE2; its adjacent-position movement labels are derived and it
|
||||||
|
is not a complete record of every internal action. Browser
|
||||||
|
ECMAScript exposes no corresponding V8, SpiderMonkey or
|
||||||
|
JavaScriptCore trace API.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Advisory analysis</h3>
|
||||||
|
<p>
|
||||||
|
Static risk findings are ECMAScript-2025-specific heuristics, and
|
||||||
|
generated-input growth observations describe only the selected
|
||||||
|
bounded samples. Neither proves safety or general complexity. Growth
|
||||||
|
and cold/warm benchmark timing run in a separate killable worker and
|
||||||
|
never include the PCRE2 trace path.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Generated cases</h3>
|
||||||
|
<p>
|
||||||
|
ECMAScript generator v1 deterministically samples supported
|
||||||
|
normalized-AST paths from an explicit seed. Every intended positive
|
||||||
|
and negative subject runs through the selected actual engine before
|
||||||
|
it can become a test; candidates with the wrong outcome are
|
||||||
|
discarded. The coverage report names unsupported constructs.
|
||||||
|
Sampling is not proof of complete language coverage. Other flavours
|
||||||
|
remain unavailable until their structural providers expose the
|
||||||
|
required AST; their syntax is never reinterpreted as ECMAScript.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Pattern formatting</h3>
|
||||||
|
<p>
|
||||||
|
The ECMAScript formatter makes only grammar-backed literal/control
|
||||||
|
escape changes; it does not insert layout whitespace or reinterpret
|
||||||
|
another flavour's syntax. Applying a preview requires
|
||||||
|
independent source and candidate reparsing, actual-engine
|
||||||
|
match/replacement comparison, every applicable exact unit test, and
|
||||||
|
an explicit confirmation. Passing bounded checks is evidence for
|
||||||
|
those snapshots, not proof for every possible subject.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
@@ -105,6 +157,78 @@ export function HelpDialog({
|
|||||||
<dt>Unit-test suite wall time</dt>
|
<dt>Unit-test suite wall time</dt>
|
||||||
<dd>60 seconds</dd>
|
<dd>60 seconds</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Corpus input</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumCorpusDocuments.toLocaleString()}{" "}
|
||||||
|
documents · {DEFAULT_REGEX_LIMITS.corpusHardBytes / 1024 / 1024}{" "}
|
||||||
|
MiB
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Corpus wall time</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs / 60_000} minutes
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Analysis wall time</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumBenchmarkWallTimeMs / 1_000}{" "}
|
||||||
|
seconds
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Benchmark iteration cap</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumBenchmarkIterations.toLocaleString()}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Growth probe steps</dt>
|
||||||
|
<dd>24</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Generated cases</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumGeneratedCases.toLocaleString()}{" "}
|
||||||
|
retained ·{" "}
|
||||||
|
{GENERATION_LIMITS.maximumCandidateAttempts.toLocaleString()}{" "}
|
||||||
|
attempts
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Generation defaults</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_GENERATION_SETTINGS.perCaseTimeoutMs} ms per case ·{" "}
|
||||||
|
{DEFAULT_GENERATION_SETTINGS.maximumWallTimeMs / 1_000} seconds
|
||||||
|
aggregate
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Generated subject text</dt>
|
||||||
|
<dd>
|
||||||
|
{GENERATION_LIMITS.maximumTotalSubjectBytes / 1024 / 1024} MiB
|
||||||
|
aggregate hard cap
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Formatter validation</dt>
|
||||||
|
<dd>1,000 tests · 60 seconds aggregate</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Independent corpus lines</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumCorpusLines.toLocaleString()}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Corpus applied output</dt>
|
||||||
|
<dd>
|
||||||
|
{DEFAULT_REGEX_LIMITS.maximumCorpusOutputBytes / 1024 / 1024}{" "}
|
||||||
|
MiB
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<p>
|
<p>
|
||||||
A timeout terminates the worker. It means “timed out”, never “no
|
A timeout terminates the worker. It means “timed out”, never “no
|
||||||
@@ -116,7 +240,15 @@ export function HelpDialog({
|
|||||||
<p>
|
<p>
|
||||||
Ad-hoc test text is excluded from local saves and exports by
|
Ad-hoc test text is excluded from local saves and exports by
|
||||||
default. Test definitions are deliberate project data. Import is
|
default. Test definitions are deliberate project data. Import is
|
||||||
validated and paused until you review and run it.
|
validated and paused until you review and run it. Corpus documents,
|
||||||
|
contents, outputs and results are always ephemeral and excluded from
|
||||||
|
projects and IndexedDB. Analysis settings and results are likewise
|
||||||
|
ephemeral. Generated-case settings and review results are ephemeral;
|
||||||
|
only cases explicitly added to the test suite persist, together with
|
||||||
|
their bounded seed and generator provenance. Test subjects remain
|
||||||
|
excluded unless separately opted in. Formatter previews and
|
||||||
|
validation observations are ephemeral; only an explicitly applied
|
||||||
|
pattern enters workbench/project state.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
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>
|
<p>
|
||||||
<strong>{project.name}</strong> · schema {project.schemaVersion}
|
<strong>{project.name}</strong> · schema {project.schemaVersion}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="field-help">
|
||||||
|
Corpus documents, applied outputs and corpus results are always excluded
|
||||||
|
from project JSON and local saves.
|
||||||
|
</p>
|
||||||
<label className="check-control">
|
<label className="check-control">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { QuickReference } from "./QuickReference";
|
|||||||
|
|
||||||
describe("QuickReference", () => {
|
describe("QuickReference", () => {
|
||||||
it("shows flavour, flags, example and authoritative source before insertion", async () => {
|
it("shows flavour, flags, example and authoritative source before insertion", async () => {
|
||||||
const onInsert = vi.fn();
|
const onUsePattern = vi.fn();
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<QuickReference onInsert={onInsert} />);
|
render(<QuickReference onUsePattern={onUsePattern} />);
|
||||||
|
|
||||||
await user.type(
|
await user.type(
|
||||||
screen.getByLabelText("Filter constructs"),
|
screen.getByLabelText("Filter constructs"),
|
||||||
@@ -20,7 +20,100 @@ describe("QuickReference", () => {
|
|||||||
screen.getByRole("link", { name: "ECMAScript Atom grammar" }),
|
screen.getByRole("link", { name: "ECMAScript Atom grammar" }),
|
||||||
).toHaveAttribute("href", expect.stringContaining("tc39.es"));
|
).toHaveAttribute("href", expect.stringContaining("tc39.es"));
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Insert example" }));
|
await user.click(
|
||||||
expect(onInsert).toHaveBeenCalledWith("a.b");
|
screen.getByRole("button", { name: "Use example as pattern" }),
|
||||||
|
);
|
||||||
|
expect(onUsePattern).toHaveBeenCalledWith("a.b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an explicit empty state for a filter without matches", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<QuickReference onUsePattern={vi.fn()} />);
|
||||||
|
|
||||||
|
await user.type(
|
||||||
|
screen.getByLabelText("Filter constructs"),
|
||||||
|
"no such construct",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("status")).toHaveTextContent(
|
||||||
|
"No reference constructs match",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("0 of 12 constructs")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches to the authoritative PCRE2 construct set", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<QuickReference flavour="pcre2" onUsePattern={vi.fn()} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Original PCRE2 reference")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("8 of 8 constructs")).toBeInTheDocument();
|
||||||
|
await user.type(screen.getByLabelText("Filter constructs"), "branch reset");
|
||||||
|
expect(screen.getByText("Branch-reset group")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "PCRE2 pattern documentation" }),
|
||||||
|
).toHaveAttribute("href", expect.stringContaining("pcre.org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an original Python 3.14 re reference without borrowing ECMAScript syntax", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<QuickReference flavour="python" onUsePattern={vi.fn()} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("Original Python 3.14 re reference"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("8 of 8 constructs")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText("Filter constructs"), "absolute end");
|
||||||
|
|
||||||
|
expect(screen.getByText("Absolute end anchor")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("\\z")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("python")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: "Python 3.14 re documentation" }),
|
||||||
|
).toHaveAttribute("href", "https://docs.python.org/3.14/library/re.html");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an original Java Pattern reference from the official Java API", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<QuickReference flavour="java" onUsePattern={vi.fn()} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("Original Java Pattern reference"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("9 of 9 constructs")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/bundled TeaVM 0\.15\.0 adapter is a tested compatibility subset/u,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText("Filter constructs"), "intersection");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("Character-class intersection"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("[class&&class]")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("java")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", {
|
||||||
|
name: "Java SE 25 Pattern documentation",
|
||||||
|
}),
|
||||||
|
).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/regex/Pattern.html",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not relabel an unreviewed flavour as ECMAScript", () => {
|
||||||
|
render(<QuickReference flavour="go" onUsePattern={vi.fn()} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("Original Go regexp reference"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("0 of 0 constructs")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("status")).toHaveTextContent(
|
||||||
|
"No reviewed quick reference is available for Go regexp.",
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("Any character")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { ECMASCRIPT_REFERENCE } from "../regex/reference/token-reference";
|
import type { RegexFlavourId } from "../regex/model/flavour";
|
||||||
|
import { REFERENCE_BY_FLAVOUR } from "../regex/reference/token-reference";
|
||||||
|
|
||||||
export function QuickReference({
|
export function QuickReference({
|
||||||
onInsert,
|
onUsePattern,
|
||||||
|
onClose,
|
||||||
|
flavour = "ecmascript",
|
||||||
}: {
|
}: {
|
||||||
readonly onInsert: (syntax: string) => void;
|
readonly onUsePattern: (syntax: string) => void;
|
||||||
|
readonly onClose?: () => void;
|
||||||
|
readonly flavour?: RegexFlavourId;
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const referenceSet = REFERENCE_BY_FLAVOUR[flavour];
|
||||||
|
const reference = referenceSet.entries;
|
||||||
const entries = useMemo(() => {
|
const entries = useMemo(() => {
|
||||||
const needle = query.toLocaleLowerCase();
|
const normalizeSearchText = (value: string) =>
|
||||||
|
value
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.replaceAll(/[^\p{L}\p{N}]+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
const needle = normalizeSearchText(query);
|
||||||
return needle
|
return needle
|
||||||
? ECMASCRIPT_REFERENCE.filter((entry) =>
|
? reference.filter((entry) =>
|
||||||
|
normalizeSearchText(
|
||||||
[
|
[
|
||||||
entry.construct,
|
entry.construct,
|
||||||
entry.syntax,
|
entry.syntax,
|
||||||
@@ -18,13 +31,11 @@ export function QuickReference({
|
|||||||
entry.example,
|
entry.example,
|
||||||
entry.flags.join(" "),
|
entry.flags.join(" "),
|
||||||
entry.source,
|
entry.source,
|
||||||
]
|
].join(" "),
|
||||||
.join(" ")
|
).includes(needle),
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(needle),
|
|
||||||
)
|
)
|
||||||
: ECMASCRIPT_REFERENCE;
|
: reference;
|
||||||
}, [query]);
|
}, [query, reference]);
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="panel reference-panel"
|
className="panel reference-panel"
|
||||||
@@ -32,9 +43,25 @@ export function QuickReference({
|
|||||||
>
|
>
|
||||||
<header className="panel-heading">
|
<header className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Original ECMAScript reference</p>
|
<p className="eyebrow">Original {referenceSet.label} reference</p>
|
||||||
<h2 id="reference-heading">Quick reference</h2>
|
<h2 id="reference-heading">Quick reference</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="panel-heading-actions">
|
||||||
|
<span>
|
||||||
|
{entries.length} of {reference.length} constructs
|
||||||
|
</span>
|
||||||
|
{onClose ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
aria-label="Close quick reference"
|
||||||
|
data-dialog-initial-focus
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<label className="stacked-control">
|
<label className="stacked-control">
|
||||||
<span>Filter constructs</span>
|
<span>Filter constructs</span>
|
||||||
@@ -44,6 +71,10 @@ export function QuickReference({
|
|||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{referenceSet.note ? (
|
||||||
|
<p className="reference-note">{referenceSet.note}</p>
|
||||||
|
) : null}
|
||||||
|
{entries.length > 0 ? (
|
||||||
<ul className="reference-list">
|
<ul className="reference-list">
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<li key={entry.id}>
|
<li key={entry.id}>
|
||||||
@@ -82,13 +113,20 @@ export function QuickReference({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary-button"
|
className="secondary-button"
|
||||||
onClick={() => onInsert(entry.example)}
|
onClick={() => onUsePattern(entry.example)}
|
||||||
>
|
>
|
||||||
Insert example
|
Use example as pattern
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="empty-state" role="status">
|
||||||
|
{reference.length === 0
|
||||||
|
? `No reviewed quick reference is available for ${referenceSet.label}.`
|
||||||
|
: `No reference constructs match “${query.trim()}”.`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,29 @@ function renderResult(result: RegexReplacementResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("ReplacementPanel completeness", () => {
|
describe("ReplacementPanel completeness", () => {
|
||||||
|
it.each([
|
||||||
|
["python", "Python re"],
|
||||||
|
["java", "Java Pattern"],
|
||||||
|
] as const)(
|
||||||
|
"labels %s templates without relabelling them",
|
||||||
|
(flavour, label) => {
|
||||||
|
render(
|
||||||
|
<ReplacementPanel
|
||||||
|
flavour={flavour}
|
||||||
|
replacement=""
|
||||||
|
onReplacementChange={() => undefined}
|
||||||
|
onSelectToken={() => undefined}
|
||||||
|
onSelectContribution={() => undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(`${label} template syntax`)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(`Native matches · bounded ${label} substitution`),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("labels a match-limited replacement as partial", () => {
|
it("labels a match-limited replacement as partial", () => {
|
||||||
renderResult({
|
renderResult({
|
||||||
execution: { ...execution, truncated: true },
|
execution: { ...execution, truncated: true },
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user