diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9ac984d..a8e6996 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,6 +14,7 @@ body: options: - Diff Checker - JSON Formatter + - JSON to Types - Base64 - List Tools - ID Generator diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62f00b5..eccc450 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: set -euo pipefail pnpm pack --pack-destination "$RUNNER_TEMP" >/dev/null LIST=$(tar -tzf "$RUNNER_TEMP"/fadeltd-devtools-core-*.tgz) - for sub in base64 color cron css csv diff env escape hash html-text id-gen json jwt list lorem number qr random slug sql text text-bytes text-stats timestamp unicode url yaml-toml; do + for sub in base64 color cron css csv diff env escape hash html-text id-gen json json-types jwt list lorem number qr random share slug sql text text-bytes text-stats timestamp unicode url yaml-toml; do # A here-string, not `echo | grep -q`: grep -q stops reading at the # first match, and under pipefail the writer's SIGPIPE fails the step. grep -qx "package/dist/$sub/index.d.ts" <<< "$LIST" || { echo "::error::Missing dist/$sub/index.d.ts"; exit 1; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7213d59..21d64fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,24 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ## [Unreleased] -Nothing yet. +### Added +- JSON to Types: paste a JSON sample and get TypeScript interfaces (or `type` aliases), a Zod schema or Go structs. Every sample is merged into one type, so a key missing from some array elements comes out optional and a key that is present but `null` comes out nullable, and the two are never confused. `1.0` is read as a float and `1` as an integer; an id beyond 2^53 is flagged instead of typed as something that would round it or that Zod 4 would reject, and in Go it widens to `int64` then `uint64`. Objects with the same structure share one type, and clashing names get the parent’s name, then a number. Go fields follow golint initialisms (`UserID`, `APIKey`), use pointers and `omitempty` for optional fields (switchable), align the way gofmt does, and a key a struct tag can’t hold is reported rather than mis-tagged. NDJSON is read one record per line, comments and trailing commas are accepted, invalid JSON shows its line and a caret, and large pastes are processed off the main thread. +- Diff: a new "Compare as" option shows both sides with their lines sorted or as key-sorted JSON, so reordered lines or keys stop showing up as changes. Both panes are read-only while it is on, a banner says which view you are looking at with Show original to go back, and a side that isn't valid JSON is shown as written with the reason. Big integers stay exact, and the copied patch says it is of the sorted view. +- List Tools: Copy link shares your pipeline's steps as a link. It carries the steps (and a set operation's compare list) but never your input text, and it lives in the part of the URL after `#`, which browsers never send to a server. Opening one replaces your steps and leaves your text alone; a damaged link changes nothing. +- Base64: a decoded SVG now previews beside its text, and every SVG gets an inspector with its size and viewBox and a report of what sanitising strips (scripts, `on*` handlers, `javascript:` URLs, ``), with Copy and Download of the sanitised SVG. The SVG is still only ever shown as an image, so nothing in it runs. +- Base64: binary output and files open in a scrollable hex view with the detected type, size and SHA-256, smooth up to the 25 MB file limit, with Copy, Download of the full dump and Download of the raw bytes. +- .env Converter: reads shell `export` files too, including several commands on one line and `'\''` quoting, detected automatically when only a shell could read the text. +- Works offline: after the first visit every tool loads without a connection. There is no install prompt. +- Links to the site now show a preview image when shared. + +### Changed +- JSON Formatter: input over 1 MB keeps validating and formatting but turns off auto-fixes, embedded-JSON expansion and lossless numbers, with a note that stays on screen; past 50 MB nothing is parsed. Output over 2 MB shows its start, with Copy and Download for all of it. At most 100 problems are listed. A 20 MB NDJSON paste stays responsive, and it is now kept across reloads (the tool saves to IndexedDB instead of localStorage, so its earlier saved input is reset once). +- YAML / JSON / TOML and Text to Binary / Hex convert off the main thread, so a large paste no longer freezes typing, and lines over 100,000 characters (in those tools and the JSON Formatter) scroll sideways instead of wrapping, which keeps a multi-MB single-line paste editable. +- The CSS and SQL formatters load Prettier and sql-formatter only when they are needed, instead of with every run. + +### Fixed +- Diff: a file with a single very long line is refused before the editor tries to draw it, instead of freezing the tab, whether it is picked, dropped, restored after a reload or pasted. +- Diff: in unified view, a file picked into the left side, or a swap, now shows up. ## [0.12.0] — 2026-10-29 diff --git a/CLAUDE.md b/CLAUDE.md index 832e7f8..b182b75 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,9 @@ This file holds the rules. The maintainer also keeps `ROADMAP.md` and `TODO.md` `src/lib/registry/index.ts` drives the router, the home grid, the sidebar, the command palette, the build-time prerender, and the sitemap. -**Adding a tool = its engine in `packages/core/src//` (with an `index.ts` and an `exports` entry in `packages/core/package.json`) + one folder under `src/tools/` + one entry in the registry. Nothing else.** If a change requires hand-editing the router, the sidebar, or the SEO scripts, the abstraction has been broken — fix that instead of working around it. +**Adding a tool = its engine in `packages/core/src//` (with an `index.ts` and an `exports` entry in `packages/core/package.json`) + one folder under `src/tools/` + one entry in the registry + its paragraph in `src/lib/registry/about.ts`. Nothing else.** If a change requires hand-editing the router, the sidebar, or the SEO scripts, the abstraction has been broken — fix that instead of working around it. + +`about.ts` holds one honest paragraph per tool (which edge cases it actually handles) for the prerendered HTML. It is a separate module because only `scripts/seo.ts` reads it: in the registry it would ship in the preloaded chunk every visitor downloads. Never import it from `src/`. Typecheck fails when a tool has no paragraph. The one deliberate exception is the GitHub issue forms. They are static YAML that GitHub reads directly, so nothing in the build can generate them. **When you add a tool or a registry category, add it to the "Which tool?" dropdown in `.github/ISSUE_TEMPLATE/bug_report.yml`**, using the registry `title`. If the new tool changes what this project will or won't build, update the list of standing no's in `tool_request.yml` as well. diff --git a/README.md b/README.md index 63388e9..cab7e09 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,63 @@ # devtools.fadeltd.dev -**Free, open-source developer tools that run entirely in your browser.** A JSON formatter, diff checker, YAML/TOML converter, SQL and CSS formatter, CSV viewer, base64 encoder/decoder, JWT decoder, timestamp and cron tools, URL encoder, string escaper, hash and HMAC calculator, UUID, ID and password generator, color and contrast checker, QR code generator, Unicode inspector, word counter, HTML-to-text extractor and list sorter — with no backend, no accounts, no ads and no tracking. +**Free, open-source developer tools that run entirely in your browser.** A JSON formatter, JSON to TypeScript / Zod / Go generator, diff checker, YAML/TOML converter, SQL and CSS formatter, CSV viewer, base64 encoder/decoder, JWT decoder, timestamp and cron tools, URL encoder, string escaper, hash and HMAC calculator, UUID, ID and password generator, color and contrast checker, QR code generator, Unicode inspector, word counter, HTML-to-text extractor and list sorter — with no backend, no accounts, no ads and no tracking. [![Live](https://img.shields.io/badge/live-devtools.fadeltd.dev-38bdf8)](https://devtools.fadeltd.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![No backend](https://img.shields.io/badge/backend-none-22c55e)](#why-another-one-of-these) +[![No backend](https://img.shields.io/badge/backend-none-22c55e)](#why-another-one) [![XERJ Corpus Hub](https://hub.xerj.org/contributors/fadeltd/badge.svg)](https://hub.xerj.org/contributors/fadeltd) ### → [**devtools.fadeltd.dev**](https://devtools.fadeltd.dev) -## Why another one of these? +## Why another one? -Because every other online JSON formatter, base64 decoder and diff checker sends what you paste to a server. +Every other online JSON formatter, base64 decoder and diff checker sends what you paste to a server. This one has no backend: it is static files on a CDN, makes zero network requests, and works offline after the first visit. Paste a production token, an API response or a customer record without thinking about it. -This one cannot. There is no backend — it is static files on a CDN, so there is no server to send anything to. You can open the network tab and watch it make zero requests, or turn off your wifi and keep using it. Paste a production token, an API response or a customer record without thinking about it. +It also does what the free sites usually don't: reports **every** JSON error with line, column and caret; **expands JSON serialised inside strings** (structured logs, double-encoded too); and never adds the **phantom blank line** to your lists. -Three other things it does that the free sites generally do not: +## Tools -- **Expands JSON that was serialised into a string** — the thing every structured logger does, including double-encoded. No more copying a `payload` field into a second tab. -- **Reports every JSON error, not just the first**, each with a line, column and a caret that lines up even in tab-indented files. -- **Does not corrupt your lists.** `a\nb\n` is two lines, not three — the phantom trailing blank line that most online list tools add. +`⌘K` jumps to any tool, `⌘1`–`⌘9` to pinned ones, `?` lists shortcuts. Your last input is restored when you return. -## Tools +### Data and formats + +- **[JSON Formatter & Validator](https://devtools.fadeltd.dev/json)** — format, minify, sort keys; every error with line and caret; one-click fixes; embedded JSON expanded; tree view with JSON Pointer / jq paths; lossless big numbers; stays responsive on 20 MB +- **[JSON to TypeScript, Zod and Go](https://devtools.fadeltd.dev/json-types)** — types inferred from sample JSON or NDJSON, optional vs nullable kept apart, int vs float, golint names +- **[YAML ↔ JSON ↔ TOML Converter](https://devtools.fadeltd.dev/yaml-toml)** — auto-detected input, positioned errors, YAML 1.1 gotchas, comments kept YAML → YAML, exact big integers +- **[CSV to Table](https://devtools.fadeltd.dev/csv)** — RFC 4180 parsing, delimiter detection, virtualised table, convert to JSON, Markdown or TSV +- **[SQL Formatter](https://devtools.fadeltd.dev/sql)** — twelve dialects, comments kept, parse errors with line and column, safe minify +- **[CSS Formatter & Minifier](https://devtools.fadeltd.dev/css)** — Prettier for CSS, SCSS and Less; a minifier that keeps `calc()` and licence comments +- **[.env Converter](https://devtools.fadeltd.dev/env)** — .env ↔ JSON ↔ shell `export` ↔ docker env-file, dotenv-accurate, never saved +- **[Cron Expression Explainer](https://devtools.fadeltd.dev/cron)** — plain English plus the next 10 runs in any time zone, DST-correct +- **[Unix Timestamp Converter](https://devtools.fadeltd.dev/timestamp)** — epoch s/ms/µs/ns ↔ ISO 8601 in any time zone, relative time, ISO week +- **[Number Base Converter](https://devtools.fadeltd.dev/number)** — bases 2–36, two's complement, KB vs KiB, chmod octal ↔ symbolic + +### Text and lists + +- **[Diff Checker](https://devtools.fadeltd.dev/diff)** — side-by-side or unified, word-level highlights, ignore whitespace or case, compare as sorted lines or key-sorted JSON, unified patch, share by link +- **[List Tools](https://devtools.fadeltd.dev/list)** — sort, natural sort, dedupe, shuffle, filter and 11 more steps chained into a shareable pipeline +- **[Word Counter & Text Statistics](https://devtools.fadeltd.dev/text-stats)** — characters four ways, words, sentences, reading time, word frequency +- **[Unicode Inspector](https://devtools.fadeltd.dev/unicode)** — finds zero-width, bidi (Trojan Source) and other invisible characters, one-click clean +- **[Slug Generator](https://devtools.fadeltd.dev/slug)** — URL slugs and cross-platform safe filenames, case collisions flagged +- **[HTML to Text](https://devtools.fadeltd.dev/html-text)** — readable text that keeps paragraphs, lists and tables + +### Encoding and security + +- **[Base64 Encode / Decode](https://devtools.fadeltd.dev/base64)** — correct UTF-8, base64url, files and images to `data:` URIs, hex view with SHA-256, SVG sanitising report +- **[URL Encode / Decode](https://devtools.fadeltd.dev/url)** — component, full-URL and form encoding, query parameter table +- **[String Escaper](https://devtools.fadeltd.dev/escape)** — JSON, JavaScript, SQL, shell, regex and HTML entities, both ways +- **[Text to Binary / Hex](https://devtools.fadeltd.dev/text-bytes)** — UTF-8 ↔ binary, hex, decimal or octal, invalid bytes located +- **[JWT Decoder & Verifier](https://devtools.fadeltd.dev/jwt)** — claims as dates, expiry, signature check against a JWK, JWKS, PEM or secret; never saved +- **[Hash Generator](https://devtools.fadeltd.dev/hash)** — SHA-1, SHA-256, SHA-384, SHA-512, compare against an expected hash +- **[HMAC Webhook Signature](https://devtools.fadeltd.dev/hmac)** — GitHub, Stripe and Slack signatures, verify with timestamp tolerance + +### Generators and color -| | | -|---|---| -| **[Diff Checker](https://devtools.fadeltd.dev/diff)** | Side-by-side or unified, word-level highlighting, ignore whitespace / case / trim, copy as a unified patch, share by link (the content lives in the URL fragment, never on a server), `Alt+↑/↓` to jump between changes | -| **[Base64](https://devtools.fadeltd.dev/base64)** | Encode/decode with correct UTF-8, base64url, padding control. Unwraps PEM/MIME line breaks and `data:` URIs, and tells you what it changed | -| **[List Tools](https://devtools.fadeltd.dev/list)** | Sort (natural or alphabetical), remove duplicates, randomize, reverse — without the phantom blank line every other list tool adds | -| **[JSON Formatter](https://devtools.fadeltd.dev/json)** | Format, minify, sort keys. Reports *every* problem with an exact line, column and caret, detects NDJSON, keeps big numbers byte-exact, and **expands JSON that was serialised into a string** — the thing structured logs do, including double-encoded. Tree view with copy-path (pointer / dot / jq), and one-click fixes for trailing commas, comments and single quotes, previewed as a diff | -| **[ID Generator](https://devtools.fadeltd.dev/id-gen)** | Prefixed Stripe-style ids, UUID v4, time-ordered UUID v7 and ULID, custom alphabet and length, bulk generation, entropy readout, and an inspector that reads a UUID's version or a v7/ULID's creation time. Doubles as a password generator; nothing generated is ever saved | -| **[Text Statistics](https://devtools.fadeltd.dev/text-stats)** | Characters, letters, words, sentences, paragraphs, reading time, and word/phrase frequency | -| **[HTML to Text](https://devtools.fadeltd.dev/html-text)** | Readable plain text from HTML, keeping paragraphs, lists and tables; scripts, hidden elements and icon ligatures dropped | -| **[Timestamp Converter](https://devtools.fadeltd.dev/timestamp)** | Epoch s/ms/µs/ns with auto-detected unit, exact to the nanosecond; ISO 8601 in UTC and any time zone with its real offset; HTTP-date, relative time, weekday and ISO week | -| **[JWT Decoder](https://devtools.fadeltd.dev/jwt)** | Header and payload as JSON, exp/iat/nbf as local and UTC dates, expiry status with clock-skew leeway, signature check against a pasted PEM, JWK, JWKS or HS secret (HS/RS/PS/ES/EdDSA, refusing `alg: none` and algorithm confusion), JWE header, malformed-segment warnings, never saved | -| **[URL Encode / Decode](https://devtools.fadeltd.dev/url)** | Component, full-URL and form encoding; decode with malformed-escape positions; double-encoding detection; URL parts and query parameter table | -| **[Hash Text](https://devtools.fadeltd.dev/hash)** | SHA-1, SHA-256, SHA-384 and SHA-512 at once in hex, HEX or base64; compare against an expected hash; trailing-newline and CRLF warnings | -| **[HMAC Signature](https://devtools.fadeltd.dev/hmac)** | GitHub, Stripe and Slack webhook signatures; verify a pasted header, with timestamp tolerance; text, hex or base64 keys; never saved | -| **[Slug Generator](https://devtools.fadeltd.dev/slug)** | URL slugs with accent folding, camelCase splitting and a word-safe max length; cross-platform safe filenames (reserved names, forbidden characters, 255-byte cap, case collisions flagged); one per line | -| **[Text to Binary / Hex](https://devtools.fadeltd.dev/text-bytes)** | UTF-8 text ↔ binary, hex, decimal or octal; `0x` / `\x` prefixes and C-string output; tolerant paste parsing with exact error positions; format auto-detect; invalid UTF-8 located, with a labelled fallback | -| **[Number Converter](https://devtools.fadeltd.dev/number)** | Bases 2–36 with no precision loss, two's complement, byte sizes in SI vs IEC, chmod octal ↔ symbolic with checkboxes | -| **[Unicode Inspector](https://devtools.fadeltd.dev/unicode)** | Every code point with line:column, UTF-8/UTF-16 and category; flags zero-width, bidi (Trojan Source), exotic spaces, tags, lone surrogates; flagged-only view; one-click clean with optional NFC/NFKC | -| **[YAML / JSON / TOML](https://devtools.fadeltd.dev/yaml-toml)** | Convert between YAML, JSON and TOML in any direction with auto-detection; positioned errors with a caret; YAML 1.1 trap notes (`no`, `on`, `0755`); comments kept YAML → YAML; anchors, merge keys and multi-document YAML; exact big integers | -| **[SQL Formatter](https://devtools.fadeltd.dev/sql)** | Twelve dialects, keyword case, indent and statement spacing; comments kept; parse errors with line and column; minify that leaves strings, quoted identifiers and comments untouched | -| **[CSS Formatter](https://devtools.fadeltd.dev/css)** | Prettier for CSS, SCSS and Less with width, indent and quote options; syntax errors with line and column; a careful CSS minifier that keeps `calc()` spacing, licence comments and data URIs intact | -| **[CSV to Table](https://devtools.fadeltd.dev/csv)** | RFC 4180 parsing with quoted newlines, delimiter detection (comma, tab, semicolon, pipe; European decimal commas handled), virtualised table with sticky header and column type hints, ragged-row and unclosed-quote warnings by line, convert to JSON, Markdown, CSV or TSV | -| **[.env Converter](https://devtools.fadeltd.dev/env)** | .env ↔ JSON ↔ shell `export` ↔ docker `--env-file`; dotenv-accurate parsing (quotes, escapes, multiline, inline comments, CRLF); per-value safe quoting; problems reported by line; flatten nested JSON with `__`; mask values for screen sharing; never saved | -| **[Cron Explainer](https://devtools.fadeltd.dev/cron)** | Cron in plain English with a per-field breakdown; next 10 runs in any time zone with DST handled the way Vixie cron does; 6-field seconds, macros, day-of-month/day-of-week OR, Quartz syntax called out by name | -| **[String Escaper](https://devtools.fadeltd.dev/escape)** | Escape and unescape for JSON, JavaScript (`"` `'` or template), SQL (standard `''` or MySQL backslashes), POSIX shell (`'\''` quoting), regex and HTML entities; positioned unescape errors with the fix | -| **[Color Converter](https://devtools.fadeltd.dev/color)** | Hex, RGB, HSL and OKLCH from any CSS color or a picker; all 148 named colors; out-of-sRGB-gamut warning with a chroma-reduced fallback; alpha; positioned errors | -| **[Contrast Checker](https://devtools.fadeltd.dev/contrast)** | WCAG 2 ratio with AA/AAA for normal and large text and 3:1 for UI; live preview; alpha blended over the background; nearest passing text color by OKLCH lightness, with Apply | -| **[QR Code](https://devtools.fadeltd.dev/qr)** | QR codes for text, URLs and Wi-Fi with a chosen error correction level, downloadable as a single-path SVG or a crisp PNG; scan-risk warnings; the Wi-Fi password is never saved | -| **[Lorem Ipsum](https://devtools.fadeltd.dev/lorem)** | Exact counts of words, sentences or paragraphs, optional classic opening, plain text / HTML `

