Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d40af2e112 | |||
| d2e9b3b569 |
3
.npmrc
3
.npmrc
@@ -1,2 +1 @@
|
|||||||
@add-ideas:registry=https://git.add-ideas.de/api/packages/zemion/npm/
|
@add-ideas:registry=https://git.add-ideas.de/api/packages/lotobo/npm/
|
||||||
|
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.4.2 — 2026-07-27
|
||||||
|
|
||||||
|
- Move canonical source and package identity from the `zemion` account to the
|
||||||
|
`lotobo` organization and consume Toolbox SDK 0.2.3 from its new namespace.
|
||||||
|
- Remove the outer application frame and align the workbench with the shared
|
||||||
|
`min(100%, 90rem)` Toolbox content width.
|
||||||
|
- Replace the favicon with a file-style Regex mark derived from the MIT-licensed
|
||||||
|
OOjs regular-expression icon, retaining its licence and attribution.
|
||||||
|
- Rebuild the Go WebAssembly engine so its embedded module path uses the new
|
||||||
|
canonical repository identity.
|
||||||
|
|
||||||
|
## 0.4.1 — 2026-07-27
|
||||||
|
|
||||||
|
- Keep the last completed syntax and execution frame visible while edited
|
||||||
|
inputs are parsed and executed, then replace it atomically with the newest
|
||||||
|
revision instead of rendering transient empty panels.
|
||||||
|
- Bind every retained result to its exact flavour, options, pattern, flags,
|
||||||
|
subject, replacement, mode and syntax snapshots. Mark it as stale, disable
|
||||||
|
execution until current syntax is ready, and suppress stale editor ranges
|
||||||
|
and cross-panel selection.
|
||||||
|
|
||||||
## 0.4.0 — 2026-07-27
|
## 0.4.0 — 2026-07-27
|
||||||
|
|
||||||
- Expand the workbench from four to twelve selectable execution profiles:
|
- Expand the workbench from four to twelve selectable execution profiles:
|
||||||
|
|||||||
28
LICENSES/OOjs-UI-icon-MIT.txt
Normal file
28
LICENSES/OOjs-UI-icon-MIT.txt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
OOjs UI regular-expression-progressive icon
|
||||||
|
|
||||||
|
Source:
|
||||||
|
https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_regular-expression-progressive.svg
|
||||||
|
|
||||||
|
The icon path is adapted into public/favicon.svg and recoloured. The
|
||||||
|
surrounding rounded square, document shape, gradient and shadow are original
|
||||||
|
Regex Tools artwork and are not part of the upstream icon.
|
||||||
|
|
||||||
|
Copyright © 2011–2017 OOjs UI Team and other contributors
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -8,9 +8,9 @@ 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 release — 0.4.0
|
## Current release — 0.4.2
|
||||||
|
|
||||||
The v0.4.0 release contains twelve selectable, executable profiles:
|
The v0.4.2 release contains twelve selectable, executable profiles:
|
||||||
|
|
||||||
- **JavaScript (ECMAScript)** — the current browser's native `RegExp`, with
|
- **JavaScript (ECMAScript)** — the current browser's native `RegExp`, with
|
||||||
ECMAScript 2025 syntax supplied by regexpp 4.12.2;
|
ECMAScript 2025 syntax supplied by regexpp 4.12.2;
|
||||||
@@ -99,6 +99,9 @@ relabeled Scala engine.
|
|||||||
- 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.
|
||||||
|
- Live editing retains the last completed, input-bound syntax and result frame
|
||||||
|
until the newest revision finishes. Stale frames are labelled and cannot
|
||||||
|
enable Run, decorate current editors, or drive current-input selections.
|
||||||
- ECMAScript capture ranges use the engine's `d` indices result; every other
|
- ECMAScript capture ranges use the engine's `d` indices result; every other
|
||||||
profile returns its native range records through a bounded fixed bridge.
|
profile returns its native range records through a bounded fixed bridge.
|
||||||
- Named, numbered, optional, empty and repeated-final captures are represented.
|
- Named, numbered, optional, empty and repeated-final captures are represented.
|
||||||
@@ -353,7 +356,7 @@ The normal build performs no network download. `vite.config.ts` uses
|
|||||||
|
|
||||||
## Toolbox SDK and Portal
|
## Toolbox SDK and Portal
|
||||||
|
|
||||||
The application uses Toolbox SDK 0.2.2 `AppShell`. Its canonical typed/static
|
The application uses Toolbox SDK 0.2.3 `AppShell`. Its canonical typed/static
|
||||||
manifest source is `src/toolbox/manifest.source.json`; generation fails if its
|
manifest source is `src/toolbox/manifest.source.json`; generation fails if its
|
||||||
version or source identity drifts.
|
version or source identity drifts.
|
||||||
|
|
||||||
|
|||||||
12
SOURCE.md
12
SOURCE.md
@@ -1,10 +1,10 @@
|
|||||||
# Source identity
|
# Source identity
|
||||||
|
|
||||||
- Project: Regex Tools
|
- Project: Regex Tools
|
||||||
- Release version: `0.4.0`
|
- Release version: `0.4.2`
|
||||||
- Release tag: `v0.4.0`
|
- Release tag: `v0.4.2`
|
||||||
- Repository: <https://git.add-ideas.de/zemion/regex-tools>
|
- Repository: <https://git.add-ideas.de/lotobo/regex-tools>
|
||||||
- Previous release tag: `v0.3.0`
|
- Previous release tag: `v0.4.1`
|
||||||
- Licence: GPL-3.0-or-later
|
- Licence: GPL-3.0-or-later
|
||||||
- Copyright: © 2026 Albrecht Degering
|
- Copyright: © 2026 Albrecht Degering
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ metadata, closed file sets, checksums and exact licence/notice material are
|
|||||||
installed under `public/engines/` and included in the application source and
|
installed under `public/engines/` and included in the application source and
|
||||||
build.
|
build.
|
||||||
|
|
||||||
The v0.4.0 release includes native browser ECMAScript, standalone PCRE2 10.47,
|
The v0.4.2 release includes native browser ECMAScript, standalone PCRE2 10.47,
|
||||||
PHP 8.5.8 `preg` with PCRE2 10.44, legacy Perl 5.28.1 via WebPerl 0.09-beta,
|
PHP 8.5.8 `preg` with PCRE2 10.44, legacy Perl 5.28.1 via WebPerl 0.09-beta,
|
||||||
CPython 3.14.2 `re`, CRuby 4.0.0 `Regexp`, TeaVM 0.15.0 `java.util.regex`,
|
CPython 3.14.2 `re`, CRuby 4.0.0 `Regexp`, TeaVM 0.15.0 `java.util.regex`,
|
||||||
Emscripten 6.0.4 libc++ `std::wregex`, Go 1.26.5 `regexp`, Rust `regex` 1.13.1,
|
Emscripten 6.0.4 libc++ `std::wregex`, Go 1.26.5 `regexp`, Rust `regex` 1.13.1,
|
||||||
@@ -91,4 +91,4 @@ TeaVM-backed Scala/JVM compatibility profile.
|
|||||||
|
|
||||||
Earlier releases remain available under their historical tags and artifact
|
Earlier releases remain available under their historical tags and artifact
|
||||||
coordinates. A future public artifact must bump package, manifest, tag and
|
coordinates. A future public artifact must bump package, manifest, tag and
|
||||||
source identity together; it must not reuse `v0.4.0`.
|
source identity together; it must not reuse `v0.4.2`.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ 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.3 | Apache-2.0 | Runtime | Manifest contract; <https://git.add-ideas.de/lotobo/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.3 | 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> |
|
||||||
@@ -46,7 +46,12 @@ Exact dependency resolution is recorded in `package-lock.json`.
|
|||||||
| 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> |
|
||||||
|
|
||||||
`@add-ideas/toolbox-testkit` 0.2.2 and the other test/build dependencies are
|
The application icon adapts and recolours the MIT-licensed
|
||||||
|
[OOjs UI regular-expression-progressive icon](https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_regular-expression-progressive.svg)
|
||||||
|
inside original Regex Tools file artwork. Its copyright notice, source and
|
||||||
|
licence are preserved in `LICENSES/OOjs-UI-icon-MIT.txt`.
|
||||||
|
|
||||||
|
`@add-ideas/toolbox-testkit` 0.2.3 and the other test/build dependencies are
|
||||||
development-only and are not part of the static runtime bundle.
|
development-only and are not part of the static runtime bundle.
|
||||||
|
|
||||||
The PHP pack's `native-components.json` is scoped to the exact
|
The PHP pack's `native-components.json` is scoped to the exact
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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 captures expose only the final retained value in all twelve v0.4.0
|
Repeated captures expose only the final retained value in all twelve v0.4.2
|
||||||
profiles. This includes .NET even though its native API can retain capture
|
profiles. This includes .NET even though its native API can retain capture
|
||||||
history: the current adapter deliberately normalizes only the final record and
|
history: the current adapter deliberately normalizes only the final record and
|
||||||
reports `captureHistory: false`. The UI labels the limitation and does not
|
reports `captureHistory: false`. The UI labels the limitation and does not
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Flavour support
|
# Flavour support
|
||||||
|
|
||||||
Version 0.4.0 exposes twelve executable profiles. “Partial lexical provider”
|
Version 0.4.2 exposes twelve executable profiles. “Partial lexical provider”
|
||||||
means that Regex Tools recognizes common structure and replacement tokens for
|
means that Regex Tools recognizes common structure and replacement tokens for
|
||||||
explanation only; the named runtime compiler and replacement operation remain
|
explanation only; the named runtime compiler and replacement operation remain
|
||||||
authoritative.
|
authoritative.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ selected real ECMAScript or PCRE2 worker. It can preserve:
|
|||||||
authoritative, untruncated output.
|
authoritative, untruncated output.
|
||||||
|
|
||||||
PHP, Perl, Python, Ruby, Java, C++, Go, Rust, .NET and Scala compatibility are
|
PHP, Perl, Python, Ruby, Java, C++, Go, Rust, .NET and Scala compatibility are
|
||||||
not supported minimization targets in v0.4.0. The workspace disables reduction
|
not supported minimization targets in v0.4.2. The workspace disables reduction
|
||||||
for those active profiles; it does not substitute ECMAScript, PCRE2 or a
|
for those active profiles; it does not substitute ECMAScript, PCRE2 or a
|
||||||
lexical provider as an execution oracle.
|
lexical provider as an execution oracle.
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ should test both a direct root mount and a nested mount such as
|
|||||||
`/apps/regex/`, including cold loading of all twelve profiles.
|
`/apps/regex/`, including cold loading of all twelve profiles.
|
||||||
|
|
||||||
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.4.0`, and mount it at a
|
manifest ID `de.add-ideas.regex-tools` and version `0.4.2`, and mount it at a
|
||||||
relative target such as `apps/regex/`.
|
relative target such as `apps/regex/`.
|
||||||
|
|
||||||
The package, manifest, tag, artifact URL and checksum must all use the v0.4.0
|
The package, manifest, tag, artifact URL and checksum must all use the v0.4.2
|
||||||
identity. Historical artifact coordinates remain immutable and must not be
|
identity. Historical artifact coordinates remain immutable and must not be
|
||||||
overwritten or reused.
|
overwritten or reused.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ 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 | Release identity `v0.4.0` | GPL-3.0-or-later | Application source written for this project. |
|
| Regex Tools | Release identity `v0.4.2` | 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 | `ef2dab4b46c61812c9a877d8a19fe497b4a4630a` / 0.2.3 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. |
|
||||||
| Toolbox Portal | `9347554324299c8ec9dfaec2c9730907736f62c3` / aggregate v0.8.0 / package 0.2.7 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
|
| Toolbox Portal | `43670a58249a920d37b7465cfda6724f644a92f7` / aggregate v0.9.1 / package 0.2.9 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
|
||||||
| regexpp | 4.12.2; pinned npm integrity in `package-lock.json` | MIT | ECMAScript 2025 syntax provider; normalized in the syntax worker. |
|
| regexpp | 4.12.2; pinned npm integrity in `package-lock.json` | MIT | ECMAScript 2025 syntax provider; normalized in the syntax worker. |
|
||||||
| PCRE2 | Signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | Standalone pinned offline build; verified 8-bit WebAssembly pack shipped, source not vendored. |
|
| PCRE2 | Signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | Standalone pinned offline build; verified 8-bit WebAssembly pack shipped, source not vendored. |
|
||||||
| PHP | Executable identity 8.5.8; embedded PCRE2 identity 10.44 | PHP License version 4 (BSD-3-Clause), elected through the earlier licence's later-version option | Actual PHP `preg_*` compiler, matcher and replacement runtime; PHP source not vendored. |
|
| PHP | Executable identity 8.5.8; embedded PCRE2 identity 10.44 | PHP License version 4 (BSD-3-Clause), elected through the earlier licence's later-version option | Actual PHP `preg_*` compiler, matcher and replacement runtime; PHP source not vendored. |
|
||||||
@@ -30,6 +30,6 @@ Inspection date: 2026-07-27.
|
|||||||
| 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 through v0.4.0 conformance cases are project-authored. See
|
All v0.1.0 through v0.4.2 conformance cases are project-authored. See
|
||||||
`tests/fixtures/README.md`. Runtime pack metadata and adjacent licence/notice
|
`tests/fixtures/README.md`. Runtime pack metadata and adjacent licence/notice
|
||||||
files are authoritative for the exact binary distribution.
|
files are authoritative for the exact binary distribution.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ on publication failure, replaces an existing exact version only with
|
|||||||
`--force`, and produces:
|
`--force`, and produces:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
release/regex-tools-0.4.0.zip
|
release/regex-tools-0.4.2.zip
|
||||||
release/regex-tools-0.4.0.zip.sha256
|
release/regex-tools-0.4.2.zip.sha256
|
||||||
```
|
```
|
||||||
|
|
||||||
The release scan checks ASCII/UTF-8, UTF-16LE and UTF-16BE content in text,
|
The release scan checks ASCII/UTF-8, UTF-16LE and UTF-16BE content in text,
|
||||||
@@ -43,6 +43,5 @@ anchored base-runtime legal files. The C++ gate requires its exact traced
|
|||||||
component/source manifest and six independently pinned legal files. The release
|
component/source manifest and six independently pinned legal files. The release
|
||||||
packager names every one of those files explicitly.
|
packager names every one of those files explicitly.
|
||||||
|
|
||||||
The existing v0.1.0, v0.2.0 and v0.3.0 ZIPs and matching sidecars are
|
The existing v0.1.0 through v0.4.1 ZIPs and matching sidecars are historical
|
||||||
historical immutable artifacts. Creating v0.4.0 must not replace, rename or
|
immutable artifacts. Creating v0.4.2 must not replace, rename or remove them.
|
||||||
remove them.
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module git.add-ideas.de/zemion/regex-tools/engines/go
|
module git.add-ideas.de/lotobo/regex-tools/engines/go
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
|
|||||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"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.3",
|
||||||
"@add-ideas/toolbox-shell-react": "0.2.2",
|
"@add-ideas/toolbox-shell-react": "0.2.3",
|
||||||
"@codemirror/commands": "6.10.4",
|
"@codemirror/commands": "6.10.4",
|
||||||
"@codemirror/language": "6.12.4",
|
"@codemirror/language": "6.12.4",
|
||||||
"@codemirror/search": "6.7.1",
|
"@codemirror/search": "6.7.1",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"react-dom": "19.2.7"
|
"react-dom": "19.2.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@add-ideas/toolbox-testkit": "0.2.2",
|
"@add-ideas/toolbox-testkit": "0.2.3",
|
||||||
"@bjorn3/browser_wasi_shim": "0.4.2",
|
"@bjorn3/browser_wasi_shim": "0.4.2",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@php-wasm/universal": "3.1.46",
|
"@php-wasm/universal": "3.1.46",
|
||||||
@@ -55,18 +55,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-contract": {
|
"node_modules/@add-ideas/toolbox-contract": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.2/toolbox-contract-0.2.2.tgz",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.3/toolbox-contract-0.2.3.tgz",
|
||||||
"integrity": "sha512-VcZ8j4O2PgFaIYgxs6r9u0uPxA+hHKwhCW+omfeVCtbjAAYhH2KhRlBGm0ERVVYcK5kchyzZLXrC4LKWRotVzg==",
|
"integrity": "sha512-T0PVSuMT40GjTDfQJhEEY3ZawQq8zz1/ry95JdKI6W39CdLacaRXdGnEpDCMHt+jUbf1Jz7Nat/M5dFCgKVM9A==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-shell-react": {
|
"node_modules/@add-ideas/toolbox-shell-react": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.2/toolbox-shell-react-0.2.2.tgz",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.3/toolbox-shell-react-0.2.3.tgz",
|
||||||
"integrity": "sha512-w/xbLCd50a2Jq7vQ9Z9ygUOuqlOCRkIYlk4vSJx0mf4REJNeMYi3PE2MbaLUC4DkQWyrmatsa5HNT89o0l91BA==",
|
"integrity": "sha512-DT5lQDH48BFkFcmFLZnQh7+Cm73JzBPcmp5WzUXypfkUXpEyDYHzaXgmW4kZ0edSwh4RK4sPmx+JPtK0X4aKCQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.2"
|
"@add-ideas/toolbox-contract": "0.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18 <20",
|
"react": ">=18 <20",
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-testkit": {
|
"node_modules/@add-ideas/toolbox-testkit": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.2/toolbox-testkit-0.2.2.tgz",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.3/toolbox-testkit-0.2.3.tgz",
|
||||||
"integrity": "sha512-ZxrRYiJI/j5aXwcd3mQItgDl1iOJ6N26+MpIG18qVZNwKKFCTCfSTzgzRooPt8rRc1MQYTPo4c+l/QpOWuTXxA==",
|
"integrity": "sha512-sq1MwhKWfFKen+N+124hl74qQimRSvmQ9sOU7jdcI+2qCKZ67+2B8rWyezeV80uTFu4Jv6deHksfYQ/tKNV6XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.2"
|
"@add-ideas/toolbox-contract": "0.2.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"toolbox-check": "dist/cli.js"
|
"toolbox-check": "dist/cli.js"
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"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",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://git.add-ideas.de/zemion/regex-tools.git"
|
"url": "git+https://git.add-ideas.de/lotobo/regex-tools.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://git.add-ideas.de/zemion/regex-tools",
|
"homepage": "https://git.add-ideas.de/lotobo/regex-tools",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.add-ideas.de/zemion/regex-tools/issues"
|
"url": "https://git.add-ideas.de/lotobo/regex-tools/issues"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
"release:artifact": "npm run check && npm run test:browser && npm run package:release"
|
"release:artifact": "npm run check && npm run test:browser && npm run package:release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.2",
|
"@add-ideas/toolbox-contract": "0.2.3",
|
||||||
"@add-ideas/toolbox-shell-react": "0.2.2",
|
"@add-ideas/toolbox-shell-react": "0.2.3",
|
||||||
"@codemirror/commands": "6.10.4",
|
"@codemirror/commands": "6.10.4",
|
||||||
"@codemirror/language": "6.12.4",
|
"@codemirror/language": "6.12.4",
|
||||||
"@codemirror/search": "6.7.1",
|
"@codemirror/search": "6.7.1",
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
"react-dom": "19.2.7"
|
"react-dom": "19.2.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@add-ideas/toolbox-testkit": "0.2.2",
|
"@add-ideas/toolbox-testkit": "0.2.3",
|
||||||
"@bjorn3/browser_wasi_shim": "0.4.2",
|
"@bjorn3/browser_wasi_shim": "0.4.2",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@php-wasm/universal": "3.1.46",
|
"@php-wasm/universal": "3.1.46",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
911f8f5782931320f5b8d1160a76365b83aea6447ee6c04fa6d5591467db9dad LICENSE-Go.txt
|
911f8f5782931320f5b8d1160a76365b83aea6447ee6c04fa6d5591467db9dad LICENSE-Go.txt
|
||||||
208034ac6bb2a1867fa082c51edd5e77098f16ff55e7907f9c9a51b59e6390ff engine-metadata.json
|
38a8e5518e670ca40efce95753d6836ed4686c5d25bb049df2c97e418ec76636 engine-metadata.json
|
||||||
ddafd6401e0674c048ee066677157db0b03480f716262845ea30639daffb3ae5 go-regex.wasm
|
d2faa632cc35d4d63040f9a8d7ad15e1bd5866a4d69c72a7bbefadf7fadcb676 go-regex.wasm
|
||||||
0c949f4996f9a89698e4b5c586de32249c3b69b7baadb64d220073cc04acba14 wasm_exec.mjs
|
0c949f4996f9a89698e4b5c586de32249c3b69b7baadb64d220073cc04acba14 wasm_exec.mjs
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "engines/go/go.mod",
|
"path": "engines/go/go.mod",
|
||||||
"sha256": "b2744a26c11ce6be55fabd2109b89f34b0db413abab8b40c9f7e8ea83e8b7bff",
|
"sha256": "1d84837beaa0d734f26a7e87ed83f5a8ffbc4e9d049260636ef127ec163c5f58",
|
||||||
"bytes": 85
|
"bytes": 85
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "go-regex.wasm",
|
"path": "go-regex.wasm",
|
||||||
"sha256": "ddafd6401e0674c048ee066677157db0b03480f716262845ea30639daffb3ae5",
|
"sha256": "d2faa632cc35d4d63040f9a8d7ad15e1bd5866a4d69c72a7bbefadf7fadcb676",
|
||||||
"bytes": 3904895
|
"bytes": 3904895
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,26 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title">
|
||||||
<title id="title">Regex Tools</title>
|
<title id="title">Regex Tools</title>
|
||||||
<rect width="64" height="64" rx="15" fill="#5532a8"/>
|
<metadata>
|
||||||
<path d="M15 22h20M15 32h12M15 42h20" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round"/>
|
The regular-expression symbol is adapted from the OOjs UI
|
||||||
<path d="m46 20-3 24m-7-8 20-8" fill="none" stroke="#80e1cb" stroke-width="5" stroke-linecap="round"/>
|
regular-expression-progressive icon, Copyright 2011–2017 OOjs UI Team and
|
||||||
|
other contributors, MIT licensed. The surrounding application icon is
|
||||||
|
Copyright 2026 Albrecht Degering, GPL-3.0-or-later.
|
||||||
|
</metadata>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="regex-bg" x1="8" y1="5" x2="56" y2="59" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#6d5cff"/>
|
||||||
|
<stop offset="1" stop-color="#18224d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="regex-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="1.25" flood-color="#060b24" flood-opacity="0.32"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="64" height="64" rx="14" fill="url(#regex-bg)"/>
|
||||||
|
<g filter="url(#regex-shadow)">
|
||||||
|
<path d="M18 9h22l9 9v33a4 4 0 0 1-4 4H18a4 4 0 0 1-4-4V13a4 4 0 0 1 4-4Z" fill="#fff"/>
|
||||||
|
<path d="M40 9v9h9Z" fill="#d9ddff"/>
|
||||||
|
<g transform="translate(20 25) scale(1.2)" fill="#36c">
|
||||||
|
<path d="M1.62 10a13.63 13.63 0 0 0 .45 3.51A13.39 13.39 0 0 0 3.4 16.7a.91.91 0 0 1 .1.27.41.41 0 0 1 0 .21.38.38 0 0 1-.1.15l-.14.11-.83.5a14.89 14.89 0 0 1-1.11-2 13.62 13.62 0 0 1-.74-2 13.22 13.22 0 0 1-.42-2 16.4 16.4 0 0 1 0-4.14 13.22 13.22 0 0 1 .42-2 13.84 13.84 0 0 1 .74-2A14.94 14.94 0 0 1 2.4 2l.83.51.14.11a.4.4 0 0 1 .1.15.41.41 0 0 1 0 .21.93.93 0 0 1-.1.27A13.6 13.6 0 0 0 1.62 10ZM5 13.51a1.53 1.53 0 0 1 .11-.59 1.5 1.5 0 0 1 .31-.48 1.5 1.5 0 0 1 1.65-.32 1.51 1.51 0 0 1 .8.8 1.47 1.47 0 0 1 .12.59 1.46 1.46 0 0 1-.12.59 1.56 1.56 0 0 1-.32.48 1.46 1.46 0 0 1-.48.32 1.57 1.57 0 0 1-1.18 0 1.4 1.4 0 0 1-.47-.32A1.5 1.5 0 0 1 5 13.51Zm10.8-4.72-.54.94-1.75-1-.34-.23a1.38 1.38 0 0 1-.27-.26A1.84 1.84 0 0 1 13 9v2h-1V9a2.16 2.16 0 0 1 .12-.76 1.82 1.82 0 0 1-.58.48l-1.74 1-.54-.94 1.73-1a2.25 2.25 0 0 1 .75-.29 1.77 1.77 0 0 1-.75-.28L9.2 6.2l.54-.94 1.75 1 .33.24a1.64 1.64 0 0 1 .27.27A2 2 0 0 1 12 6V4h1v2a2.93 2.93 0 0 1 0 .4 1.36 1.36 0 0 1-.1.36 2.24 2.24 0 0 1 .59-.49l1.74-1 .54.94-1.73 1-.36.18a1.29 1.29 0 0 1-.36.1 2.11 2.11 0 0 1 .36.1 2 2 0 0 1 .36.19ZM18.37 10a13.65 13.65 0 0 0-.45-3.51 13.81 13.81 0 0 0-1.32-3.27.93.93 0 0 1-.1-.27.45.45 0 0 1 0-.21.36.36 0 0 1 .1-.15l.14-.11.86-.48a15.54 15.54 0 0 1 1.1 2 13.79 13.79 0 0 1 .74 2 13.18 13.18 0 0 1 .42 2 16.16 16.16 0 0 1 .14 2 16.21 16.21 0 0 1-.13 2 13.18 13.18 0 0 1-.42 2 13.57 13.57 0 0 1-.74 2 15.49 15.49 0 0 1-1.1 2l-.84-.5-.14-.11a.35.35 0 0 1-.1-.15.44.44 0 0 1 0-.21.91.91 0 0 1 .1-.27 13.62 13.62 0 0 0 1.31-3.23 13.69 13.69 0 0 0 .43-3.53Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 2.7 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
|
"$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.regex-tools",
|
"id": "de.add-ideas.regex-tools",
|
||||||
"name": "Regex Tools",
|
"name": "Regex Tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"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",
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
"telemetry": false
|
"telemetry": false
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"repository": "https://git.add-ideas.de/zemion/regex-tools",
|
"repository": "https://git.add-ideas.de/lotobo/regex-tools",
|
||||||
"license": "GPL-3.0-or-later"
|
"license": "GPL-3.0-or-later"
|
||||||
},
|
},
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"id": "source",
|
"id": "source",
|
||||||
"label": "Source",
|
"label": "Source",
|
||||||
"url": "https://git.add-ideas.de/zemion/regex-tools"
|
"url": "https://git.add-ideas.de/lotobo/regex-tools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
source.source?.repository !== "https://git.add-ideas.de/zemion/regex-tools" ||
|
source.source?.repository !== "https://git.add-ideas.de/lotobo/regex-tools" ||
|
||||||
source.source?.license !== "GPL-3.0-or-later"
|
source.source?.license !== "GPL-3.0-or-later"
|
||||||
) {
|
) {
|
||||||
throw new Error("Manifest source identity is incomplete or inconsistent");
|
throw new Error("Manifest source identity is incomplete or inconsistent");
|
||||||
|
|||||||
@@ -19,7 +19,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.4.0";
|
const expectedApplicationVersion = "0.4.2";
|
||||||
const requiredRootFiles = [
|
const requiredRootFiles = [
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
@@ -341,7 +341,7 @@ async function collectReleaseEntries() {
|
|||||||
packageJson.version !== expectedApplicationVersion ||
|
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/lotobo/regex-tools.git"
|
||||||
) {
|
) {
|
||||||
throw new Error("Package version, licence or source identity drifted.");
|
throw new Error("Package version, licence or source identity drifted.");
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ async function collectReleaseEntries() {
|
|||||||
`- Release tag: \`v${expectedApplicationVersion}\``,
|
`- Release tag: \`v${expectedApplicationVersion}\``,
|
||||||
) ||
|
) ||
|
||||||
!sourceIdentity.includes(
|
!sourceIdentity.includes(
|
||||||
"- Repository: <https://git.add-ideas.de/zemion/regex-tools>",
|
"- Repository: <https://git.add-ideas.de/lotobo/regex-tools>",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error("SOURCE.md release identity drifted.");
|
throw new Error("SOURCE.md release identity drifted.");
|
||||||
@@ -565,7 +565,7 @@ async function collectReleaseEntries() {
|
|||||||
manifest.id !== "de.add-ideas.regex-tools" ||
|
manifest.id !== "de.add-ideas.regex-tools" ||
|
||||||
manifest.version !== packageJson.version ||
|
manifest.version !== packageJson.version ||
|
||||||
manifest.source?.repository !==
|
manifest.source?.repository !==
|
||||||
"https://git.add-ideas.de/zemion/regex-tools" ||
|
"https://git.add-ideas.de/lotobo/regex-tools" ||
|
||||||
manifest.source?.license !== "GPL-3.0-or-later"
|
manifest.source?.license !== "GPL-3.0-or-later"
|
||||||
) {
|
) {
|
||||||
throw new Error("Packaged manifest identity is inconsistent.");
|
throw new Error("Packaged manifest identity is inconsistent.");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface ExplanationTreeProps {
|
|||||||
readonly root?: NormalizedRegexNode;
|
readonly root?: NormalizedRegexNode;
|
||||||
readonly selectedId?: string;
|
readonly selectedId?: string;
|
||||||
readonly flags?: readonly string[];
|
readonly flags?: readonly string[];
|
||||||
|
readonly stale?: boolean;
|
||||||
readonly onSelect: (node: NormalizedRegexNode) => void;
|
readonly onSelect: (node: NormalizedRegexNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +166,7 @@ export function ExplanationTree({
|
|||||||
root,
|
root,
|
||||||
selectedId,
|
selectedId,
|
||||||
flags = [],
|
flags = [],
|
||||||
|
stale = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: ExplanationTreeProps) {
|
}: ExplanationTreeProps) {
|
||||||
const totalNodes = useMemo(() => (root ? countNodes(root) : 0), [root]);
|
const totalNodes = useMemo(() => (root ? countNodes(root) : 0), [root]);
|
||||||
@@ -191,8 +193,18 @@ export function ExplanationTree({
|
|||||||
<p className="eyebrow">Pattern structure</p>
|
<p className="eyebrow">Pattern structure</p>
|
||||||
<h2 id="explanation-heading">Explanation tree</h2>
|
<h2 id="explanation-heading">Explanation tree</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className="provenance-badge">
|
<span
|
||||||
Deterministic · syntax provider
|
className={`provenance-badge ${stale ? "is-stale" : ""}`}
|
||||||
|
data-testid={stale ? "stale-pattern-syntax" : undefined}
|
||||||
|
title={
|
||||||
|
stale
|
||||||
|
? "This is the last parsed pattern. Its ranges are not applied to the current editor."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{stale
|
||||||
|
? "Stale snapshot · checking current pattern"
|
||||||
|
: "Deterministic · syntax provider"}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
{totalNodes > renderedNodes ? (
|
{totalNodes > renderedNodes ? (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "../regex/generation/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/lotobo/regex-tools/src/tag/v${manifest.version}`;
|
||||||
|
|
||||||
export function HelpDialog({
|
export function HelpDialog({
|
||||||
open,
|
open,
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ describe("ReplacementPanel completeness", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("labels retained tokens with the flavour that parsed the snapshot", () => {
|
||||||
|
render(
|
||||||
|
<ReplacementPanel
|
||||||
|
flavour="python"
|
||||||
|
presentedFlavour="java"
|
||||||
|
replacement="$1"
|
||||||
|
onReplacementChange={() => undefined}
|
||||||
|
onSelectToken={() => undefined}
|
||||||
|
onSelectContribution={() => undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Python re template syntax")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Java Pattern derived token explanation"),
|
||||||
|
).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 },
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ export function ReplacementPanel({
|
|||||||
replacement,
|
replacement,
|
||||||
onReplacementChange,
|
onReplacementChange,
|
||||||
syntax,
|
syntax,
|
||||||
|
presentedSyntax,
|
||||||
|
syntaxStale = false,
|
||||||
|
presentedFlavour,
|
||||||
result,
|
result,
|
||||||
|
resultFlavour,
|
||||||
preview,
|
preview,
|
||||||
selectedToken,
|
selectedToken,
|
||||||
selectedContributionId,
|
selectedContributionId,
|
||||||
@@ -83,7 +87,11 @@ export function ReplacementPanel({
|
|||||||
readonly replacement: string;
|
readonly replacement: string;
|
||||||
readonly onReplacementChange: (value: string) => void;
|
readonly onReplacementChange: (value: string) => void;
|
||||||
readonly syntax?: ReplacementSyntaxResult;
|
readonly syntax?: ReplacementSyntaxResult;
|
||||||
|
readonly presentedSyntax?: ReplacementSyntaxResult;
|
||||||
|
readonly syntaxStale?: boolean;
|
||||||
|
readonly presentedFlavour?: RegexFlavourId;
|
||||||
readonly result?: RegexReplacementResult;
|
readonly result?: RegexReplacementResult;
|
||||||
|
readonly resultFlavour?: RegexFlavourId;
|
||||||
readonly preview?: ReplacementPreview;
|
readonly preview?: ReplacementPreview;
|
||||||
readonly selectedToken?: ReplacementToken;
|
readonly selectedToken?: ReplacementToken;
|
||||||
readonly selectedContributionId?: string;
|
readonly selectedContributionId?: string;
|
||||||
@@ -96,8 +104,16 @@ export function ReplacementPanel({
|
|||||||
const activeFlavour =
|
const activeFlavour =
|
||||||
flavour ?? result?.execution.engine.flavour ?? "ecmascript";
|
flavour ?? result?.execution.engine.flavour ?? "ecmascript";
|
||||||
const engineLabel = TEMPLATE_FLAVOUR_LABELS[activeFlavour];
|
const engineLabel = TEMPLATE_FLAVOUR_LABELS[activeFlavour];
|
||||||
const retainedTokens = syntax?.tokens ?? [];
|
const presentedEngineLabel =
|
||||||
const editorTokens = retainedTokens.slice(
|
TEMPLATE_FLAVOUR_LABELS[presentedFlavour ?? activeFlavour];
|
||||||
|
const resultEngineLabel =
|
||||||
|
TEMPLATE_FLAVOUR_LABELS[
|
||||||
|
resultFlavour ?? result?.execution.engine.flavour ?? activeFlavour
|
||||||
|
];
|
||||||
|
const retainedSyntax = presentedSyntax ?? syntax;
|
||||||
|
const retainedTokens = retainedSyntax?.tokens ?? [];
|
||||||
|
const currentTokens = syntax?.tokens ?? [];
|
||||||
|
const editorTokens = currentTokens.slice(
|
||||||
0,
|
0,
|
||||||
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks,
|
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks,
|
||||||
);
|
);
|
||||||
@@ -129,7 +145,7 @@ export function ReplacementPanel({
|
|||||||
maximumLengthUtf16={
|
maximumLengthUtf16={
|
||||||
DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
|
DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
|
||||||
}
|
}
|
||||||
selectedRange={selectedToken?.range}
|
selectedRange={syntaxStale ? undefined : selectedToken?.range}
|
||||||
marks={[
|
marks={[
|
||||||
...editorTokens.map((token) => ({
|
...editorTokens.map((token) => ({
|
||||||
id: token.id,
|
id: token.id,
|
||||||
@@ -137,7 +153,7 @@ export function ReplacementPanel({
|
|||||||
className: `cm-replacement-token cm-replacement-${token.kind}`,
|
className: `cm-replacement-token cm-replacement-${token.kind}`,
|
||||||
label: token.explanation,
|
label: token.explanation,
|
||||||
})),
|
})),
|
||||||
...(selectedToken
|
...(!syntaxStale && selectedToken
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: "selected-replacement-token",
|
id: "selected-replacement-token",
|
||||||
@@ -163,7 +179,7 @@ export function ReplacementPanel({
|
|||||||
unit limit.
|
unit limit.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{retainedTokens.length >
|
{currentTokens.length >
|
||||||
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks ? (
|
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks ? (
|
||||||
<p
|
<p
|
||||||
className="render-limit-banner"
|
className="render-limit-banner"
|
||||||
@@ -171,7 +187,7 @@ export function ReplacementPanel({
|
|||||||
>
|
>
|
||||||
<strong>Editor decorations limited.</strong> Showing the first{" "}
|
<strong>Editor decorations limited.</strong> Showing the first{" "}
|
||||||
{TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks.toLocaleString()}{" "}
|
{TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks.toLocaleString()}{" "}
|
||||||
of {retainedTokens.length.toLocaleString()} complete replacement
|
of {currentTokens.length.toLocaleString()} complete replacement
|
||||||
tokens.
|
tokens.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -182,9 +198,24 @@ export function ReplacementPanel({
|
|||||||
>
|
>
|
||||||
<header className="panel-heading">
|
<header className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Derived token explanation</p>
|
<p className="eyebrow">
|
||||||
|
{presentedEngineLabel} derived token explanation
|
||||||
|
</p>
|
||||||
<h2 id="replacement-tree-heading">Replacement tokens</h2>
|
<h2 id="replacement-tree-heading">Replacement tokens</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
className={`provenance-badge ${syntaxStale ? "is-stale" : ""}`}
|
||||||
|
data-testid={syntaxStale ? "stale-replacement-syntax" : undefined}
|
||||||
|
title={
|
||||||
|
syntaxStale
|
||||||
|
? "These are the last parsed template tokens. Their ranges are not applied to the current editor."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{syntaxStale
|
||||||
|
? "Stale snapshot · checking current template"
|
||||||
|
: "Current template"}
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
{renderedTokens.length > 0 ? (
|
{renderedTokens.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -222,12 +253,12 @@ export function ReplacementPanel({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="empty-state">
|
<p className="empty-state">
|
||||||
{syntax?.accepted === false
|
{retainedSyntax?.accepted === false
|
||||||
? "Token parsing is unavailable until the template is within the input limit."
|
? "Token parsing is unavailable until the template is within the input limit."
|
||||||
: "Enter a replacement template."}
|
: "Enter a replacement template."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{syntax?.diagnosticsTruncated ? (
|
{retainedSyntax?.diagnosticsTruncated ? (
|
||||||
<p
|
<p
|
||||||
className="render-limit-banner"
|
className="render-limit-banner"
|
||||||
data-testid="replacement-diagnostic-render-limit"
|
data-testid="replacement-diagnostic-render-limit"
|
||||||
@@ -237,7 +268,8 @@ export function ReplacementPanel({
|
|||||||
{TEMPLATE_PRESENTATION_LIMITS.replacementDiagnostics.toLocaleString()}{" "}
|
{TEMPLATE_PRESENTATION_LIMITS.replacementDiagnostics.toLocaleString()}{" "}
|
||||||
entries, including a summary, for{" "}
|
entries, including a summary, for{" "}
|
||||||
{(
|
{(
|
||||||
syntax.totalDiagnostics ?? syntax.diagnostics.length
|
retainedSyntax.totalDiagnostics ??
|
||||||
|
retainedSyntax.diagnostics.length
|
||||||
).toLocaleString()}{" "}
|
).toLocaleString()}{" "}
|
||||||
findings.
|
findings.
|
||||||
</p>
|
</p>
|
||||||
@@ -252,7 +284,7 @@ export function ReplacementPanel({
|
|||||||
<header className="panel-heading">
|
<header className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">
|
<p className="eyebrow">
|
||||||
Native matches · bounded {engineLabel} substitution
|
Native matches · bounded {resultEngineLabel} substitution
|
||||||
</p>
|
</p>
|
||||||
<h2>Replacement output</h2>
|
<h2>Replacement output</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const syntaxHarness = vi.hoisted(() => ({
|
|||||||
|
|
||||||
const engineHarness = vi.hoisted(() => ({
|
const engineHarness = vi.hoisted(() => ({
|
||||||
requests: [] as RegexExecutionRequest[],
|
requests: [] as RegexExecutionRequest[],
|
||||||
|
emitWholeFirstCharacterMatch: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../editors/CodeEditor", () => ({
|
vi.mock("../editors/CodeEditor", () => ({
|
||||||
@@ -45,18 +46,35 @@ vi.mock("../editors/CodeEditor", () => ({
|
|||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
marks,
|
||||||
|
selectedRange,
|
||||||
|
onSelectionChange,
|
||||||
}: {
|
}: {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
readonly onChange?: (value: string) => void;
|
readonly onChange?: (value: string) => void;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly readOnly?: boolean;
|
readonly readOnly?: boolean;
|
||||||
|
readonly marks?: readonly unknown[];
|
||||||
|
readonly selectedRange?: { readonly startUtf16: number };
|
||||||
|
readonly onSelectionChange?: (range: {
|
||||||
|
readonly startUtf16: number;
|
||||||
|
readonly endUtf16: number;
|
||||||
|
}) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
value={value}
|
value={value}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
data-mark-count={marks?.length ?? 0}
|
||||||
|
data-selected-start={selectedRange?.startUtf16}
|
||||||
onChange={(event) => onChange?.(event.target.value)}
|
onChange={(event) => onChange?.(event.target.value)}
|
||||||
|
onSelect={(event) =>
|
||||||
|
onSelectionChange?.({
|
||||||
|
startUtf16: event.currentTarget.selectionStart,
|
||||||
|
endUtf16: event.currentTarget.selectionEnd,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -115,7 +133,18 @@ vi.mock("../regex/execution/EngineSupervisor", () => ({
|
|||||||
internallyAddedIndicesFlag: false,
|
internallyAddedIndicesFlag: false,
|
||||||
internallyAddedGlobalFlag: false,
|
internallyAddedGlobalFlag: false,
|
||||||
},
|
},
|
||||||
matches: [],
|
matches: engineHarness.emitWholeFirstCharacterMatch
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
matchNumber: 1,
|
||||||
|
value: request.subject.slice(0, 1),
|
||||||
|
valueStatus: "complete" as const,
|
||||||
|
range: { startUtf16: 0, endUtf16: 1 },
|
||||||
|
nativeRange: { start: 0, end: 1, unit: "utf16" as const },
|
||||||
|
captures: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
elapsedMs: 1,
|
elapsedMs: 1,
|
||||||
truncated: false,
|
truncated: false,
|
||||||
@@ -231,6 +260,7 @@ afterEach(() => {
|
|||||||
syntaxHarness.patterns.length = 0;
|
syntaxHarness.patterns.length = 0;
|
||||||
syntaxHarness.replacements.length = 0;
|
syntaxHarness.replacements.length = 0;
|
||||||
engineHarness.requests.length = 0;
|
engineHarness.requests.length = 0;
|
||||||
|
engineHarness.emitWholeFirstCharacterMatch = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Workbench syntax coordination", () => {
|
describe("Workbench syntax coordination", () => {
|
||||||
@@ -346,6 +376,79 @@ describe("Workbench syntax coordination", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not revive an old syntax snapshot when rapid edits return to the same text", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
render(<Workbench />);
|
||||||
|
fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));
|
||||||
|
|
||||||
|
await advanceParseDebounce();
|
||||||
|
await resolvePattern(0, []);
|
||||||
|
const patternInput = screen.getByRole("textbox", {
|
||||||
|
name: "Regular expression pattern",
|
||||||
|
});
|
||||||
|
const initialPattern = (patternInput as HTMLTextAreaElement).value;
|
||||||
|
const runButton = screen.getByRole("button", { name: "Run" });
|
||||||
|
expect(runButton).toBeEnabled();
|
||||||
|
|
||||||
|
fireEvent.change(patternInput, { target: { value: "temporary" } });
|
||||||
|
fireEvent.change(patternInput, { target: { value: initialPattern } });
|
||||||
|
|
||||||
|
expect(runButton).toBeDisabled();
|
||||||
|
fireEvent.click(screen.getByRole("treeitem"));
|
||||||
|
expect(patternInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
await advanceParseDebounce();
|
||||||
|
await resolvePattern(1, []);
|
||||||
|
expect(patternInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
expect(runButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains one exact result frame until the edited subject has a replacement frame", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
engineHarness.emitWholeFirstCharacterMatch = true;
|
||||||
|
render(<Workbench />);
|
||||||
|
fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));
|
||||||
|
|
||||||
|
await advanceParseDebounce();
|
||||||
|
await resolvePattern(0, []);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Run" }));
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const subjectInput = screen.getByRole("textbox", { name: "Test text" });
|
||||||
|
const originalSubject = (subjectInput as HTMLTextAreaElement).value;
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "1");
|
||||||
|
expect(document.querySelector(".extraction-row")).toHaveTextContent("2");
|
||||||
|
|
||||||
|
fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
|
||||||
|
|
||||||
|
expect(screen.getByTestId("stale-result-frame")).toHaveTextContent(
|
||||||
|
"bound to the last completed inputs",
|
||||||
|
);
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
expect(document.querySelector(".extraction-row")).toHaveTextContent("2");
|
||||||
|
|
||||||
|
fireEvent.change(subjectInput, { target: { value: originalSubject } });
|
||||||
|
expect(screen.getByTestId("stale-result-frame")).toBeInTheDocument();
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
|
||||||
|
fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
|
||||||
|
(subjectInput as HTMLTextAreaElement).setSelectionRange(0, 1);
|
||||||
|
fireEvent.select(subjectInput);
|
||||||
|
expect(
|
||||||
|
document.querySelector(".extraction-row.is-selected"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Run" }));
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("stale-result-frame")).not.toBeInTheDocument();
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "1");
|
||||||
|
expect(document.querySelector(".extraction-row")).toHaveTextContent("Z");
|
||||||
|
});
|
||||||
|
|
||||||
it("captures a complete result and reruns only the selected saved test", async () => {
|
it("captures a complete result and reruns only the selected saved test", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
render(<Workbench />);
|
render(<Workbench />);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from "../regex/model/match";
|
} from "../regex/model/match";
|
||||||
import type {
|
import type {
|
||||||
NormalizedRegexNode,
|
NormalizedRegexNode,
|
||||||
|
RegexSyntaxRequest,
|
||||||
RegexSyntaxResult,
|
RegexSyntaxResult,
|
||||||
ReplacementSyntaxResult,
|
ReplacementSyntaxResult,
|
||||||
ReplacementToken,
|
ReplacementToken,
|
||||||
@@ -105,6 +106,79 @@ type RunState =
|
|||||||
| { readonly status: "timeout"; readonly message: string }
|
| { readonly status: "timeout"; readonly message: string }
|
||||||
| { readonly status: "error"; readonly message: string };
|
| { readonly status: "error"; readonly message: string };
|
||||||
|
|
||||||
|
interface ReplacementSyntaxSnapshot {
|
||||||
|
readonly revision: number;
|
||||||
|
readonly flavour: RegexFlavourId;
|
||||||
|
readonly replacement: string;
|
||||||
|
readonly patternSyntax: RegexSyntaxResult;
|
||||||
|
readonly result: ReplacementSyntaxResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultFrame {
|
||||||
|
readonly revision: number;
|
||||||
|
readonly flavour: RegexFlavourId;
|
||||||
|
readonly flavourVersion: string;
|
||||||
|
readonly options: RegexEngineOptions;
|
||||||
|
readonly pattern: string;
|
||||||
|
readonly flags: readonly string[];
|
||||||
|
readonly subject: string;
|
||||||
|
readonly replacement: string;
|
||||||
|
readonly mode: WorkspaceMode;
|
||||||
|
readonly scanAll: boolean;
|
||||||
|
readonly syntaxRequest: RegexSyntaxRequest;
|
||||||
|
readonly syntax: RegexSyntaxResult;
|
||||||
|
readonly replacementSyntax?: ReplacementSyntaxResult;
|
||||||
|
readonly execution: RegexExecutionResult;
|
||||||
|
readonly replacementResult?: RegexReplacementResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineOptionsEqual(
|
||||||
|
left: RegexEngineOptions,
|
||||||
|
right: RegexEngineOptions,
|
||||||
|
): boolean {
|
||||||
|
const entries = Object.entries(left);
|
||||||
|
return (
|
||||||
|
entries.length === Object.keys(right).length &&
|
||||||
|
entries.every(([name, value]) => right[name] === value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultFrameMatchesInput(
|
||||||
|
frame: ResultFrame | undefined,
|
||||||
|
revision: number,
|
||||||
|
input: {
|
||||||
|
readonly flavour: RegexFlavourId;
|
||||||
|
readonly flavourVersion: string;
|
||||||
|
readonly options: RegexEngineOptions;
|
||||||
|
readonly pattern: string;
|
||||||
|
readonly flags: readonly string[];
|
||||||
|
readonly subject: string;
|
||||||
|
readonly replacement: string;
|
||||||
|
readonly mode: WorkspaceMode;
|
||||||
|
readonly scanAll: boolean;
|
||||||
|
readonly syntax: RegexSyntaxResult | undefined;
|
||||||
|
readonly replacementSyntax: ReplacementSyntaxResult | undefined;
|
||||||
|
},
|
||||||
|
): frame is ResultFrame {
|
||||||
|
return (
|
||||||
|
frame !== undefined &&
|
||||||
|
frame.revision === revision &&
|
||||||
|
frame.flavour === input.flavour &&
|
||||||
|
frame.flavourVersion === input.flavourVersion &&
|
||||||
|
frame.pattern === input.pattern &&
|
||||||
|
frame.subject === input.subject &&
|
||||||
|
frame.replacement === input.replacement &&
|
||||||
|
frame.mode === input.mode &&
|
||||||
|
frame.scanAll === input.scanAll &&
|
||||||
|
frame.syntax === input.syntax &&
|
||||||
|
frame.flags.length === input.flags.length &&
|
||||||
|
frame.flags.every((flag, index) => flag === input.flags[index]) &&
|
||||||
|
engineOptionsEqual(frame.options, input.options) &&
|
||||||
|
(frame.mode !== "replace" ||
|
||||||
|
frame.replacementSyntax === input.replacementSyntax)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function findSmallestNode(
|
function findSmallestNode(
|
||||||
node: NormalizedRegexNode,
|
node: NormalizedRegexNode,
|
||||||
range: SourceRange,
|
range: SourceRange,
|
||||||
@@ -401,11 +475,9 @@ export function Workbench() {
|
|||||||
);
|
);
|
||||||
const [syntaxSnapshot, setSyntaxSnapshot] =
|
const [syntaxSnapshot, setSyntaxSnapshot] =
|
||||||
useState<PatternSyntaxSnapshot<RegexSyntaxResult>>();
|
useState<PatternSyntaxSnapshot<RegexSyntaxResult>>();
|
||||||
const [replacementSyntax, setReplacementSyntax] =
|
const [replacementSyntaxSnapshot, setReplacementSyntaxSnapshot] =
|
||||||
useState<ReplacementSyntaxResult>();
|
useState<ReplacementSyntaxSnapshot>();
|
||||||
const [execution, setExecution] = useState<RegexExecutionResult>();
|
const [resultFrame, setResultFrame] = useState<ResultFrame>();
|
||||||
const [replacementResult, setReplacementResult] =
|
|
||||||
useState<RegexReplacementResult>();
|
|
||||||
const [selectedSyntax, setSelectedSyntax] = useState<NormalizedRegexNode>();
|
const [selectedSyntax, setSelectedSyntax] = useState<NormalizedRegexNode>();
|
||||||
const [requestedPatternRange, setRequestedPatternRange] =
|
const [requestedPatternRange, setRequestedPatternRange] =
|
||||||
useState<SourceRange>();
|
useState<SourceRange>();
|
||||||
@@ -443,6 +515,10 @@ export function Workbench() {
|
|||||||
const [showSubjectWhitespace, setShowSubjectWhitespace] = useState(false);
|
const [showSubjectWhitespace, setShowSubjectWhitespace] = useState(false);
|
||||||
const [corpusSession, setCorpusSession] = useState(0);
|
const [corpusSession, setCorpusSession] = useState(0);
|
||||||
const [subjectCursor, setSubjectCursor] = useState({ line: 1, column: 1 });
|
const [subjectCursor, setSubjectCursor] = useState({ line: 1, column: 1 });
|
||||||
|
const [syntaxGeneration, setSyntaxGeneration] = useState(0);
|
||||||
|
const [replacementSyntaxGeneration, setReplacementSyntaxGeneration] =
|
||||||
|
useState(0);
|
||||||
|
const [executionGeneration, setExecutionGeneration] = useState(0);
|
||||||
const [projectIdentity, setProjectIdentity] = useState(() => ({
|
const [projectIdentity, setProjectIdentity] = useState(() => ({
|
||||||
id: projectId(),
|
id: projectId(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -490,9 +566,7 @@ export function Workbench() {
|
|||||||
return testEngineSupervisor.current;
|
return testEngineSupervisor.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearExecutionResults = useCallback(() => {
|
const clearResultSelections = useCallback(() => {
|
||||||
setExecution(undefined);
|
|
||||||
setReplacementResult(undefined);
|
|
||||||
setSelectedExtraction(undefined);
|
setSelectedExtraction(undefined);
|
||||||
setSelectedCaptureRow(undefined);
|
setSelectedCaptureRow(undefined);
|
||||||
setRequestedSubjectRange(undefined);
|
setRequestedSubjectRange(undefined);
|
||||||
@@ -502,22 +576,24 @@ export function Workbench() {
|
|||||||
|
|
||||||
const invalidateExecution = (preserveCurrentResult = false) => {
|
const invalidateExecution = (preserveCurrentResult = false) => {
|
||||||
executionRevision.current += 1;
|
executionRevision.current += 1;
|
||||||
|
setExecutionGeneration(executionRevision.current);
|
||||||
engineSupervisor.current?.cancel();
|
engineSupervisor.current?.cancel();
|
||||||
clearExecutionResults();
|
clearResultSelections();
|
||||||
if (!preserveCurrentResult) setCurrentResultDraft(undefined);
|
if (!preserveCurrentResult) setCurrentResultDraft(undefined);
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "idle",
|
status: "idle",
|
||||||
message: "Inputs changed; previous results were cleared.",
|
message:
|
||||||
|
"Inputs changed. The last completed result remains visible until the current inputs complete.",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidatePatternSyntax = () => {
|
const invalidatePatternSyntax = () => {
|
||||||
syntaxRevision.current += 1;
|
syntaxRevision.current += 1;
|
||||||
replacementSyntaxRevision.current += 1;
|
replacementSyntaxRevision.current += 1;
|
||||||
|
setSyntaxGeneration(syntaxRevision.current);
|
||||||
|
setReplacementSyntaxGeneration(replacementSyntaxRevision.current);
|
||||||
patternSyntaxSupervisor.current?.cancel();
|
patternSyntaxSupervisor.current?.cancel();
|
||||||
replacementSyntaxSupervisor.current?.cancel();
|
replacementSyntaxSupervisor.current?.cancel();
|
||||||
setSyntaxSnapshot(undefined);
|
|
||||||
setReplacementSyntax(undefined);
|
|
||||||
setSelectedSyntax(undefined);
|
setSelectedSyntax(undefined);
|
||||||
setSelectedReplacementToken(undefined);
|
setSelectedReplacementToken(undefined);
|
||||||
setSelectedReplacementContribution(undefined);
|
setSelectedReplacementContribution(undefined);
|
||||||
@@ -539,8 +615,8 @@ export function Workbench() {
|
|||||||
const changeReplacement = (value: string) => {
|
const changeReplacement = (value: string) => {
|
||||||
invalidateExecution();
|
invalidateExecution();
|
||||||
replacementSyntaxRevision.current += 1;
|
replacementSyntaxRevision.current += 1;
|
||||||
|
setReplacementSyntaxGeneration(replacementSyntaxRevision.current);
|
||||||
replacementSyntaxSupervisor.current?.cancel();
|
replacementSyntaxSupervisor.current?.cancel();
|
||||||
setReplacementSyntax(undefined);
|
|
||||||
setSelectedReplacementToken(undefined);
|
setSelectedReplacementToken(undefined);
|
||||||
setSelectedReplacementContribution(undefined);
|
setSelectedReplacementContribution(undefined);
|
||||||
setRequestedOutputRange(undefined);
|
setRequestedOutputRange(undefined);
|
||||||
@@ -582,16 +658,20 @@ export function Workbench() {
|
|||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
}, [flags, flavour, flavourVersion, options, pattern]);
|
}, [flags, flavour, flavourVersion, options, pattern]);
|
||||||
const syntax = isPatternSyntaxForInput(syntaxSnapshot, syntaxRequest)
|
const syntax =
|
||||||
? syntaxSnapshot.result
|
syntaxSnapshot?.revision === syntaxGeneration &&
|
||||||
: undefined;
|
isPatternSyntaxForInput(syntaxSnapshot, syntaxRequest)
|
||||||
|
? syntaxSnapshot.result
|
||||||
|
: undefined;
|
||||||
|
const presentedSyntax = syntax ?? syntaxSnapshot?.result;
|
||||||
|
const presentedSyntaxIsStale =
|
||||||
|
presentedSyntax !== undefined && syntax === undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRevision = syntaxRevision.current;
|
const currentRevision = syntaxRevision.current;
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
if (pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
|
if (pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
|
||||||
if (syntaxRevision.current !== currentRevision) return;
|
if (syntaxRevision.current !== currentRevision) return;
|
||||||
setSyntaxSnapshot(undefined);
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
|
message: `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
|
||||||
@@ -613,7 +693,6 @@ export function Workbench() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
if (!result.accepted) {
|
if (!result.accepted) {
|
||||||
clearExecutionResults();
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message:
|
message:
|
||||||
@@ -622,7 +701,6 @@ export function Workbench() {
|
|||||||
} else if (
|
} else if (
|
||||||
result.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
|
result.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
|
||||||
) {
|
) {
|
||||||
clearExecutionResults();
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: captureGroupLimitMessage(result.captures.length),
|
message: captureGroupLimitMessage(result.captures.length),
|
||||||
@@ -637,7 +715,6 @@ export function Workbench() {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearExecutionResults();
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : String(error),
|
||||||
@@ -645,20 +722,30 @@ export function Workbench() {
|
|||||||
});
|
});
|
||||||
}, DEFAULT_REGEX_LIMITS.liveParseDebounceMs);
|
}, DEFAULT_REGEX_LIMITS.liveParseDebounceMs);
|
||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [clearExecutionResults, pattern, syntaxRequest]);
|
}, [pattern, syntaxRequest]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!syntax) return;
|
if (!syntax) return;
|
||||||
const currentRevision = ++replacementSyntaxRevision.current;
|
const currentRevision = replacementSyntaxRevision.current;
|
||||||
|
const patternSyntax = syntax;
|
||||||
void replacementSyntaxEngine()
|
void replacementSyntaxEngine()
|
||||||
.parseReplacement({
|
.parseReplacement({
|
||||||
flavour,
|
flavour,
|
||||||
replacement,
|
replacement,
|
||||||
captureMetadata: syntax.captures,
|
captureMetadata: patternSyntax.captures,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (replacementSyntaxRevision.current !== currentRevision) return;
|
if (replacementSyntaxRevision.current !== currentRevision) return;
|
||||||
setReplacementSyntax(result);
|
setReplacementSyntaxSnapshot({
|
||||||
|
revision: currentRevision,
|
||||||
|
flavour,
|
||||||
|
replacement,
|
||||||
|
patternSyntax,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
setSelectedReplacementToken(undefined);
|
||||||
|
setSelectedReplacementContribution(undefined);
|
||||||
|
setRequestedOutputRange(undefined);
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (replacementSyntaxRevision.current !== currentRevision) return;
|
if (replacementSyntaxRevision.current !== currentRevision) return;
|
||||||
@@ -671,6 +758,22 @@ export function Workbench() {
|
|||||||
});
|
});
|
||||||
}, [flavour, replacement, syntax]);
|
}, [flavour, replacement, syntax]);
|
||||||
|
|
||||||
|
const replacementSyntax =
|
||||||
|
replacementSyntaxSnapshot?.revision === replacementSyntaxGeneration &&
|
||||||
|
replacementSyntaxSnapshot.flavour === flavour &&
|
||||||
|
replacementSyntaxSnapshot.replacement === replacement &&
|
||||||
|
replacementSyntaxSnapshot.patternSyntax === syntax
|
||||||
|
? replacementSyntaxSnapshot.result
|
||||||
|
: undefined;
|
||||||
|
const presentedReplacementSyntax =
|
||||||
|
replacementSyntax ?? replacementSyntaxSnapshot?.result;
|
||||||
|
const presentedReplacementSyntaxIsStale =
|
||||||
|
presentedReplacementSyntax !== undefined && replacementSyntax === undefined;
|
||||||
|
const presentedReplacementFlavour =
|
||||||
|
replacementSyntax !== undefined
|
||||||
|
? flavour
|
||||||
|
: replacementSyntaxSnapshot?.flavour;
|
||||||
|
|
||||||
const subjectBytes = useMemo(() => utf8ByteLength(subject), [subject]);
|
const subjectBytes = useMemo(() => utf8ByteLength(subject), [subject]);
|
||||||
|
|
||||||
const makeRequest = useCallback(
|
const makeRequest = useCallback(
|
||||||
@@ -704,7 +807,6 @@ export function Workbench() {
|
|||||||
window.clearTimeout(liveExecutionTimer.current);
|
window.clearTimeout(liveExecutionTimer.current);
|
||||||
liveExecutionTimer.current = undefined;
|
liveExecutionTimer.current = undefined;
|
||||||
}
|
}
|
||||||
clearExecutionResults();
|
|
||||||
setCurrentResultDraft(undefined);
|
setCurrentResultDraft(undefined);
|
||||||
if (
|
if (
|
||||||
!syntax?.accepted ||
|
!syntax?.accepted ||
|
||||||
@@ -744,6 +846,23 @@ export function Workbench() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const requestRevision = ++executionRevision.current;
|
const requestRevision = ++executionRevision.current;
|
||||||
|
setExecutionGeneration(requestRevision);
|
||||||
|
const currentSyntax = syntax;
|
||||||
|
const currentReplacementSyntax = replacementSyntax;
|
||||||
|
const frameIdentity = {
|
||||||
|
revision: requestRevision,
|
||||||
|
flavour,
|
||||||
|
flavourVersion,
|
||||||
|
options: { ...options },
|
||||||
|
pattern,
|
||||||
|
flags: [...flags],
|
||||||
|
subject,
|
||||||
|
replacement,
|
||||||
|
mode,
|
||||||
|
scanAll,
|
||||||
|
syntaxRequest,
|
||||||
|
syntax: currentSyntax,
|
||||||
|
} as const;
|
||||||
const limit =
|
const limit =
|
||||||
kind === "live"
|
kind === "live"
|
||||||
? DEFAULT_REGEX_LIMITS.liveExecutionTimeoutMs
|
? DEFAULT_REGEX_LIMITS.liveExecutionTimeoutMs
|
||||||
@@ -766,16 +885,13 @@ export function Workbench() {
|
|||||||
limit,
|
limit,
|
||||||
);
|
);
|
||||||
if (executionRevision.current !== requestRevision) return;
|
if (executionRevision.current !== requestRevision) return;
|
||||||
setExecution(result.execution);
|
|
||||||
if (!result.execution.accepted) {
|
if (!result.execution.accepted) {
|
||||||
setReplacementResult(undefined);
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: engineRejectionMessage(result.execution),
|
message: engineRejectionMessage(result.execution),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setReplacementResult(result);
|
|
||||||
const commonDraft = {
|
const commonDraft = {
|
||||||
name: "Current replacement result",
|
name: "Current replacement result",
|
||||||
pattern,
|
pattern,
|
||||||
@@ -825,11 +941,17 @@ export function Workbench() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
resultDraftNotice = decision.notice;
|
resultDraftNotice = decision.notice;
|
||||||
setCurrentResultDraft(resultDraft);
|
setCurrentResultDraft(resultDraft);
|
||||||
|
setResultFrame({
|
||||||
|
...frameIdentity,
|
||||||
|
replacementSyntax: currentReplacementSyntax,
|
||||||
|
execution: result.execution,
|
||||||
|
replacementResult: result,
|
||||||
|
});
|
||||||
|
clearResultSelections();
|
||||||
elapsedMs = result.execution.elapsedMs;
|
elapsedMs = result.execution.elapsedMs;
|
||||||
} else {
|
} else {
|
||||||
const result = await executionEngine().execute(makeRequest(), limit);
|
const result = await executionEngine().execute(makeRequest(), limit);
|
||||||
if (executionRevision.current !== requestRevision) return;
|
if (executionRevision.current !== requestRevision) return;
|
||||||
setExecution(result);
|
|
||||||
if (!result.accepted) {
|
if (!result.accepted) {
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
@@ -861,6 +983,11 @@ export function Workbench() {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
|
setResultFrame({
|
||||||
|
...frameIdentity,
|
||||||
|
execution: result,
|
||||||
|
});
|
||||||
|
clearResultSelections();
|
||||||
elapsedMs = result.elapsedMs;
|
elapsedMs = result.elapsedMs;
|
||||||
}
|
}
|
||||||
setRunState({
|
setRunState({
|
||||||
@@ -871,7 +998,6 @@ export function Workbench() {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (executionRevision.current !== requestRevision) return;
|
if (executionRevision.current !== requestRevision) return;
|
||||||
clearExecutionResults();
|
|
||||||
const timedOut =
|
const timedOut =
|
||||||
error instanceof WorkerRequestError && error.kind === "timeout";
|
error instanceof WorkerRequestError && error.kind === "timeout";
|
||||||
setRunState({
|
setRunState({
|
||||||
@@ -886,20 +1012,22 @@ export function Workbench() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeTestConfiguration,
|
activeTestConfiguration,
|
||||||
clearExecutionResults,
|
clearResultSelections,
|
||||||
|
flags,
|
||||||
|
flavour,
|
||||||
|
flavourVersion,
|
||||||
makeRequest,
|
makeRequest,
|
||||||
mode,
|
mode,
|
||||||
|
options,
|
||||||
|
pattern,
|
||||||
replacement,
|
replacement,
|
||||||
replacementSyntax?.accepted,
|
replacementSyntax,
|
||||||
scanAll,
|
scanAll,
|
||||||
subjectBytes,
|
|
||||||
subject,
|
subject,
|
||||||
syntax?.accepted,
|
subjectBytes,
|
||||||
syntax?.captures.length,
|
syntax,
|
||||||
syntaxSnapshot,
|
syntaxSnapshot,
|
||||||
syntaxRequest,
|
syntaxRequest,
|
||||||
flags,
|
|
||||||
pattern,
|
|
||||||
tests,
|
tests,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
],
|
],
|
||||||
@@ -943,22 +1071,49 @@ export function Workbench() {
|
|||||||
syntax?.accepted,
|
syntax?.accepted,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const resultFrameIsCurrent = resultFrameMatchesInput(
|
||||||
|
resultFrame,
|
||||||
|
executionGeneration,
|
||||||
|
{
|
||||||
|
flavour,
|
||||||
|
flavourVersion,
|
||||||
|
options,
|
||||||
|
pattern,
|
||||||
|
flags,
|
||||||
|
subject,
|
||||||
|
replacement,
|
||||||
|
mode,
|
||||||
|
scanAll,
|
||||||
|
syntax,
|
||||||
|
replacementSyntax,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const resultFrameIsStale = resultFrame !== undefined && !resultFrameIsCurrent;
|
||||||
|
const execution = resultFrame?.execution;
|
||||||
|
const replacementResult = resultFrame?.replacementResult;
|
||||||
|
const resultSubject = resultFrame?.subject ?? subject;
|
||||||
|
const resultSyntax = resultFrame?.syntax;
|
||||||
|
|
||||||
const extraction = useMemo(
|
const extraction = useMemo(
|
||||||
() => buildExtractionTree(execution?.matches ?? [], syntax?.captures ?? []),
|
() =>
|
||||||
[execution?.matches, syntax?.captures],
|
buildExtractionTree(
|
||||||
|
execution?.matches ?? [],
|
||||||
|
resultSyntax?.captures ?? [],
|
||||||
|
),
|
||||||
|
[execution?.matches, resultSyntax?.captures],
|
||||||
);
|
);
|
||||||
const captureRows = useMemo(
|
const captureRows = useMemo(
|
||||||
() => buildCaptureRows(execution?.matches ?? [], subject),
|
() => buildCaptureRows(execution?.matches ?? [], resultSubject),
|
||||||
[execution?.matches, subject],
|
[execution?.matches, resultSubject],
|
||||||
);
|
);
|
||||||
const repeatedCaptures = useMemo(
|
const repeatedCaptures = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Set(
|
new Set(
|
||||||
syntax?.captures
|
resultSyntax?.captures
|
||||||
.filter((capture) => capture.repeated)
|
.filter((capture) => capture.repeated)
|
||||||
.map((capture) => capture.number) ?? [],
|
.map((capture) => capture.number) ?? [],
|
||||||
),
|
),
|
||||||
[syntax?.captures],
|
[resultSyntax?.captures],
|
||||||
);
|
);
|
||||||
const selectedSubjectRange = useMemo(
|
const selectedSubjectRange = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -971,8 +1126,12 @@ export function Workbench() {
|
|||||||
[selectedCaptureRow, selectedExtraction?.range],
|
[selectedCaptureRow, selectedExtraction?.range],
|
||||||
);
|
);
|
||||||
const subjectMarkSummary = useMemo(
|
const subjectMarkSummary = useMemo(
|
||||||
() => subjectMarks(execution, selectedSubjectRange),
|
() =>
|
||||||
[execution, selectedSubjectRange],
|
subjectMarks(
|
||||||
|
resultFrameIsCurrent ? execution : undefined,
|
||||||
|
resultFrameIsCurrent ? selectedSubjectRange : undefined,
|
||||||
|
),
|
||||||
|
[execution, resultFrameIsCurrent, selectedSubjectRange],
|
||||||
);
|
);
|
||||||
const patternMarkSummary = useMemo(
|
const patternMarkSummary = useMemo(
|
||||||
() => syntaxMarks(syntax, selectedSyntax),
|
() => syntaxMarks(syntax, selectedSyntax),
|
||||||
@@ -980,15 +1139,19 @@ export function Workbench() {
|
|||||||
);
|
);
|
||||||
const replacementPreview = useMemo(
|
const replacementPreview = useMemo(
|
||||||
() =>
|
() =>
|
||||||
replacementSyntax?.accepted === true && replacementResult
|
resultFrame?.replacementSyntax?.accepted === true && replacementResult
|
||||||
? buildReplacementPreview(replacementSyntax, replacementResult, subject)
|
? buildReplacementPreview(
|
||||||
|
resultFrame.replacementSyntax,
|
||||||
|
replacementResult,
|
||||||
|
resultSubject,
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
[replacementResult, replacementSyntax, subject],
|
[replacementResult, resultFrame, resultSubject],
|
||||||
);
|
);
|
||||||
const diagnostics = [
|
const diagnostics = [
|
||||||
...(syntax?.diagnostics ?? []),
|
...(syntax?.diagnostics ?? []),
|
||||||
...(replacementSyntax?.diagnostics ?? []),
|
...(replacementSyntax?.diagnostics ?? []),
|
||||||
...(execution?.diagnostics ?? []),
|
...(resultFrameIsCurrent ? (execution?.diagnostics ?? []) : []),
|
||||||
];
|
];
|
||||||
const currentProject: RegexProjectV1 = {
|
const currentProject: RegexProjectV1 = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
@@ -1029,14 +1192,14 @@ export function Workbench() {
|
|||||||
);
|
);
|
||||||
setSelectedCaptureRow(captureRow);
|
setSelectedCaptureRow(captureRow);
|
||||||
setRequestedSubjectRange(
|
setRequestedSubjectRange(
|
||||||
requestEditorSelection && node.range
|
resultFrameIsCurrent && requestEditorSelection && node.range
|
||||||
? {
|
? {
|
||||||
startUtf16: node.range.startUtf16,
|
startUtf16: node.range.startUtf16,
|
||||||
endUtf16: node.range.endUtf16,
|
endUtf16: node.range.endUtf16,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
if (node.groupNumber !== undefined) {
|
if (resultFrameIsCurrent && node.groupNumber !== undefined) {
|
||||||
const definition = syntax?.captures.find(
|
const definition = syntax?.captures.find(
|
||||||
(capture) => capture.number === node.groupNumber,
|
(capture) => capture.number === node.groupNumber,
|
||||||
);
|
);
|
||||||
@@ -1052,16 +1215,29 @@ export function Workbench() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (resultFrameIsCurrent) {
|
||||||
setSelectedSyntax(syntax?.root);
|
setSelectedSyntax(syntax?.root);
|
||||||
setRequestedPatternRange(syntax?.root.range);
|
setRequestedPatternRange(syntax?.root.range);
|
||||||
|
} else {
|
||||||
|
setSelectedSyntax(undefined);
|
||||||
|
setRequestedPatternRange(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectSyntaxNode = (
|
const selectSyntaxNode = (
|
||||||
node: NormalizedRegexNode,
|
node: NormalizedRegexNode,
|
||||||
requestEditorSelection = true,
|
requestEditorSelection = true,
|
||||||
|
mapsToCurrentPattern = true,
|
||||||
) => {
|
) => {
|
||||||
|
if (!mapsToCurrentPattern) {
|
||||||
|
setSelectedSyntax(undefined);
|
||||||
|
setRequestedPatternRange(undefined);
|
||||||
|
setSelectedExtraction(undefined);
|
||||||
|
setSelectedCaptureRow(undefined);
|
||||||
|
setRequestedSubjectRange(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedSyntax(node);
|
setSelectedSyntax(node);
|
||||||
setRequestedPatternRange(
|
setRequestedPatternRange(
|
||||||
requestEditorSelection
|
requestEditorSelection
|
||||||
@@ -1072,6 +1248,13 @@ export function Workbench() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!resultFrameIsCurrent) {
|
||||||
|
setSelectedExtraction(undefined);
|
||||||
|
setSelectedCaptureRow(undefined);
|
||||||
|
setRequestedSubjectRange(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const enclosingCapture = syntax?.captures
|
const enclosingCapture = syntax?.captures
|
||||||
.filter(
|
.filter(
|
||||||
(capture) =>
|
(capture) =>
|
||||||
@@ -1129,7 +1312,9 @@ export function Workbench() {
|
|||||||
contribution: ReplacementContributionPreview,
|
contribution: ReplacementContributionPreview,
|
||||||
) => {
|
) => {
|
||||||
setSelectedReplacementContribution(contribution);
|
setSelectedReplacementContribution(contribution);
|
||||||
setSelectedReplacementToken(contribution.token);
|
setSelectedReplacementToken(
|
||||||
|
resultFrameIsCurrent ? contribution.token : undefined,
|
||||||
|
);
|
||||||
setRequestedOutputRange(contribution.outputRange);
|
setRequestedOutputRange(contribution.outputRange);
|
||||||
|
|
||||||
const match = execution?.matches.find(
|
const match = execution?.matches.find(
|
||||||
@@ -1163,7 +1348,7 @@ export function Workbench() {
|
|||||||
} else {
|
} else {
|
||||||
setSelectedExtraction(undefined);
|
setSelectedExtraction(undefined);
|
||||||
setSelectedCaptureRow(undefined);
|
setSelectedCaptureRow(undefined);
|
||||||
setRequestedSubjectRange(match?.range);
|
setRequestedSubjectRange(resultFrameIsCurrent ? match?.range : undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1416,6 +1601,7 @@ export function Workbench() {
|
|||||||
const importedFlavour = AVAILABLE_REGEX_FLAVOURS.require(project.flavour);
|
const importedFlavour = AVAILABLE_REGEX_FLAVOURS.require(project.flavour);
|
||||||
invalidatePatternSyntax();
|
invalidatePatternSyntax();
|
||||||
executionRevision.current += 1;
|
executionRevision.current += 1;
|
||||||
|
setExecutionGeneration(executionRevision.current);
|
||||||
engineSupervisor.current?.cancel();
|
engineSupervisor.current?.cancel();
|
||||||
cancelTestRun();
|
cancelTestRun();
|
||||||
setFlavour(importedFlavour.id);
|
setFlavour(importedFlavour.id);
|
||||||
@@ -1442,7 +1628,10 @@ export function Workbench() {
|
|||||||
setTestResults(new Map());
|
setTestResults(new Map());
|
||||||
setLive(false);
|
setLive(false);
|
||||||
setScanAll(project.ui?.scanAll ?? false);
|
setScanAll(project.ui?.scanAll ?? false);
|
||||||
clearExecutionResults();
|
setResultFrame(undefined);
|
||||||
|
setSyntaxSnapshot(undefined);
|
||||||
|
setReplacementSyntaxSnapshot(undefined);
|
||||||
|
clearResultSelections();
|
||||||
setSelectedSyntax(undefined);
|
setSelectedSyntax(undefined);
|
||||||
setRequestedPatternRange(undefined);
|
setRequestedPatternRange(undefined);
|
||||||
setSelectedReplacementToken(undefined);
|
setSelectedReplacementToken(undefined);
|
||||||
@@ -1973,7 +2162,7 @@ export function Workbench() {
|
|||||||
>
|
>
|
||||||
<CapabilityPanel
|
<CapabilityPanel
|
||||||
syntax={syntax}
|
syntax={syntax}
|
||||||
execution={execution}
|
execution={resultFrameIsCurrent ? execution : undefined}
|
||||||
onClose={() => setShowCapabilities(false)}
|
onClose={() => setShowCapabilities(false)}
|
||||||
/>
|
/>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
@@ -2041,10 +2230,17 @@ export function Workbench() {
|
|||||||
<span>{runState.status}</span>
|
<span>{runState.status}</span>
|
||||||
<strong>{runState.message}</strong>
|
<strong>{runState.message}</strong>
|
||||||
{execution?.accepted ? (
|
{execution?.accepted ? (
|
||||||
<small>
|
<small
|
||||||
|
className={resultFrameIsStale ? "is-stale" : undefined}
|
||||||
|
data-testid={resultFrameIsStale ? "stale-result-frame" : undefined}
|
||||||
|
>
|
||||||
|
{resultFrameIsStale ? "Stale result · " : ""}
|
||||||
{execution.matches.length.toLocaleString()} matches ·{" "}
|
{execution.matches.length.toLocaleString()} matches ·{" "}
|
||||||
{execution.elapsedMs.toFixed(2)} ms · native offsets{" "}
|
{execution.elapsedMs.toFixed(2)} ms · native offsets{" "}
|
||||||
{execution.engine.offsetUnit}
|
{execution.engine.offsetUnit}
|
||||||
|
{resultFrameIsStale
|
||||||
|
? " · bound to the last completed inputs; no ranges are applied to current editors"
|
||||||
|
: ""}
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -2097,10 +2293,17 @@ export function Workbench() {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<ExplanationTree
|
<ExplanationTree
|
||||||
root={syntax?.root}
|
root={presentedSyntax?.root}
|
||||||
selectedId={selectedSyntax?.id}
|
selectedId={selectedSyntax?.id}
|
||||||
flags={flags}
|
flags={
|
||||||
onSelect={(node) => selectSyntaxNode(node)}
|
presentedSyntaxIsStale
|
||||||
|
? (syntaxSnapshot?.request.flags ?? [])
|
||||||
|
: flags
|
||||||
|
}
|
||||||
|
stale={presentedSyntaxIsStale}
|
||||||
|
onSelect={(node) =>
|
||||||
|
selectSyntaxNode(node, true, !presentedSyntaxIsStale)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{mode !== "tests" && mode !== "corpus" ? (
|
{mode !== "tests" && mode !== "corpus" ? (
|
||||||
@@ -2146,6 +2349,11 @@ export function Workbench() {
|
|||||||
onChange={changeSubject}
|
onChange={changeSubject}
|
||||||
onSelectionChange={(range) => {
|
onSelectionChange={(range) => {
|
||||||
setRequestedSubjectRange(undefined);
|
setRequestedSubjectRange(undefined);
|
||||||
|
if (!resultFrameIsCurrent) {
|
||||||
|
setSelectedExtraction(undefined);
|
||||||
|
setSelectedCaptureRow(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const node = findSmallestExtractionNode(extraction, range);
|
const node = findSmallestExtractionNode(extraction, range);
|
||||||
if (node) {
|
if (node) {
|
||||||
selectExtraction(node, false);
|
selectExtraction(node, false);
|
||||||
@@ -2180,7 +2388,7 @@ export function Workbench() {
|
|||||||
{mode === "match" ? (
|
{mode === "match" ? (
|
||||||
<CaptureTable
|
<CaptureTable
|
||||||
rows={captureRows}
|
rows={captureRows}
|
||||||
subject={subject}
|
subject={resultSubject}
|
||||||
sourceResultTruncated={execution?.truncated ?? false}
|
sourceResultTruncated={execution?.truncated ?? false}
|
||||||
selectedId={selectedCaptureRow?.id}
|
selectedId={selectedCaptureRow?.id}
|
||||||
onSelect={(row) => {
|
onSelect={(row) => {
|
||||||
@@ -2194,7 +2402,7 @@ export function Workbench() {
|
|||||||
setSelectedCaptureRow(row);
|
setSelectedCaptureRow(row);
|
||||||
setSelectedExtraction(undefined);
|
setSelectedExtraction(undefined);
|
||||||
setRequestedSubjectRange(
|
setRequestedSubjectRange(
|
||||||
row.start === undefined
|
!resultFrameIsCurrent || row.start === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
startUtf16: row.start,
|
startUtf16: row.start,
|
||||||
@@ -2202,9 +2410,11 @@ export function Workbench() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const definition = syntax?.captures.find(
|
const definition = resultFrameIsCurrent
|
||||||
(capture) => capture.number === row.groupNumber,
|
? syntax?.captures.find(
|
||||||
);
|
(capture) => capture.number === row.groupNumber,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
if (definition && syntax) {
|
if (definition && syntax) {
|
||||||
const selected = findSmallestNode(syntax.root, definition.range);
|
const selected = findSmallestNode(syntax.root, definition.range);
|
||||||
setSelectedSyntax(selected);
|
setSelectedSyntax(selected);
|
||||||
@@ -2226,7 +2436,11 @@ export function Workbench() {
|
|||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
onReplacementChange={changeReplacement}
|
onReplacementChange={changeReplacement}
|
||||||
syntax={replacementSyntax}
|
syntax={replacementSyntax}
|
||||||
|
presentedSyntax={presentedReplacementSyntax}
|
||||||
|
syntaxStale={presentedReplacementSyntaxIsStale}
|
||||||
|
presentedFlavour={presentedReplacementFlavour}
|
||||||
result={replacementResult}
|
result={replacementResult}
|
||||||
|
resultFlavour={resultFrame?.flavour}
|
||||||
preview={replacementPreview}
|
preview={replacementPreview}
|
||||||
selectedToken={selectedReplacementToken}
|
selectedToken={selectedReplacementToken}
|
||||||
selectedContributionId={selectedReplacementContribution?.id}
|
selectedContributionId={selectedReplacementContribution?.id}
|
||||||
@@ -2244,7 +2458,7 @@ export function Workbench() {
|
|||||||
templateSource={listTemplate}
|
templateSource={listTemplate}
|
||||||
onTemplateChange={setListTemplate}
|
onTemplateChange={setListTemplate}
|
||||||
matches={execution?.matches ?? []}
|
matches={execution?.matches ?? []}
|
||||||
subject={subject}
|
subject={resultSubject}
|
||||||
sourceResultTruncated={execution?.truncated ?? false}
|
sourceResultTruncated={execution?.truncated ?? false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -48,6 +48,18 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100%;
|
||||||
|
background: var(--toolbox-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
@@ -74,7 +86,7 @@ textarea:focus-visible,
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: min(100%, 90rem);
|
width: min(100%, 90rem);
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding: 1rem clamp(0.75rem, 2.5vw, 2rem) 3rem;
|
padding: 1rem 0 3rem;
|
||||||
color: var(--toolbox-text);
|
color: var(--toolbox-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +173,24 @@ textarea:focus-visible,
|
|||||||
color: color-mix(in srgb, var(--regex-mint) 70%, var(--toolbox-text));
|
color: color-mix(in srgb, var(--regex-mint) 70%, var(--toolbox-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provenance-badge.is-stale,
|
||||||
|
.run-status > small.is-stale {
|
||||||
|
color: color-mix(in srgb, var(--regex-warning) 78%, var(--toolbox-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.provenance-badge.is-stale {
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--regex-warning) 45%,
|
||||||
|
var(--toolbox-border)
|
||||||
|
);
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--regex-warning) 9%,
|
||||||
|
var(--toolbox-surface)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.primary-button,
|
.primary-button,
|
||||||
.secondary-button,
|
.secondary-button,
|
||||||
.icon-button {
|
.icon-button {
|
||||||
@@ -2007,10 +2037,6 @@ tbody tr.is-selected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
@media (max-width: 40rem) {
|
||||||
.regex-application {
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-bar > label:not(.check-control) {
|
.command-bar > label:not(.check-control) {
|
||||||
flex: 1 1 10rem;
|
flex: 1 1 10rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
|
"$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.regex-tools",
|
"id": "de.add-ideas.regex-tools",
|
||||||
"name": "Regex Tools",
|
"name": "Regex Tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"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",
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
"telemetry": false
|
"telemetry": false
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"repository": "https://git.add-ideas.de/zemion/regex-tools",
|
"repository": "https://git.add-ideas.de/lotobo/regex-tools",
|
||||||
"license": "GPL-3.0-or-later"
|
"license": "GPL-3.0-or-later"
|
||||||
},
|
},
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"id": "source",
|
"id": "source",
|
||||||
"label": "Source",
|
"label": "Source",
|
||||||
"url": "https://git.add-ideas.de/zemion/regex-tools"
|
"url": "https://git.add-ideas.de/lotobo/regex-tools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const APPLICATION_VERSION = "0.4.0";
|
export const APPLICATION_VERSION = "0.4.2";
|
||||||
export const SYNTAX_PROFILE = "community";
|
export const SYNTAX_PROFILE = "community";
|
||||||
export const REGEXPP_VERSION = "4.12.2";
|
export const REGEXPP_VERSION = "4.12.2";
|
||||||
|
|||||||
@@ -15,6 +15,49 @@ async function waitForReady(page: Page) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function attemptRunWhileDisabled(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const button = document.querySelector<HTMLButtonElement>(".run-button");
|
||||||
|
if (!button) throw new Error("Run button missing");
|
||||||
|
if (!button.disabled) return false;
|
||||||
|
button.click();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("uses the shared 90rem content width without a browser body frame", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 1800, height: 900 });
|
||||||
|
await page.goto("./");
|
||||||
|
await expect(page.locator(".regex-application")).toBeVisible();
|
||||||
|
|
||||||
|
const layout = await page.evaluate(() => {
|
||||||
|
const shellMain = document.querySelector<HTMLElement>(
|
||||||
|
".toolbox-shell__main",
|
||||||
|
);
|
||||||
|
const application =
|
||||||
|
document.querySelector<HTMLElement>(".regex-application");
|
||||||
|
if (!shellMain || !application)
|
||||||
|
throw new Error("Application shell missing");
|
||||||
|
|
||||||
|
const shellMainStyle = getComputedStyle(shellMain);
|
||||||
|
return {
|
||||||
|
bodyMargin: getComputedStyle(document.body).margin,
|
||||||
|
shellMainWidth: shellMain.getBoundingClientRect().width,
|
||||||
|
shellMainContentWidth:
|
||||||
|
shellMain.clientWidth -
|
||||||
|
Number.parseFloat(shellMainStyle.paddingLeft) -
|
||||||
|
Number.parseFloat(shellMainStyle.paddingRight),
|
||||||
|
applicationWidth: application.getBoundingClientRect().width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(layout.bodyMargin).toBe("0px");
|
||||||
|
expect(layout.shellMainWidth).toBe(1440);
|
||||||
|
expect(layout.applicationWidth).toBe(layout.shellMainContentWidth);
|
||||||
|
});
|
||||||
|
|
||||||
test("standalone ECMAScript workbench parses, explains and extracts", async ({
|
test("standalone ECMAScript workbench parses, explains and extracts", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -446,23 +489,29 @@ test("rapid pattern and replacement edits cannot run with stale syntax", async (
|
|||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
const initialExtractionCount = await page.locator(".extraction-row").count();
|
||||||
await page.getByRole("button", { name: "Replace", exact: true }).click();
|
await page.getByRole("button", { name: "Replace", exact: true }).click();
|
||||||
|
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "(?<stale>\\w+)");
|
await setEditor(page, "editor-regular-expression-pattern", "(?<stale>\\w+)");
|
||||||
await setEditor(page, "editor-replacement-template", "$<stale>");
|
await setEditor(page, "editor-replacement-template", "$<stale>");
|
||||||
|
await setEditor(page, "editor-replacement-template", "[$<letter>]");
|
||||||
|
await setEditor(page, "editor-test-text", "A B");
|
||||||
await setEditor(
|
await setEditor(
|
||||||
page,
|
page,
|
||||||
"editor-regular-expression-pattern",
|
"editor-regular-expression-pattern",
|
||||||
"(?<letter>[A-Z])",
|
"(?<letter>[A-Z])",
|
||||||
);
|
);
|
||||||
await setEditor(page, "editor-replacement-template", "[$<letter>]");
|
|
||||||
await setEditor(page, "editor-test-text", "A B");
|
|
||||||
|
|
||||||
const runButton = page.getByRole("button", { name: "Run", exact: true });
|
const runButton = page.getByRole("button", { name: "Run", exact: true });
|
||||||
await expect(runButton).toBeDisabled();
|
expect(await attemptRunWhileDisabled(page)).toBe(true);
|
||||||
await runButton.evaluate((button: HTMLButtonElement) => button.click());
|
await expect(page.locator(".run-status")).toHaveClass(/status-idle/u);
|
||||||
await expect(page.locator(".run-status")).not.toHaveClass(/status-running/u);
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
"last completed result remains visible",
|
||||||
|
);
|
||||||
|
await expect(page.locator(".extraction-row")).toHaveCount(
|
||||||
|
initialExtractionCount,
|
||||||
|
);
|
||||||
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
|
|
||||||
await expect(runButton).toBeEnabled();
|
await expect(runButton).toBeEnabled();
|
||||||
const currentNamedToken = page.locator(".token-row").filter({
|
const currentNamedToken = page.locator(".token-row").filter({
|
||||||
@@ -472,9 +521,8 @@ test("rapid pattern and replacement edits cannot run with stale syntax", async (
|
|||||||
await expect(currentNamedToken.locator("small")).toHaveText("named capture");
|
await expect(currentNamedToken.locator("small")).toHaveText("named capture");
|
||||||
|
|
||||||
await page.getByRole("checkbox", { name: "Ignore case (i)" }).check();
|
await page.getByRole("checkbox", { name: "Ignore case (i)" }).check();
|
||||||
await expect(runButton).toBeDisabled();
|
expect(await attemptRunWhileDisabled(page)).toBe(true);
|
||||||
await runButton.evaluate((button: HTMLButtonElement) => button.click());
|
await expect(page.locator(".run-status")).toHaveClass(/status-idle/u);
|
||||||
await expect(page.locator(".run-status")).not.toHaveClass(/status-running/u);
|
|
||||||
|
|
||||||
await expect(runButton).toBeEnabled();
|
await expect(runButton).toBeEnabled();
|
||||||
await runButton.click();
|
await runButton.click();
|
||||||
@@ -484,22 +532,31 @@ test("rapid pattern and replacement edits cannot run with stale syntax", async (
|
|||||||
).toHaveText("[A] [B]");
|
).toHaveText("[A] [B]");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("manual editing clears results that no longer describe the inputs", async ({
|
test("manual editing retains an explicitly stale result without applying its ranges", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
|
||||||
|
const extractionRows = page.locator(".extraction-row");
|
||||||
|
const captureRows = page.locator(".capture-table-scroll tbody tr");
|
||||||
|
const initialExtractionCount = await extractionRows.count();
|
||||||
|
const initialCaptureCount = await captureRows.count();
|
||||||
|
expect(initialExtractionCount).toBeGreaterThan(0);
|
||||||
|
expect(initialCaptureCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "nomatch");
|
await setEditor(page, "editor-regular-expression-pattern", "nomatch");
|
||||||
|
|
||||||
await expect(page.locator(".run-status")).toHaveClass(/status-idle/u);
|
await expect(page.locator(".run-status")).toHaveClass(/status-idle/u);
|
||||||
await expect(page.locator(".run-status")).toContainText(
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
"previous results were cleared",
|
"last completed result remains visible until the current inputs complete",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".run-status small")).toHaveCount(0);
|
await expect(page.getByTestId("stale-result-frame")).toContainText(
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
"no ranges are applied to current editors",
|
||||||
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(0);
|
);
|
||||||
|
await expect(extractionRows).toHaveCount(initialExtractionCount);
|
||||||
|
await expect(captureRows).toHaveCount(initialCaptureCount);
|
||||||
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -509,6 +566,10 @@ test("capture-group resource limits block execution visibly", async ({
|
|||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
const initialExtractionCount = await page.locator(".extraction-row").count();
|
||||||
|
const initialCaptureCount = await page
|
||||||
|
.locator(".capture-table-scroll tbody tr")
|
||||||
|
.count();
|
||||||
|
|
||||||
await setEditor(
|
await setEditor(
|
||||||
page,
|
page,
|
||||||
@@ -520,8 +581,14 @@ test("capture-group resource limits block execution visibly", async ({
|
|||||||
await expect(page.locator(".run-status")).toContainText(
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
"above the 1,000 group execution limit",
|
"above the 1,000 group execution limit",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".run-status small")).toHaveCount(0);
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
await expect(page.locator(".extraction-row")).toHaveCount(
|
||||||
|
initialExtractionCount,
|
||||||
|
);
|
||||||
|
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(
|
||||||
|
initialCaptureCount,
|
||||||
|
);
|
||||||
|
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("large valid patterns bound syntax tree nodes and editor decorations", async ({
|
test("large valid patterns bound syntax tree nodes and editor decorations", async ({
|
||||||
@@ -779,6 +846,25 @@ test("replacement and list modes use native and typed templates", async ({
|
|||||||
page.getByTestId("editor-replacement-output").locator(".cm-selected-range"),
|
page.getByTestId("editor-replacement-output").locator(".cm-selected-range"),
|
||||||
).toHaveCount(1);
|
).toHaveCount(1);
|
||||||
|
|
||||||
|
await page.getByLabel("Live").uncheck();
|
||||||
|
await setEditor(page, "editor-replacement-template", "[$<user> — $<date>]");
|
||||||
|
await expect(page.getByTestId("stale-replacement-syntax")).not.toBeVisible();
|
||||||
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
|
await userContribution.click();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("editor-replacement-template")
|
||||||
|
.locator(".cm-selected-range"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("editor-regular-expression-pattern")
|
||||||
|
.locator(".cm-selected-range"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("editor-test-text").locator(".cm-selected-range"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "List", exact: true }).click();
|
await page.getByRole("button", { name: "List", exact: true }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "Generated rows" }),
|
page.getByRole("heading", { name: "Generated rows" }),
|
||||||
@@ -1117,6 +1203,10 @@ test("a timeout kills the worker and the next request uses a fresh engine", asyn
|
|||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
const initialExtractionCount = await page.locator(".extraction-row").count();
|
||||||
|
const initialCaptureCount = await page
|
||||||
|
.locator(".capture-table-scroll tbody tr")
|
||||||
|
.count();
|
||||||
await page.getByLabel("Manual limit").selectOption("250");
|
await page.getByLabel("Manual limit").selectOption("250");
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$");
|
await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$");
|
||||||
await setEditor(page, "editor-test-text", `${"a".repeat(48)}!`);
|
await setEditor(page, "editor-test-text", `${"a".repeat(48)}!`);
|
||||||
@@ -1127,9 +1217,13 @@ test("a timeout kills the worker and the next request uses a fresh engine", asyn
|
|||||||
await expect(page.locator(".run-status")).toContainText(
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
"worker was terminated",
|
"worker was terminated",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".run-status small")).toHaveCount(0);
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
await expect(page.locator(".extraction-row")).toHaveCount(
|
||||||
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(0);
|
initialExtractionCount,
|
||||||
|
);
|
||||||
|
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(
|
||||||
|
initialCaptureCount,
|
||||||
|
);
|
||||||
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
||||||
|
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "^ok$");
|
await setEditor(page, "editor-regular-expression-pattern", "^ok$");
|
||||||
|
|||||||
Reference in New Issue
Block a user