From 5e962ff0f318cec43f682bb80605e7513cca6566 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 2 Sep 2026 07:24:52 +0200 Subject: [PATCH] Release Text Tools 0.2.0 --- .gitea/workflows/verify.yml | 39 ++++ CHANGELOG.md | 7 + README.md | 20 +- SOURCE.md | 4 +- THIRD_PARTY_NOTICES.md | 8 +- docs/ARCHITECTURE.md | 6 +- docs/PRIVACY-SECURITY.md | 4 +- package-lock.json | 38 ++-- package.json | 10 +- playwright.config.ts | 22 +- public/CHANGELOG.md | 7 + public/LICENSES/npm-runtime-licenses.txt | 6 +- public/README.md | 20 +- public/SOURCE.md | 4 +- public/THIRD_PARTY_NOTICES.md | 8 +- public/docs/ARCHITECTURE.md | 6 +- public/docs/PRIVACY-SECURITY.md | 4 +- public/sw.js | 2 +- public/toolbox-app.json | 37 +++- src/components/Workbench.tsx | 223 +++++++++++++++++-- src/styles.css | 14 ++ src/text/evidence.ts | 251 ++++++++++++++++++++++ src/text/pipeline.ts | 259 +++++++++++++++++++++-- src/toolbox/manifest.source.json | 37 +++- src/version.ts | 2 +- tests/browser/app.spec.ts | 32 ++- tests/browser/responsive.spec.ts | 18 ++ tests/text/pipeline.test.ts | 105 +++++++++ tsconfig.app.json | 2 +- 29 files changed, 1093 insertions(+), 102 deletions(-) create mode 100644 .gitea/workflows/verify.yml create mode 100644 src/text/evidence.ts create mode 100644 tests/browser/responsive.spec.ts diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc21ab..e9e4ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added composable literal and line transforms with allocation preflights, plus BOM, UTF-8 validity, zero-byte and byte-newline evidence. +- Added paired text/artifact-evidence downloads with source/output hashes and + pipeline provenance; automatic Toolbox handoff remains intentionally disabled + pending the coordinated Portal consumer rollout. + ## 0.1.0 - 2026-09-01 - Added the initial local-first Text Tools workbench. diff --git a/README.md b/README.md index 7489526..b0b4dc6 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,25 @@ # Text Tools -Transform and inspect plain text locally in the browser. +Compose, inspect and evidence plain-text transformations locally in the browser. Text Tools is a standalone local-first application in the [add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal). Inputs are processed in the browser and are not uploaded. -## Version 0.1 scope +## Version 0.2 scope -- Ordered pipelines for line endings, trimming, whitespace, stable sorting and deduplication +- Ordered pipelines for line endings, trimming, whitespace, stable sorting, deduplication, literal replacement, line prefix/suffix/filter/number/join and line-order reversal - Locale-aware case conversion, Unicode NFC/NFD/NFKC/NFKD and best-effort Latin transliteration -- JSON/HTML/URL/Base64/hex escaping, wrapping and delimited-column selection -- Explicit UTF-8, UTF-16LE and Latin-1 file decoding plus text/code-point/line inventory -- Versioned JSON recipe import/export and per-step size/change reports +- JSON/HTML/URL/Base64/hex escaping plus strict inverse decode/unescape stages; binary decoders accept canonical encodings and require valid UTF-8 text +- Bounded RFC-style quoted CSV column selection across embedded delimiters and newlines, alongside the retained literal multi-character delimiter-per-line mode +- Explicit UTF-8, UTF-16LE and Latin-1 file decoding plus BOM, malformed UTF-8 offset, zero-byte parity, byte-level newline and replacement-character evidence +- Versioned JSON recipe import/export, per-step size/change reports, and a paired text-plus-JSON artifact download with source/output SHA-256 and transformation provenance Pipelines are limited to 2,000,000 UTF-16 input units and an 8×/32 MiB output expansion. File input is limited to 16 MiB before decoding. Encoding is selected explicitly rather than guessed; compatibility normalisation, transliteration, narrow encodings and several transforms can be lossy, so source, result, and warnings remain visible. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +The artifact evidence is a portable download contract, not an automatic Toolbox +handoff. SDK 0.3.0 provides the shared transfer contract, but automatic Open With +remains disabled in this release until the Portal consumer rollout is +coordinated. Local downloads stay explicit and portable in the meantime. + ## Development Requires Node.js 22 and npm 11. @@ -26,7 +32,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/text-tools-0.1.0.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/text-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/SOURCE.md b/SOURCE.md index b7ff323..7d44bc5 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Text Tools 0.1.0 is available at: +The corresponding source for Text Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/text-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/text-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 4a86456..b0d6612 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,12 +1,12 @@ # Third-party notices -Text Tools 0.1.0 directly depends on these runtime packages: +Text Tools 0.2.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9825471..cf1d583 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,6 +2,8 @@ Text Tools is a static React/Vite application wrapped in the shared Toolbox shell. `text/pipeline.ts` defines an ordered, typed list of transformations. `applyPipeline` starts from the exact source, applies enabled steps one at a time, checks expansion after every step, and returns the last successful output with per-step before/after counts and warnings. -Text encoding/decoding, line-ending conversion and Unicode-aware case primitives come from `@add-ideas/toolbox-helpers`. File decoding uses an explicit encoding and fatal-or-replacement choice; it does not guess a charset. Recipe JSON carries a schema/app identity, accepts at most 100 supported steps, discards supplied IDs and assigns local monotonic IDs. +Text encoding/decoding, line-ending conversion, bounded CSV parsing and Unicode-aware case primitives come from `@add-ideas/toolbox-helpers`. File decoding uses an explicit encoding and fatal-or-replacement choice; it does not guess a charset. Inverse JSON/HTML/URL/Base64/hex stages are strict: malformed syntax and non-UTF-8 byte output fail the step rather than being silently repaired. Column selection offers a quoted CSV mode that preserves embedded delimiters/newlines and a separate literal-delimiter-per-line mode. Recipe JSON carries a schema/app identity, accepts at most 100 supported steps, discards supplied IDs and assigns local monotonic IDs. -Version 0.1 operations are synchronous and bounded, so the app creates no worker and has no persistence or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. +`text/evidence.ts` inspects original bytes before decoding, including BOM, exact UTF-8 validity/first invalid offset, zero-byte parity and CR/LF/CRLF counts. Artifact export emits the selected output bytes and a deterministic evidence document containing canonical-source and exact-output SHA-256 values, inventories, recipe and step reports. The document records that direct Toolbox handoff is unavailable in this release; the SDK 0.3.0 transfer contract is intentionally not activated until the Portal consumer rollout is coordinated. + +Version 0.2 operations are synchronous and bounded, so the app creates no worker and has no persistence or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. diff --git a/docs/PRIVACY-SECURITY.md b/docs/PRIVACY-SECURITY.md index 97fd3ef..d6bc791 100644 --- a/docs/PRIVACY-SECURITY.md +++ b/docs/PRIVACY-SECURITY.md @@ -2,6 +2,8 @@ Source text, imported files, recipes and results stay in page memory. There is no telemetry, analytics, account, persistence or runtime network path. Imported text is rendered as text/preformatted content, not executable HTML; HTML escaping produces a string and does not preview it as markup. -Files are rejected above 16 MiB before reading. Decoded/pasted pipeline input is limited to 2,000,000 UTF-16 units, recipe text to 1,000,000 units/100 steps, and output to both 8× the original (with a small-input floor) and 32 MiB. These limits reduce accidental expansion but do not prove that output is safe for a downstream interpreter. +Files are rejected above 16 MiB before reading. Decoded/pasted pipeline input is limited to 2,000,000 UTF-16 units, recipe text to 1,000,000 units/100 steps, and output to both 8× the original (with a small-input floor) and 32 MiB. Quoted CSV selection additionally limits rows, columns and field size. Strict inverse stages reject malformed escapes/encodings and invalid UTF-8; they do not attempt permissive repair. These limits reduce accidental expansion but do not prove that output is safe for a downstream interpreter. Compatibility normalisation, case conversion, transliteration, whitespace/line transforms, column selection and narrow encodings can change or lose information. Locale sorting depends on the browser's `Intl.Collator`. The exact source, output, per-step counts and warnings remain visible so users can review those changes before copying or downloading. + +Byte evidence does not guess an encoding or prove the selected decoder is correct. A valid UTF-8 byte sequence may represent text intended for another encoding, zero-byte parity is only a UTF-16 review signal, and replacement characters may already have existed in the source. Artifact evidence hashes canonical UTF-8 source text and exact exported bytes, clearly labels both, and includes filenames and transformation details that may themselves be sensitive. diff --git a/package-lock.json b/package-lock.json index f2065ad..381064c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "text-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "text-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -42,24 +42,25 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", + "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "engines": { "node": ">=22" } }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -67,17 +68,16 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" - }, - "engines": { - "node": ">=20" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index 5799dbc..7120d1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "text-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Transform and inspect plain text locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,14 +39,14 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 731da62..2ab6a22 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index fbc21ab..e9e4ff0 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added composable literal and line transforms with allocation preflights, plus BOM, UTF-8 validity, zero-byte and byte-newline evidence. +- Added paired text/artifact-evidence downloads with source/output hashes and + pipeline provenance; automatic Toolbox handoff remains intentionally disabled + pending the coordinated Portal consumer rollout. + ## 0.1.0 - 2026-09-01 - Added the initial local-first Text Tools workbench. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 45a7c95..4f412f1 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index 7489526..b0b4dc6 100644 --- a/public/README.md +++ b/public/README.md @@ -1,19 +1,25 @@ # Text Tools -Transform and inspect plain text locally in the browser. +Compose, inspect and evidence plain-text transformations locally in the browser. Text Tools is a standalone local-first application in the [add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal). Inputs are processed in the browser and are not uploaded. -## Version 0.1 scope +## Version 0.2 scope -- Ordered pipelines for line endings, trimming, whitespace, stable sorting and deduplication +- Ordered pipelines for line endings, trimming, whitespace, stable sorting, deduplication, literal replacement, line prefix/suffix/filter/number/join and line-order reversal - Locale-aware case conversion, Unicode NFC/NFD/NFKC/NFKD and best-effort Latin transliteration -- JSON/HTML/URL/Base64/hex escaping, wrapping and delimited-column selection -- Explicit UTF-8, UTF-16LE and Latin-1 file decoding plus text/code-point/line inventory -- Versioned JSON recipe import/export and per-step size/change reports +- JSON/HTML/URL/Base64/hex escaping plus strict inverse decode/unescape stages; binary decoders accept canonical encodings and require valid UTF-8 text +- Bounded RFC-style quoted CSV column selection across embedded delimiters and newlines, alongside the retained literal multi-character delimiter-per-line mode +- Explicit UTF-8, UTF-16LE and Latin-1 file decoding plus BOM, malformed UTF-8 offset, zero-byte parity, byte-level newline and replacement-character evidence +- Versioned JSON recipe import/export, per-step size/change reports, and a paired text-plus-JSON artifact download with source/output SHA-256 and transformation provenance Pipelines are limited to 2,000,000 UTF-16 input units and an 8×/32 MiB output expansion. File input is limited to 16 MiB before decoding. Encoding is selected explicitly rather than guessed; compatibility normalisation, transliteration, narrow encodings and several transforms can be lossy, so source, result, and warnings remain visible. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +The artifact evidence is a portable download contract, not an automatic Toolbox +handoff. SDK 0.3.0 provides the shared transfer contract, but automatic Open With +remains disabled in this release until the Portal consumer rollout is +coordinated. Local downloads stay explicit and portable in the meantime. + ## Development Requires Node.js 22 and npm 11. @@ -26,7 +32,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/text-tools-0.1.0.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/text-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/public/SOURCE.md b/public/SOURCE.md index b7ff323..7d44bc5 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Text Tools 0.1.0 is available at: +The corresponding source for Text Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/text-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/text-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index 4a86456..b0d6612 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -1,12 +1,12 @@ # Third-party notices -Text Tools 0.1.0 directly depends on these runtime packages: +Text Tools 0.2.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/public/docs/ARCHITECTURE.md b/public/docs/ARCHITECTURE.md index 9825471..cf1d583 100644 --- a/public/docs/ARCHITECTURE.md +++ b/public/docs/ARCHITECTURE.md @@ -2,6 +2,8 @@ Text Tools is a static React/Vite application wrapped in the shared Toolbox shell. `text/pipeline.ts` defines an ordered, typed list of transformations. `applyPipeline` starts from the exact source, applies enabled steps one at a time, checks expansion after every step, and returns the last successful output with per-step before/after counts and warnings. -Text encoding/decoding, line-ending conversion and Unicode-aware case primitives come from `@add-ideas/toolbox-helpers`. File decoding uses an explicit encoding and fatal-or-replacement choice; it does not guess a charset. Recipe JSON carries a schema/app identity, accepts at most 100 supported steps, discards supplied IDs and assigns local monotonic IDs. +Text encoding/decoding, line-ending conversion, bounded CSV parsing and Unicode-aware case primitives come from `@add-ideas/toolbox-helpers`. File decoding uses an explicit encoding and fatal-or-replacement choice; it does not guess a charset. Inverse JSON/HTML/URL/Base64/hex stages are strict: malformed syntax and non-UTF-8 byte output fail the step rather than being silently repaired. Column selection offers a quoted CSV mode that preserves embedded delimiters/newlines and a separate literal-delimiter-per-line mode. Recipe JSON carries a schema/app identity, accepts at most 100 supported steps, discards supplied IDs and assigns local monotonic IDs. -Version 0.1 operations are synchronous and bounded, so the app creates no worker and has no persistence or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. +`text/evidence.ts` inspects original bytes before decoding, including BOM, exact UTF-8 validity/first invalid offset, zero-byte parity and CR/LF/CRLF counts. Artifact export emits the selected output bytes and a deterministic evidence document containing canonical-source and exact-output SHA-256 values, inventories, recipe and step reports. The document records that direct Toolbox handoff is unavailable in this release; the SDK 0.3.0 transfer contract is intentionally not activated until the Portal consumer rollout is coordinated. + +Version 0.2 operations are synchronous and bounded, so the app creates no worker and has no persistence or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. diff --git a/public/docs/PRIVACY-SECURITY.md b/public/docs/PRIVACY-SECURITY.md index 97fd3ef..d6bc791 100644 --- a/public/docs/PRIVACY-SECURITY.md +++ b/public/docs/PRIVACY-SECURITY.md @@ -2,6 +2,8 @@ Source text, imported files, recipes and results stay in page memory. There is no telemetry, analytics, account, persistence or runtime network path. Imported text is rendered as text/preformatted content, not executable HTML; HTML escaping produces a string and does not preview it as markup. -Files are rejected above 16 MiB before reading. Decoded/pasted pipeline input is limited to 2,000,000 UTF-16 units, recipe text to 1,000,000 units/100 steps, and output to both 8× the original (with a small-input floor) and 32 MiB. These limits reduce accidental expansion but do not prove that output is safe for a downstream interpreter. +Files are rejected above 16 MiB before reading. Decoded/pasted pipeline input is limited to 2,000,000 UTF-16 units, recipe text to 1,000,000 units/100 steps, and output to both 8× the original (with a small-input floor) and 32 MiB. Quoted CSV selection additionally limits rows, columns and field size. Strict inverse stages reject malformed escapes/encodings and invalid UTF-8; they do not attempt permissive repair. These limits reduce accidental expansion but do not prove that output is safe for a downstream interpreter. Compatibility normalisation, case conversion, transliteration, whitespace/line transforms, column selection and narrow encodings can change or lose information. Locale sorting depends on the browser's `Intl.Collator`. The exact source, output, per-step counts and warnings remain visible so users can review those changes before copying or downloading. + +Byte evidence does not guess an encoding or prove the selected decoder is correct. A valid UTF-8 byte sequence may represent text intended for another encoding, zero-byte parity is only a UTF-16 review signal, and replacement characters may already have existed in the source. Artifact evidence hashes canonical UTF-8 source text and exact exported bytes, clearly labels both, and includes filenames and transformation details that may themselves be sensitive. diff --git a/public/sw.js b/public/sw.js index ae4232e..e018ca8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "text-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.0"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index 70e42f7..e2ddccd 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,12 +3,21 @@ "schemaVersion": 1, "id": "de.add-ideas.text-tools", "name": "Text Tools", - "version": "0.1.0", - "description": "Transform and inspect plain text locally in the browser.", + "version": "0.2.0", + "description": "Compose text transforms and export encoding evidence locally.", "entry": "./", "icon": "./favicon.svg", "categories": ["text", "developer", "productivity"], - "tags": ["text", "unicode", "normalize", "sort", "escape", "encoding"], + "tags": [ + "text", + "unicode", + "pipeline", + "normalize", + "sort", + "escape", + "encoding", + "newline" + ], "integration": { "contextVersion": 1, "launchModes": ["navigate", "new-tab"], @@ -21,6 +30,28 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { + "mediaType": "text/*", + "extensions": [".txt", ".csv", ".md", ".log"], + "label": "Bounded text files" + } + ], + "produces": [ + { + "mediaType": "text/plain", + "extensions": [".txt"], + "label": "Transformed text" + }, + { + "mediaType": "application/json", + "extensions": [".json"], + "label": "Recipe and artifact evidence" + } + ] + }, + "capabilities": { "required": [], "optional": ["web-crypto"] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index c3d8159..867a9d7 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { - decodeText, encodeText, triggerBlobDownload, + triggerBlobDownloads, type TextEncoding, } from "@add-ideas/toolbox-helpers"; import { @@ -13,6 +13,11 @@ import { type StepType, type TransformStep, } from "../text/pipeline"; +import { + createTextArtifactEvidence, + decodeTextWithEvidence, + type TextByteEvidence, +} from "../text/evidence"; const initial = " Crème brûlée \r\nAlpha\nalpha\r\n Cedar \n"; const initialSteps: TransformStep[] = [ @@ -31,8 +36,16 @@ const STEP_LABELS: Record = { normalize: "Unicode normalization", transliterate: "Best-effort transliteration", escape: "Escape / encode", + unescape: "Decode / unescape strictly", wrap: "Wrap text", columns: "Select/reorder columns", + "replace-literal": "Replace literal text", + "prefix-lines": "Prefix lines", + "suffix-lines": "Suffix lines", + "filter-lines": "Keep matching lines", + "number-lines": "Number lines", + "join-lines": "Join lines", + "reverse-lines": "Reverse line order", }; function Option({ @@ -43,9 +56,13 @@ function Option({ change: (option: string) => void; }) { if ( - ["trim-lines", "trim-document", "dedupe-lines", "transliterate"].includes( - step.type, - ) + [ + "trim-lines", + "trim-document", + "dedupe-lines", + "transliterate", + "reverse-lines", + ].includes(step.type) ) return No options; if (step.type === "line-endings") @@ -104,10 +121,10 @@ function Option({ ))} ); - if (step.type === "escape") + if (step.type === "escape" || step.type === "unescape") return ( + change(JSON.stringify([event.target.value, values[1]])) + } + /> + + change(JSON.stringify([values[0], event.target.value])) + } + /> + + ); + } + if ( + [ + "prefix-lines", + "suffix-lines", + "filter-lines", + "number-lines", + "join-lines", + ].includes(step.type) + ) + return ( + change(event.target.value)} + placeholder={ + step.type === "join-lines" + ? "Delimiter; \\n and \\t supported" + : "Text" + } + /> + ); + const pieces = step.option.split("|"); + const mode = pieces.length >= 3 ? pieces[0]! : "literal"; + const delimiter = pieces.length >= 3 ? pieces[1]! : pieces[0] || ","; + const order = + pieces.length >= 3 ? pieces.slice(2).join("|") : pieces[1] || "1"; + const encode = (nextMode: string, nextDelimiter: string, nextOrder: string) => + change(`${nextMode}|${nextDelimiter}|${nextOrder}`); return ( - change(event.target.value)} - placeholder=",|3,1,2" - /> +
+ + encode(mode, event.target.value, order)} + placeholder=", or \\t" + /> + encode(mode, delimiter, event.target.value)} + placeholder="3,1,2" + /> +
); } @@ -188,6 +281,8 @@ export function Workbench() { const [fatalDecode, setFatalDecode] = useState(true); const [error, setError] = useState(""); const [recipe, setRecipe] = useState(""); + const [sourceEvidence, setSourceEvidence] = useState(); + const [sourceName, setSourceName] = useState("pasted-text.txt"); const outputLoss = outputEncoding === "latin1" && [...result.output].some((character) => character.codePointAt(0)! > 255); @@ -218,16 +313,18 @@ export function Workbench() { return; } try { - const decoded = decodeText( + const decoded = decodeTextWithEvidence( new Uint8Array(await file.arrayBuffer()), inputEncoding, fatalDecode, ); - if (decoded.length > 2_000_000) + if (decoded.text.length > 2_000_000) throw new Error( "Decoded text exceeds the 2,000,000 UTF-16-unit pipeline limit.", ); - setSource(decoded); + setSource(decoded.text); + setSourceEvidence(decoded.evidence); + setSourceName(file.name); setError(""); } catch (reason) { setError( @@ -253,7 +350,7 @@ export function Workbench() { }; const exportRecipe = () => { const value = JSON.stringify( - { schemaVersion: 1, app: "text-tools", version: "0.1.0", steps }, + { schemaVersion: 1, app: "text-tools", version: "0.2.0", steps }, null, 2, ); @@ -263,6 +360,41 @@ export function Workbench() { "text-tools-recipe.json", ); }; + const exportArtifact = async () => { + try { + const bytes = encodeText(result.output, outputEncoding); + const evidence = await createTextArtifactEvidence({ + sourceName, + sourceEvidence, + sourceText: source, + outputName: `transformed-${outputEncoding}.txt`, + outputEncoding, + outputBytes: bytes, + pipeline: result, + steps, + }); + triggerBlobDownloads( + [ + { + blob: new Blob([bytes as BlobPart], { type: "text/plain" }), + filename: evidence.output.name, + }, + { + blob: new Blob([JSON.stringify(evidence, null, 2) + "\n"], { + type: "application/json", + }), + filename: "text-tools-artifact-evidence.json", + }, + ], + { maximumFiles: 2, order: "input", revokeDelayMs: 1_000 }, + ); + setError(""); + } catch (reason) { + setError( + reason instanceof Error ? reason.message : "Artifact export failed.", + ); + } + }; const importRecipe = () => { try { if (recipe.length > 1_000_000) @@ -315,7 +447,8 @@ export function Workbench() {

Text Tools

Build an ordered, visible transformation pipeline for normalization, - lines, casing, escaping, wrapping, and columns. + lines, casing, strict escaping/decoding, wrapping, and quoted or + literal-delimited columns.

Browser-local @@ -362,11 +495,54 @@ export function Workbench() {