` / Markdown output, one-key Regenerate | - -Press `⌘K` anywhere to jump to a tool, `⌘1`–`⌘9` for pinned ones, and `?` for the shortcut list. Your last input is restored when you come back. +- **[UUID, ULID & Password Generator](https://devtools.fadeltd.dev/id-gen)** — UUID v4/v7, ULID, Stripe-style prefixed ids, passwords with an entropy readout +- **[QR Code Generator](https://devtools.fadeltd.dev/qr)** — text, URLs and Wi-Fi, SVG or PNG download +- **[Lorem Ipsum Generator](https://devtools.fadeltd.dev/lorem)** — exact words, sentences or paragraphs as text, HTML or Markdown +- **[Color Converter](https://devtools.fadeltd.dev/color)** — hex ↔ RGB ↔ HSL ↔ OKLCH with gamut warnings +- **[WCAG Contrast Checker](https://devtools.fadeltd.dev/contrast)** — AA/AAA verdicts and the nearest passing color ## Development @@ -71,7 +75,7 @@ pnpm deploy # build + deploy ### Adding a tool -Each tool is an engine in `packages/core/src//` (pure functions: no React, no DOM, no store — this is where all the tests live), a thin `.tsx` shell in `src/tools//`, and one entry in `src/lib/registry/index.ts`. That registry drives the router, home grid, sidebar, command palette, build-time prerender and sitemap — so nothing else needs editing. +Each tool is an engine in `packages/core/src//` (pure functions, where all the tests live), a thin `.tsx` shell in `src/tools//`, one entry in `src/lib/registry/index.ts` and a paragraph in `src/lib/registry/about.ts`. The registry drives the router, home grid, sidebar, command palette, prerender and sitemap, so nothing else needs editing. ### Use the engines diff --git a/index.html b/index.html index 42ce577..63d1e58 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,11 @@ content="Diff, base64, JSON and list tools that run entirely in your browser. Nothing you paste is ever uploaded." /> + + + + +

diff --git a/package.json b/package.json index 1785f53..c40b990 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "cmdk": "1.1.1", "codemirror": "6.0.2", "comlink": "4.4.2", + "dompurify": "3.4.17", "idb-keyval": "6.3.0", "lucide-react": "1.47.0", "prettier": "3.9.10", @@ -51,6 +52,7 @@ "typescript": "7.0.2", "vite": "8.3.0", "vite-node": "^6.0.0", + "vite-plugin-pwa": "2.0.0", "vitest": "5.0.1", "wrangler": "4.136.1" }, diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 1428c88..04c15e4 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -6,7 +6,23 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ## [Unreleased] -Nothing yet. +### Added +- `/json-types`: `jsonTypes(text, { target, rootName, declaration, inferTypes, pointers })` generates TypeScript, Zod (syntax valid in v3 and v4) or Go from sample JSON, JSONC or NDJSON. Optional (missing) and nullable (`null`) fields are kept apart; integers and floats are told apart by literal via lossless parsing; integer width is tracked (`safe`, `int64`, `uint64`, beyond); identical structures share one named type; Go output uses golint initialisms and gofmt column alignment. Returns positioned parse errors and path-tagged notes for anything approximated; never throws. Also exports `inferShape`. +- `/share`: `encodeCompact` / `decodeCompact` turn any JSON value into a versioned, URL-safe `1.` string and back, with a cap on decoded size; decoding never throws. `fragmentValue` reads `#key=` from a location hash. +- `/list`: `encodePipeline` / `decodePipeline` for `#p=` pipeline links, and strict `parseOp` / `parseOps` / `parseSharedSteps` validators that rebuild every op field by field and reject anything malformed. +- `/diff`: `transformForDiff(text, 'sort-lines' | 'json', { lossless })` for comparing sorted or key-sorted JSON views, and the size thresholds `MAX_DIFF_CHARS`, `MAX_DIFF_LINE_CHARS`, `LINE_ONLY_CHARS` with `longestLineOf`. +- `/base64`: `hexRow`, `formatHexRow`, `hexDumpRows` and `hexRowCount` for virtualised hex views (`hexDump` uses them, same output); `summarizeRemovals` groups sanitiser removals into a strip report; `standaloneSvg`, `svgRoot` and `SVG_INSPECT_MAX_BYTES`. +- `/json`: `assessJsonSize` / `jsonSizeTier` (`ok` up to 1 MB, `large` up to 50 MB, then `refuse`) and `outputHead`; `analyze` reports at most `MAX_ISSUES` problems with a `moreIssues` flag and windows long snippet lines; `flattenTree` and `countNodes` take limits. +- `/text`: `utf8Length` (UTF-8 size without encoding) and `hasLongLine` with `NO_WRAP_LINE`. +- `/env`: `'shell'` input format: `convertEnv` reads shell export files (BOM and CRLF normalised) and `detectEnvFormat` picks it when dotenv cannot read the text. + +### Changed +- `/diff`: `assessSize` is much faster on large input; share links are built on `/share` (same format, same API). +- `/json`: `analyze` no longer builds a parse tree to find errors, and `stripJsonc` copies in slices, which makes both several times faster on large input. +- `/env`: `parseShellExports` accepts several `;`-separated commands on one line. + +### Fixed +- `/list`: a filter line-number range such as `1-999999999999` no longer loops over numbers past the end of the document. ## [0.5.0] — 2026-10-29 diff --git a/packages/core/README.md b/packages/core/README.md index 26f596e..489f45e 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -96,6 +96,14 @@ convert('country: no\n', 'auto', 'json').notes[0] // { code: 'yaml11-bool', … convert('{"a": null}', 'json', 'toml').errors[0] // { code: 'toml-null', path: '.a', line: 1, column: 2, … } ``` +```js +import { jsonTypes } from '@fadeltd/devtools-core/json-types' + +jsonTypes('[{"id": 1, "note": null}, {"id": 2, "note": "x", "tag": "a"}]', { target: 'typescript', rootName: 'Row' }).output +// export type RowList = Row[]; +// export interface Row { id: number; note: string | null; tag?: string; } (one field per line) +``` + ```js import { describeCron, nextRuns, parseCron } from '@fadeltd/devtools-core/cron' @@ -115,10 +123,11 @@ await formatSql('select a from t where b = 1', { dialect: 'postgresql', keywordC | Subpath | What it does | |---|---| -| `/list` | Sort, natural sort, dedupe, shuffle, case, prefix/suffix, filter, number, wrap, join, split, trim, normalise, set operations, slice; chained with per-step line counts | -| `/json` | Every error with line and column, auto-fixes for almost-JSON, embedded JSON-in-a-string expansion, lossless big numbers, tree flattening, path formats (pointer, dot, jq) | -| `/diff` | Two-stage line → word diff, ignore whitespace/case/trim, exact stats, unified patch, share-link encoding | -| `/base64` | UTF-8-correct encode and decode, base64url, input sanitising, magic-byte type sniffing, hex dump, `data:` URI / `` / CSS output | +| `/list` | Sort, natural sort, dedupe, shuffle, case, prefix/suffix, filter, number, wrap, join, split, trim, normalise, set operations, slice; chained with per-step line counts; validated `#p=` pipeline links | +| `/json` | Every error with line and column, auto-fixes for almost-JSON, embedded JSON-in-a-string expansion, lossless big numbers, tree flattening, path formats (pointer, dot, jq), size tiers for large input | +| `/json-types` | TypeScript, Zod or Go types inferred from sample JSON or NDJSON: optional vs nullable, shared and collision-safe type names, int vs float by literal, Go initialisms and gofmt alignment, notes for anything approximated | +| `/diff` | Two-stage line → word diff, ignore whitespace/case/trim, sorted-lines and key-sorted-JSON views, exact stats, size guard, unified patch, share-link encoding | +| `/base64` | UTF-8-correct encode and decode, base64url, input sanitising, magic-byte type sniffing, hex dump rows for virtualised views, SVG strip reports, `data:` URI / `` / CSS output | | `/id-gen` | Prefixed IDs and passwords from `crypto.getRandomValues` with rejection sampling, UUID v4/v7 and ULID (monotonic), an ID inspector, plus entropy | | `/text-stats` | Characters (four meanings), words, sentences (abbreviation-aware), paragraphs, reading time, word and phrase frequency | | `/html-text` | Readable text from HTML that keeps paragraphs, lists and tables | @@ -134,13 +143,14 @@ await formatSql('select a from t where b = 1', { dialect: 'postgresql', keywordC | `/sql` | SQL formatting in twelve dialects via `sql-formatter` (optional peer, loaded on first use), errors with line and column, and a dialect-aware minifier | | `/css` | CSS/SCSS/Less formatting via Prettier standalone (optional peer, loaded on first use), errors with line and column, and a plain-CSS minifier | | `/csv` | RFC 4180 parsing in linear time with positioned issues, delimiter sniffing, column type hints, and JSON / Markdown / CSV / TSV output | -| `/env` | .env ↔ JSON ↔ shell export ↔ docker env-file: dotenv-accurate parsing, safe quoting, positioned errors, JSON flattening | +| `/env` | .env ↔ JSON ↔ shell export ↔ docker env-file: dotenv-accurate parsing, shell export files as input, safe quoting, positioned errors, JSON flattening | | `/cron` | 5- and 6-field cron and `@` macros, Vixie day OR semantics, next runs in any IANA zone with Vixie DST handling, never-fires detection, English via `cronstrue` | | `/escape` | JSON, JavaScript, SQL (standard/MySQL), POSIX shell, regex and HTML-entity escaping and strict unescaping, with positioned errors | | `/color` | CSS color parsing with positioned errors; sRGB ↔ HSL ↔ OKLCH; gamut detection and clipping; WCAG contrast and nearest passing foreground | | `/qr` | QR payloads for text, URLs and Wi-Fi, encoded to a module matrix, plus a single-path SVG | | `/lorem` | Lorem ipsum by exact word, sentence or paragraph count, as text, HTML or Markdown, seedable | -| `/text` | Line/column offsets and character counts shared by the tools | +| `/text` | Line/column offsets, character counts, UTF-8 size without encoding and long-line detection, shared by the tools | +| `/share` | Versioned, URL-safe compact encoding of any JSON value (deflate-raw + base64url) with a decode cap, for links that keep their content in the URL fragment | | `/random` | Unbiased crypto random source and a seeded PRNG | ## Stability diff --git a/packages/core/package.json b/packages/core/package.json index 05bfdc2..d596997 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@fadeltd/devtools-core", "version": "0.5.0", - "description": "The engines behind devtools.fadeltd.dev: list pipelines, JSON errors and fixes, diff, base64, IDs and UUIDs, timestamps, JWT decoding and verification, URL encoding, hashes and HMAC, slugs, text bytes, number bases, Unicode inspection, YAML/JSON/TOML, SQL and CSS formatting, CSV, .env, cron, string escaping, colors and contrast, QR codes and lorem ipsum. Pure functions, no DOM, no network.", + "description": "The engines behind devtools.fadeltd.dev: list pipelines, JSON errors and fixes, JSON to TypeScript, Zod and Go types, diff, base64, IDs and UUIDs, timestamps, JWT decoding and verification, URL encoding, hashes and HMAC, slugs, text bytes, number bases, Unicode inspection, YAML/JSON/TOML, SQL and CSS formatting, CSV, .env, cron, string escaping, colors and contrast, QR codes and lorem ipsum. Pure functions, no DOM, no network.", "license": "MIT", "type": "module", "sideEffects": false, @@ -54,6 +54,10 @@ "types": "./dist/json/index.d.ts", "default": "./dist/json/index.js" }, + "./json-types": { + "types": "./dist/json-types/index.d.ts", + "default": "./dist/json-types/index.js" + }, "./jwt": { "types": "./dist/jwt/index.d.ts", "default": "./dist/jwt/index.js" @@ -78,6 +82,10 @@ "types": "./dist/random/index.d.ts", "default": "./dist/random/index.js" }, + "./share": { + "types": "./dist/share/index.d.ts", + "default": "./dist/share/index.js" + }, "./slug": { "types": "./dist/slug/index.d.ts", "default": "./dist/slug/index.js" @@ -161,7 +169,8 @@ "prettier": "3.9.10", "sql-formatter": "15.9.0", "typescript": "7.0.2", - "vitest": "5.0.1" + "vitest": "5.0.1", + "zod": "4.6.7" }, "repository": { "type": "git", @@ -190,6 +199,9 @@ "unicode", "jwt verify", "jwks", + "json to typescript", + "zod", + "go struct", "yaml", "toml", "sql formatter", diff --git a/packages/core/src/base64/hex.test.ts b/packages/core/src/base64/hex.test.ts new file mode 100644 index 0000000..3ad690e --- /dev/null +++ b/packages/core/src/base64/hex.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest' +import { formatHexRow, hexDump, hexDumpRows, hexRow, hexRowCount, HEX_ROW_BYTES } from './hex.ts' + +const ascending = (n: number) => Uint8Array.from({ length: n }, (_, i) => i & 0xff) + +describe('hexRow', () => { + it('formats a full row into offset, two groups and ascii', () => { + const bytes = new TextEncoder().encode('Hello, world! 0123') + const row = hexRow(bytes, 0) + expect(row.offset).toBe('00000000') + expect(row.hex).toEqual(['48 65 6c 6c 6f 2c 20 77', '6f 72 6c 64 21 20 30 31']) + expect(row.ascii).toBe('Hello, world! 01') + expect(row.length).toBe(16) + }) + + it('pads a partial last row so its columns line up', () => { + const bytes = ascending(16 + 3) + const row = hexRow(bytes, 1) + expect(row.offset).toBe('00000010') + expect(row.hex[0]).toBe('10 11 12'.padEnd(23, ' ')) + expect(row.hex[1]).toBe(' '.repeat(23)) + expect(row.ascii).toBe('...') + expect(row.length).toBe(3) + expect(formatHexRow(row)).toHaveLength(formatHexRow(hexRow(bytes, 0)).length - 13) + }) + + it('pads a row that ends inside the second group', () => { + const row = hexRow(ascending(10), 0) + expect(row.hex[1]).toBe('08 09'.padEnd(23, ' ')) + }) + + it('shows only printable ASCII and dots for the rest', () => { + const row = hexRow(new Uint8Array([0x00, 0x1f, 0x20, 0x41, 0x7e, 0x7f, 0x80, 0xff]), 0) + expect(row.ascii).toBe('.. A~...') + }) + + it('uses absolute offsets and widens past 32 bits', () => { + expect(hexRow(ascending(64), 3).offset).toBe('00000030') + // A row index past 4 GB: the offset is not truncated, the row is just empty. + const far = hexRow(new Uint8Array(0), 0x10000000) + expect(far.offset).toBe('100000000') + expect(far.length).toBe(0) + }) +}) + +describe('hexRowCount', () => { + it('rounds up', () => { + expect(hexRowCount(0)).toBe(0) + expect(hexRowCount(1)).toBe(1) + expect(hexRowCount(HEX_ROW_BYTES)).toBe(1) + expect(hexRowCount(HEX_ROW_BYTES + 1)).toBe(2) + }) +}) + +describe('hexDumpRows', () => { + it('joins a range of rows and clamps it to the data', () => { + const bytes = ascending(40) + expect(hexDumpRows(bytes, 1, 2)).toBe(formatHexRow(hexRow(bytes, 1))) + expect(hexDumpRows(bytes, 0, 99).split('\n')).toHaveLength(3) + expect(hexDumpRows(bytes, 5, 9)).toBe('') + }) + + it('builds the same dump in chunks as in one go', () => { + const bytes = ascending(1000) + const whole = hexDumpRows(bytes, 0, hexRowCount(bytes.length)) + const chunked = [hexDumpRows(bytes, 0, 20), hexDumpRows(bytes, 20, 40), hexDumpRows(bytes, 40, 63)].join('\n') + expect(chunked).toBe(whole) + }) +}) + +describe('hexDump', () => { + it('keeps the classic layout', () => { + expect(hexDump(new TextEncoder().encode('AB\n'))).toBe( + `00000000 41 42 0a${' '.repeat(15)} ${' '.repeat(23)} |AB.|`, + ) + }) + + it('caps the output and says how much is left', () => { + const lines = hexDump(ascending(100), 32).split('\n') + expect(lines).toHaveLength(3) + expect(lines[2]).toBe('… 68 more bytes') + }) + + it('is empty for no bytes, and only the note when the cap is zero', () => { + expect(hexDump(new Uint8Array(0))).toBe('') + expect(hexDump(ascending(5), 0)).toBe('… 5 more bytes') + }) +}) diff --git a/packages/core/src/base64/hex.ts b/packages/core/src/base64/hex.ts index 7056d06..78d8b64 100644 --- a/packages/core/src/base64/hex.ts +++ b/packages/core/src/base64/hex.ts @@ -1,19 +1,72 @@ -/** Compact hex dump, for the "this isn't UTF-8" escape hatch. */ -export function hexDump(bytes: Uint8Array, maxBytes = 4096): string { - const limit = Math.min(bytes.length, maxBytes) - const lines: string[] = [] +/** Bytes per hex-dump row, as in `xxd` and `hexdump -C`. */ +export const HEX_ROW_BYTES = 16 - for (let offset = 0; offset < limit; offset += 16) { - const row = bytes.subarray(offset, Math.min(offset + 16, limit)) - const hex = [...row].map((b) => b.toString(16).padStart(2, '0')) - const left = hex.slice(0, 8).join(' ').padEnd(23, ' ') - const right = hex.slice(8).join(' ').padEnd(23, ' ') - const ascii = [...row].map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : '.')).join('') - lines.push(`${offset.toString(16).padStart(8, '0')} ${left} ${right} |${ascii}|`) - } +/** One formatted row of a hex dump, split so a UI can style each column. */ +export interface HexRow { + /** Offset of the row's first byte: 8 hex digits, more past 4 GB. */ + offset: string + /** Bytes 0-7 and 8-15 as `00 11 22 …`, each padded to 23 columns so a short last row still lines up. */ + hex: [string, string] + /** Printable ASCII as itself, everything else as `.`. */ + ascii: string + /** How many bytes the row holds: 16, or fewer on the last row. */ + length: number +} + +const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0')) +const ASCII = Array.from({ length: 256 }, (_, i) => (i >= 0x20 && i < 0x7f ? String.fromCharCode(i) : '.')) +const GROUP_WIDTH = 23 - if (bytes.length > limit) { - lines.push(`… ${bytes.length - limit} more bytes`) +/** Rows needed for `byteLength` bytes. */ +export function hexRowCount(byteLength: number): number { + return Math.ceil(byteLength / HEX_ROW_BYTES) +} + +/** + * Row `rowIndex` of the dump of `bytes`. Cheap enough to call per visible row, + * so a virtualised view never formats more than it shows. + */ +export function hexRow(bytes: Uint8Array, rowIndex: number): HexRow { + const start = rowIndex * HEX_ROW_BYTES + const end = Math.min(start + HEX_ROW_BYTES, bytes.length) + let left = '' + let right = '' + let ascii = '' + for (let i = start; i < end; i++) { + const b = bytes[i]! + if (i - start < 8) left += left === '' ? HEX[b] : ` ${HEX[b]}` + else right += right === '' ? HEX[b] : ` ${HEX[b]}` + ascii += ASCII[b] + } + return { + offset: start.toString(16).padStart(8, '0'), + hex: [left.padEnd(GROUP_WIDTH, ' '), right.padEnd(GROUP_WIDTH, ' ')], + ascii, + length: Math.max(0, end - start), } +} + +/** A row as one line: `00000000 00 11 … 77 88 … ff |........|`. */ +export function formatHexRow(row: HexRow): string { + return `${row.offset} ${row.hex[0]} ${row.hex[1]} |${row.ascii}|` +} + +/** + * Rows `[fromRow, toRow)` as newline-joined lines, offsets absolute. Lets a + * caller build a dump of a large input chunk by chunk instead of as one string. + */ +export function hexDumpRows(bytes: Uint8Array, fromRow: number, toRow: number): string { + const end = Math.min(toRow, hexRowCount(bytes.length)) + const lines: string[] = [] + for (let r = Math.max(0, fromRow); r < end; r++) lines.push(formatHexRow(hexRow(bytes, r))) return lines.join('\n') } + +/** Compact hex dump, for the "this isn't UTF-8" escape hatch. */ +export function hexDump(bytes: Uint8Array, maxBytes = 4096): string { + const limit = Math.min(bytes.length, maxBytes) + const dump = hexDumpRows(bytes.subarray(0, limit), 0, hexRowCount(limit)) + if (bytes.length <= limit) return dump + const more = `… ${bytes.length - limit} more bytes` + return dump === '' ? more : `${dump}\n${more}` +} diff --git a/packages/core/src/base64/index.ts b/packages/core/src/base64/index.ts index f6badfc..6b2869f 100644 --- a/packages/core/src/base64/index.ts +++ b/packages/core/src/base64/index.ts @@ -3,5 +3,6 @@ export * from './hex.ts' export * from './magic.ts' export * from './sanitize.ts' export * from './snippets.ts' +export * from './strip.ts' export * from './svg.ts' export * from './tiers.ts' diff --git a/packages/core/src/base64/strip.test.ts b/packages/core/src/base64/strip.test.ts new file mode 100644 index 0000000..51c3e3e --- /dev/null +++ b/packages/core/src/base64/strip.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest' +import { standaloneSvg, summarizeRemovals, type Removal } from './strip.ts' +import { svgRoot } from './svg.ts' + +describe('summarizeRemovals', () => { + it('says so when nothing was removed', () => { + const report = summarizeRemovals([]) + expect(report).toEqual({ total: 0, dangerous: 0, items: [], summary: 'Nothing to strip' }) + }) + + it('groups handlers, counts script URLs and puts the dangerous ones first', () => { + const removed: Removal[] = [ + { kind: 'element', name: '#comment' }, + { kind: 'attribute', name: 'onload', element: 'svg' }, + { kind: 'element', name: 'script' }, + { kind: 'attribute', name: 'onclick', element: 'rect' }, + { kind: 'attribute', name: 'xlink:href', element: 'a', value: 'javascript:alert(1)' }, + { kind: 'element', name: 'foreignObject' }, + ] + const report = summarizeRemovals(removed) + expect(report.total).toBe(6) + expect(report.dangerous).toBe(5) + expect(report.summary).toBe('Stripped 1