diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..305884e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+target/
+frontend/node_modules/
+frontend/dist/
+frontend/src/wasm/
+.cursor/
+.DS_Store
+*.local
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8067220
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+.PHONY: wasm frontend dev build test clean
+
+wasm:
+ cd parser-wasm && wasm-pack build --target web --out-dir ../frontend/src/wasm
+
+frontend:
+ cd frontend && npm install
+
+dev: wasm frontend
+ cd frontend && npm run dev
+
+build: wasm
+ cd frontend && npm run build
+
+test:
+ cd parser-wasm && cargo test
+ cd frontend && npm install && npm test
+
+clean:
+ rm -rf frontend/src/wasm frontend/dist parser-wasm/target
diff --git a/README.md b/README.md
index 3adcbde..545b001 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,43 @@
# HexPigeon
+Paste a hex dump, describe the layout in JSON or YAML, get a field tree and a highlighted hex view.
+
+The parser is Rust compiled to WASM. The UI is Vue 3.
+
+## Layout
+
+```
+parser-wasm/ binary types, endianness, bitflags, strings, CRC
+frontend/ schema editor, hex view, parsed tree
+```
+
+Field types: `uint8/16/32/64`, `int8/16/32`, `float32/64`, `bitflags8/16`, `bytes`, `string_fixed`, `string_lp8`, `string_lp16`, `crc16`, `crc32`, `repeated`, `padding`.
+
+## Run
+
+Needs `wasm-pack`, a Rust toolchain with `wasm32-unknown-unknown`, and Node 22.
+
+```bash
+nix-shell # optional
+make test
+make dev
+```
+
+`make dev` builds the WASM module, installs frontend deps, and starts Vite on http://localhost:5173.
+
+## Schema
+
+```json
+{
+ "name": "Example Frame",
+ "endian": "big",
+ "fields": [
+ { "name": "magic", "type": "uint8" },
+ { "name": "len", "type": "uint8" },
+ { "name": "payload", "type": "string_fixed", "length": 5 },
+ { "name": "checksum", "type": "crc32", "checksum_range": [0, 11] }
+ ]
+}
+```
+
+Hex input is whitespace-insensitive. Hovering a field highlights its bytes; CRC fields report valid/invalid when `checksum_range` is set.
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..425c615
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ HexPigeon — Binary Protocol Dissector
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..07ee279
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2351 @@
+{
+ "name": "hex-pigeon-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hex-pigeon-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "@codemirror/lang-json": "^6.0.1",
+ "@codemirror/state": "^6.4.1",
+ "@codemirror/theme-one-dark": "^6.1.2",
+ "@codemirror/view": "^6.26.3",
+ "js-yaml": "^4.1.0",
+ "pinia": "^2.1.7",
+ "vue": "^3.4.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.4",
+ "typescript": "^5.4.5",
+ "vite": "^5.2.11",
+ "vite-plugin-top-level-await": "^1.4.4",
+ "vite-plugin-wasm": "^3.3.0",
+ "vitest": "^2.1.8",
+ "vue-tsc": "^2.0.13"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@codemirror/lang-json": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
+ "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/json": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.12.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
+ "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.2.tgz",
+ "integrity": "sha512-U3RiPX62Wl/Gx4ftQ7UxLlloSfsFTQqKa+7vFBYteZGCzkp6oBqcsD7iSwniWRGobCAmDcwZSY+Six3+3ztdfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/theme-one-dark": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
+ "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.43.11",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.11.tgz",
+ "integrity": "sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.7.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
+ "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
+ "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/json": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
+ "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
+ "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz",
+ "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@rollup/plugin-virtual": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
+ "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz",
+ "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz",
+ "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
+ "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz",
+ "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz",
+ "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz",
+ "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz",
+ "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz",
+ "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz",
+ "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz",
+ "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz",
+ "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz",
+ "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz",
+ "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz",
+ "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz",
+ "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz",
+ "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz",
+ "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz",
+ "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz",
+ "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz",
+ "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz",
+ "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz",
+ "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz",
+ "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz",
+ "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
+ "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz",
+ "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.28"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.16.1",
+ "@swc/core-darwin-x64": "1.16.1",
+ "@swc/core-linux-arm-gnueabihf": "1.16.1",
+ "@swc/core-linux-arm64-gnu": "1.16.1",
+ "@swc/core-linux-arm64-musl": "1.16.1",
+ "@swc/core-linux-ppc64-gnu": "1.16.1",
+ "@swc/core-linux-s390x-gnu": "1.16.1",
+ "@swc/core-linux-x64-gnu": "1.16.1",
+ "@swc/core-linux-x64-musl": "1.16.1",
+ "@swc/core-win32-arm64-msvc": "1.16.1",
+ "@swc/core-win32-ia32-msvc": "1.16.1",
+ "@swc/core-win32-x64-msvc": "1.16.1"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz",
+ "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz",
+ "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz",
+ "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz",
+ "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz",
+ "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-ppc64-gnu": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz",
+ "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-s390x-gnu": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz",
+ "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz",
+ "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz",
+ "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz",
+ "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz",
+ "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz",
+ "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.28",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz",
+ "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@swc/wasm": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.16.1.tgz",
+ "integrity": "sha512-CHDxeM0GJQyYbS/sxTgJQl0Io+yPSMwptKIt8nQA9e3yvcLphhsLBOzhDaUEGBIkP8DRAOSnDvW2d65CrZ3FjA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
+ "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
+ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
+ "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
+ "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "2.1.9",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
+ "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
+ "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
+ "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
+ "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/shared": "3.5.42",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
+ "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.42",
+ "@vue/shared": "3.5.42"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
+ "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/compiler-core": "3.5.42",
+ "@vue/compiler-dom": "3.5.42",
+ "@vue/compiler-ssr": "3.5.42",
+ "@vue/shared": "3.5.42",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.19",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
+ "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.42",
+ "@vue/shared": "3.5.42"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
+ "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.42"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
+ "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.42",
+ "@vue/shared": "3.5.42"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
+ "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.42",
+ "@vue/runtime-core": "3.5.42",
+ "@vue/shared": "3.5.42",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
+ "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.42",
+ "@vue/runtime-dom": "3.5.42",
+ "@vue/shared": "3.5.42"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
+ "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
+ "license": "MIT"
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/crelt": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
+ "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/pinia": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.3",
+ "vue-demi": "^0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
+ "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.1",
+ "@rollup/rollup-android-arm64": "4.63.1",
+ "@rollup/rollup-darwin-arm64": "4.63.1",
+ "@rollup/rollup-darwin-x64": "4.63.1",
+ "@rollup/rollup-freebsd-arm64": "4.63.1",
+ "@rollup/rollup-freebsd-x64": "4.63.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.1",
+ "@rollup/rollup-linux-arm64-musl": "4.63.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.1",
+ "@rollup/rollup-linux-loong64-musl": "4.63.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.1",
+ "@rollup/rollup-linux-x64-gnu": "4.63.1",
+ "@rollup/rollup-linux-x64-musl": "4.63.1",
+ "@rollup/rollup-openbsd-x64": "4.63.1",
+ "@rollup/rollup-openharmony-arm64": "4.63.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.1",
+ "@rollup/rollup-win32-x64-gnu": "4.63.1",
+ "@rollup/rollup-win32-x64-msvc": "4.63.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
+ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
+ "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.7",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-plugin-top-level-await": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz",
+ "integrity": "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/plugin-virtual": "^3.0.2",
+ "@swc/core": "^1.12.14",
+ "@swc/wasm": "^1.12.14",
+ "uuid": "10.0.0"
+ },
+ "peerDependencies": {
+ "vite": ">=2.8"
+ }
+ },
+ "node_modules/vite-plugin-wasm": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz",
+ "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
+ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.9",
+ "@vitest/mocker": "2.1.9",
+ "@vitest/pretty-format": "^2.1.9",
+ "@vitest/runner": "2.1.9",
+ "@vitest/snapshot": "2.1.9",
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.9",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.9",
+ "@vitest/ui": "2.1.9",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+ "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
+ "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.42",
+ "@vue/compiler-sfc": "3.5.42",
+ "@vue/runtime-dom": "3.5.42",
+ "@vue/server-renderer": "3.5.42",
+ "@vue/shared": "3.5.42"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..13948db
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "hex-pigeon-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build",
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "build:wasm": "cd ../parser-wasm && wasm-pack build --target web --out-dir ../frontend/src/wasm"
+ },
+ "dependencies": {
+ "vue": "^3.4.0",
+ "pinia": "^2.1.7",
+ "@codemirror/view": "^6.26.3",
+ "@codemirror/state": "^6.4.1",
+ "@codemirror/lang-json": "^6.0.1",
+ "@codemirror/theme-one-dark": "^6.1.2",
+ "js-yaml": "^4.1.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.4",
+ "typescript": "^5.4.5",
+ "vite": "^5.2.11",
+ "vitest": "^2.1.8",
+ "vue-tsc": "^2.0.13",
+ "vite-plugin-wasm": "^3.3.0",
+ "vite-plugin-top-level-await": "^1.4.4"
+ }
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..e10ea5c
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+ {{ store.wasmError }}
+ Run make wasm then make dev.
+
+
+
+
+
+
+
+ {{ store.bytes.length }} bytes
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/HexInput.vue b/frontend/src/components/HexInput.vue
new file mode 100644
index 0000000..54dd0a2
--- /dev/null
+++ b/frontend/src/components/HexInput.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/HexViewer.vue b/frontend/src/components/HexViewer.vue
new file mode 100644
index 0000000..90b01ca
--- /dev/null
+++ b/frontend/src/components/HexViewer.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+ {{ row.offset.toString(16).padStart(4, '0') }}
+
+
+
+ {{ byte.toString(16).padStart(2, '0') }}
+
+
+
+
+
+
+ {{ toAscii(byte) }}
+
+
+
+
+
+
+ Selected:
+ {{ store.selectedField.name }}
+ offset {{ store.selectedField.offset }}, {{ store.selectedField.size }}B
+
+
+
+
+
diff --git a/frontend/src/components/PanelHeader.vue b/frontend/src/components/PanelHeader.vue
new file mode 100644
index 0000000..c50347f
--- /dev/null
+++ b/frontend/src/components/PanelHeader.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/frontend/src/components/ParsedNode.vue b/frontend/src/components/ParsedNode.vue
new file mode 100644
index 0000000..323ef91
--- /dev/null
+++ b/frontend/src/components/ParsedNode.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+ {{ kindLabel }}
+ {{ field.name }}
+
+
+
+
+ {{ flag.name }}
+
+
+
+ {{ (field.value as number[]).map(b => b.toString(16).padStart(2, '0')).join(' ') }}
+
+
+
+
+ {{ displayValue }}
+
+
+
+ +{{ field.offset }} / {{ field.size }}B
+
+ VALID
+ INVALID
+
+
+
+ {{ field.description }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ParsedTree.vue b/frontend/src/components/ParsedTree.vue
new file mode 100644
index 0000000..5f644fa
--- /dev/null
+++ b/frontend/src/components/ParsedTree.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/frontend/src/components/SchemaEditor.vue b/frontend/src/components/SchemaEditor.vue
new file mode 100644
index 0000000..5d2178f
--- /dev/null
+++ b/frontend/src/components/SchemaEditor.vue
@@ -0,0 +1,48 @@
+
+
+
+
Error: {{ schemaError }}
+
+
+
+
diff --git a/frontend/src/fixtures/example-schema.json b/frontend/src/fixtures/example-schema.json
new file mode 100644
index 0000000..8c82dcb
--- /dev/null
+++ b/frontend/src/fixtures/example-schema.json
@@ -0,0 +1,51 @@
+{
+ "name": "Example Frame",
+ "endian": "big",
+ "fields": [
+ {
+ "name": "magic",
+ "type": "uint8",
+ "description": "Protocol magic byte (0xFF)"
+ },
+ {
+ "name": "payload_len",
+ "type": "uint8",
+ "description": "Length of payload string"
+ },
+ {
+ "name": "payload",
+ "type": "string_fixed",
+ "length": 5,
+ "description": "Fixed-length ASCII payload"
+ },
+ {
+ "name": "raw_data",
+ "type": "bytes",
+ "length": 4,
+ "description": "Raw 4-byte data blob"
+ },
+ {
+ "name": "tag",
+ "type": "string_fixed",
+ "length": 4,
+ "description": "ASCII tag (ABCD)"
+ },
+ {
+ "name": "checksum",
+ "type": "crc32",
+ "checksum_range": [0, 11],
+ "description": "CRC-32 over bytes 0-10"
+ },
+ {
+ "name": "flags",
+ "type": "bitflags16",
+ "description": "Status flags",
+ "flags": [
+ { "bit": 0, "name": "ACK", "description": "Acknowledge" },
+ { "bit": 1, "name": "SYN", "description": "Synchronise" },
+ { "bit": 2, "name": "FIN", "description": "Finished" },
+ { "bit": 7, "name": "ERR", "description": "Error" }
+ ]
+ }
+ ]
+}
diff --git a/frontend/src/fixtures/example.ts b/frontend/src/fixtures/example.ts
new file mode 100644
index 0000000..d61e1eb
--- /dev/null
+++ b/frontend/src/fixtures/example.ts
@@ -0,0 +1,64 @@
+export const EXAMPLE_SCHEMA = JSON.stringify(
+ {
+ name: 'Example Frame',
+ endian: 'big',
+ fields: [
+ {
+ name: 'magic',
+ type: 'uint8',
+ description: 'Protocol magic byte (0xFF)',
+ },
+ {
+ name: 'payload_len',
+ type: 'uint8',
+ description: 'Length of payload string',
+ },
+ {
+ name: 'payload',
+ type: 'string_fixed',
+ length: 5,
+ description: 'Fixed-length ASCII payload',
+ },
+ {
+ name: 'raw_data',
+ type: 'bytes',
+ length: 4,
+ description: 'Raw 4-byte data blob',
+ },
+ {
+ name: 'tag',
+ type: 'string_fixed',
+ length: 4,
+ description: 'ASCII tag (ABCD)',
+ },
+ {
+ name: 'checksum',
+ type: 'crc32',
+ checksum_range: [0, 11],
+ description: 'CRC-32 over bytes 0-10',
+ },
+ {
+ name: 'flags',
+ type: 'bitflags16',
+ description: 'Status flags',
+ flags: [
+ { bit: 0, name: 'ACK', description: 'Acknowledge' },
+ { bit: 1, name: 'SYN', description: 'Synchronise' },
+ { bit: 2, name: 'FIN', description: 'Finished' },
+ { bit: 7, name: 'ERR', description: 'Error' },
+ ],
+ },
+ ],
+ },
+ null,
+ 2,
+)
+
+/** Example frame with valid CRC-32 (ISO HDLC) over bytes 0-10 and flags ACK|SYN. */
+export const EXAMPLE_HEX = [
+ 'FF 05 48 65 6C 6C 6F',
+ '00 01 02 03',
+ '41 42 43 44',
+ 'E8 6B B6 E1',
+ '00 03',
+].join('\n')
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
new file mode 100644
index 0000000..dcc6d11
--- /dev/null
+++ b/frontend/src/main.ts
@@ -0,0 +1,8 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import App from './App.vue'
+import './styles/main.css'
+
+const app = createApp(App)
+app.use(createPinia())
+app.mount('#app')
diff --git a/frontend/src/stores/parser.test.ts b/frontend/src/stores/parser.test.ts
new file mode 100644
index 0000000..d9edbab
--- /dev/null
+++ b/frontend/src/stores/parser.test.ts
@@ -0,0 +1,62 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { createPinia, setActivePinia } from 'pinia'
+import { useParserStore } from '@/stores/parser'
+import { EXAMPLE_HEX } from '@/fixtures/example'
+import type { ParseResult } from '@/types/parser'
+
+describe('useParserStore', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ })
+
+ it('derives bytes from hex input', () => {
+ const store = useParserStore()
+ store.hexInput = 'FF 00 AB'
+ expect(store.bytes).toEqual([0xFF, 0x00, 0xAB])
+ })
+
+ it('loads example fixture with valid byte length', () => {
+ const store = useParserStore()
+ store.hexInput = EXAMPLE_HEX
+ expect(store.bytes.length).toBe(21)
+ })
+
+ it('reports parse error when WASM is unavailable', () => {
+ const store = useParserStore()
+ store.runParse()
+ expect(store.parseError).toBe('WASM module not ready.')
+ })
+
+ it('stores parse result from WASM module', () => {
+ const store = useParserStore()
+ const result: ParseResult = {
+ fields: [],
+ total_bytes: 4,
+ consumed_bytes: 4,
+ }
+
+ store.wasmReady = true
+ store.wasm = {
+ parse: vi.fn().mockReturnValue(result),
+ hex_to_bytes_js: vi.fn(),
+ }
+
+ store.runParse()
+ expect(store.parseResult).toEqual(result)
+ expect(store.parseError).toBeNull()
+ })
+
+ it('captures invalid JSON schema errors', () => {
+ const store = useParserStore()
+ store.wasmReady = true
+ store.wasm = {
+ parse: vi.fn(),
+ hex_to_bytes_js: vi.fn(),
+ }
+ store.schemaText = '{bad json'
+
+ store.runParse()
+ expect(store.parseError).toMatch(/JSON schema error/)
+ expect(store.parseResult).toBeNull()
+ })
+})
diff --git a/frontend/src/stores/parser.ts b/frontend/src/stores/parser.ts
new file mode 100644
index 0000000..94b4552
--- /dev/null
+++ b/frontend/src/stores/parser.ts
@@ -0,0 +1,100 @@
+import { defineStore } from 'pinia'
+import { ref, computed, shallowRef } from 'vue'
+import type { ParseResult, ParsedField } from '@/types/parser'
+import { parseHexInput } from '@/utils/hex'
+import { validateJsonSchema } from '@/utils/schema'
+import { EXAMPLE_HEX, EXAMPLE_SCHEMA } from '@/fixtures/example'
+
+interface WasmModule {
+ parse: (hex: string, schema: string) => ParseResult
+ hex_to_bytes_js: (hex: string) => Uint8Array
+}
+
+export const useParserStore = defineStore('parser', () => {
+ // ── State ──────────────────────────────────────────────────────────────
+ const wasm = shallowRef(null)
+ const wasmReady = ref(false)
+ const wasmError = ref(null)
+
+ const hexInput = ref(EXAMPLE_HEX)
+ const schemaText = ref(EXAMPLE_SCHEMA)
+ const schemaFormat = ref<'json' | 'yaml'>('json')
+
+ const parseResult = ref(null)
+ const parseError = ref(null)
+
+ // Highlighted byte range [start, end) for hover sync.
+ const hoveredRange = ref<[number, number] | null>(null)
+ // Clicked / selected field for persistent highlight.
+ const selectedField = ref(null)
+
+ // ── WASM init ──────────────────────────────────────────────────────────
+ async function initWasm() {
+ try {
+ // Dynamic import — wasm-pack output directory resolved at build time.
+ const mod = await import('../wasm/hex_pigeon_parser.js')
+ await mod.default() // run wasm-bindgen init
+ wasm.value = mod as unknown as WasmModule
+ wasmReady.value = true
+ } catch (e) {
+ wasmError.value = `Failed to load WASM: ${e}`
+ }
+ }
+
+ // ── Actions ────────────────────────────────────────────────────────────
+ function runParse() {
+ parseError.value = null
+ parseResult.value = null
+
+ if (!wasmReady.value || !wasm.value) {
+ parseError.value = 'WASM module not ready.'
+ return
+ }
+
+ let schemaJson = schemaText.value
+ if (schemaFormat.value === 'yaml') {
+ try {
+ // @ts-ignore – dynamic import resolved at runtime
+ const yaml = (window as any).__yaml
+ schemaJson = JSON.stringify(yaml.load(schemaText.value))
+ } catch (e) {
+ parseError.value = `YAML parse error: ${e}`
+ return
+ }
+ } else {
+ const err = validateJsonSchema(schemaJson)
+ if (err) {
+ parseError.value = `JSON schema error: ${err}`
+ return
+ }
+ }
+
+ try {
+ const result = wasm.value.parse(hexInput.value, schemaJson)
+ parseResult.value = result
+ } catch (e) {
+ parseError.value = String(e)
+ }
+ }
+
+ const bytes = computed(() => parseHexInput(hexInput.value))
+
+ function setHovered(range: [number, number] | null) {
+ hoveredRange.value = range
+ }
+
+ function setSelected(field: ParsedField | null) {
+ selectedField.value = field
+ }
+
+ return {
+ wasm, wasmReady, wasmError,
+ hexInput, schemaText, schemaFormat,
+ parseResult, parseError,
+ hoveredRange, selectedField,
+ bytes,
+ initWasm, runParse, setHovered, setSelected,
+ }
+})
+
+export { EXAMPLE_HEX, EXAMPLE_SCHEMA }
diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css
new file mode 100644
index 0000000..1e9df1a
--- /dev/null
+++ b/frontend/src/styles/main.css
@@ -0,0 +1,227 @@
+/* ── Reset & tokens ────────────────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+:root {
+ --bg0: #0d1117;
+ --bg1: #161b22;
+ --bg2: #21262d;
+ --bg3: #30363d;
+ --border: #30363d;
+ --text: #e6edf3;
+ --text-muted: #7d8590;
+ --accent: #58a6ff;
+ --accent2: #3fb950;
+ --warn: #f85149;
+ --yellow: #e3b341;
+ --purple: #bc8cff;
+ --orange: #ffa657;
+
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
+ --font-ui: system-ui, -apple-system, 'Segoe UI', sans-serif;
+ --radius: 6px;
+}
+
+html, body { height: 100%; background: var(--bg0); color: var(--text); font-family: var(--font-ui); font-size: 14px; }
+
+/* ── App shell ──────────────────────────────────────────────────────────── */
+.app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
+
+/* ── Header ────────────────────────────────────────────────────────────── */
+.app-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 10px 16px;
+ background: var(--bg1);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.brand { display: flex; align-items: baseline; gap: 12px; }
+.logo {
+ font-size: 1rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--text);
+}
+.subtitle { color: var(--text-muted); font-size: 0.82rem; }
+.header-actions { display: flex; gap: 8px; }
+
+/* ── Buttons ────────────────────────────────────────────────────────────── */
+.btn { padding: 6px 16px; border-radius: var(--radius); border: none; cursor: pointer; font-weight: 600; transition: opacity 0.15s; }
+.btn:disabled { opacity: 0.4; cursor: not-allowed; }
+.btn-primary { background: var(--bg3); color: var(--text); border: 1px solid var(--border); }
+.btn-primary:not(:disabled):hover { border-color: var(--accent); color: var(--accent); }
+
+/* ── Banners ────────────────────────────────────────────────────────────── */
+.banner { padding: 8px 16px; font-size: 0.85rem; }
+.banner-error { background: #2d1a1a; color: var(--warn); border-bottom: 1px solid var(--warn); }
+.banner code { background: rgba(255,255,255,0.08); padding: 0 4px; border-radius: 3px; }
+
+/* ── Panels ─────────────────────────────────────────────────────────────── */
+.panels { display: grid; grid-template-columns: 320px 1fr 340px; flex: 1; overflow: hidden; }
+
+.panel { display: flex; flex-direction: column; overflow: hidden; border-right: 1px solid var(--border); }
+.panel:last-child { border-right: none; }
+
+.panel-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 6px 12px;
+ background: var(--bg2);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.panel-title {
+ font-weight: 600;
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+}
+.byte-count { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); }
+
+.format-select {
+ background: var(--bg3); color: var(--text); border: 1px solid var(--border);
+ border-radius: var(--radius); padding: 2px 6px; font-size: 0.8rem;
+}
+
+/* ── Schema editor ───────────────────────────────────────────────────────── */
+.schema-editor { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
+.schema-textarea {
+ flex: 1; resize: none; font-family: var(--font-mono); font-size: 0.8rem;
+ background: var(--bg1); color: var(--text); border: none; outline: none;
+ padding: 12px; line-height: 1.6; tab-size: 2;
+}
+.schema-error { padding: 6px 12px; background: #2d1a1a; color: var(--warn); font-size: 0.8rem; flex-shrink: 0; }
+
+/* ── Hex input ───────────────────────────────────────────────────────────── */
+.hex-input-wrapper { flex-shrink: 0; border-bottom: 1px solid var(--border); }
+.hex-input {
+ width: 100%; resize: none; font-family: var(--font-mono); font-size: 0.85rem;
+ background: var(--bg2); color: var(--accent); border: none; outline: none;
+ padding: 10px 12px; line-height: 1.7;
+}
+
+/* ── Hex viewer ──────────────────────────────────────────────────────────── */
+.hex-viewer { flex: 1; overflow-y: auto; padding: 8px 0; }
+
+.hex-grid {
+ display: grid;
+ grid-template-columns: 50px 1fr auto;
+ gap: 0;
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ line-height: 1.9;
+}
+
+.hex-offset { color: var(--text-muted); padding: 0 8px; user-select: none; }
+
+.hex-bytes { display: flex; flex-wrap: wrap; gap: 2px; padding: 0 4px; }
+
+.hex-byte {
+ display: inline-block;
+ width: 22px; text-align: center;
+ cursor: pointer;
+ border-radius: 3px;
+ transition: background 0.08s;
+ padding: 0 1px;
+}
+.hex-pad { color: transparent; }
+
+.hex-ascii { color: var(--text-muted); padding: 0 8px; user-select: none; letter-spacing: 0.05em; }
+.ascii-char { cursor: pointer; border-radius: 2px; }
+
+/* ── Byte states ─────────────────────────────────────────────────────────── */
+.byte-parsed { color: var(--text); }
+.byte-hovered { background: rgba(88,166,255,0.25); color: #fff; }
+.byte-selected { background: var(--accent); color: #000 !important; border-radius: 3px; }
+.byte-error { color: var(--warn); }
+.byte-ok { color: var(--accent2); }
+
+/* ── Hex legend ─────────────────────────────────────────────────────────── */
+.hex-legend {
+ display: flex; align-items: center; gap: 6px;
+ padding: 6px 12px; font-size: 0.78rem; color: var(--text-muted);
+ border-top: 1px solid var(--border); flex-shrink: 0;
+}
+.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
+.legend-dot.selected { background: var(--accent); }
+
+/* ── Parsed tree ────────────────────────────────────────────────────────── */
+.parsed-tree { flex: 1; overflow-y: auto; }
+
+.parsed-node {
+ display: flex; align-items: center; gap: 6px;
+ padding: 3px 8px; cursor: pointer;
+ border-left: 2px solid transparent;
+ transition: background 0.08s;
+ font-size: 0.82rem;
+}
+.parsed-node:hover { background: var(--bg2); }
+.node-selected { background: rgba(88,166,255,0.12) !important; border-left-color: var(--accent); }
+.node-hovered { background: rgba(88,166,255,0.07); }
+.node-error { border-left-color: var(--warn); }
+.node-ok { border-left-color: var(--accent2); }
+
+.expand-btn {
+ background: none; border: none; cursor: pointer;
+ width: 14px; height: 14px; flex-shrink: 0;
+ display: inline-flex; align-items: center; justify-content: center;
+}
+.expand-icon {
+ display: block;
+ width: 0; height: 0;
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 6px solid var(--text-muted);
+ transition: transform 0.12s ease;
+}
+.expand-icon.expanded { transform: rotate(90deg); }
+.expand-spacer { display: inline-block; width: 14px; }
+
+.node-kind {
+ font-size: 0.68rem; font-family: var(--font-mono);
+ background: var(--bg3); color: var(--purple);
+ border-radius: 3px; padding: 1px 5px;
+ white-space: nowrap; flex-shrink: 0;
+}
+
+.node-name { font-weight: 600; color: var(--text); flex-shrink: 0; }
+
+.node-value { flex: 1; font-family: var(--font-mono); font-size: 0.78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.node-meta { color: var(--text-muted); font-family: var(--font-mono); font-size: 0.7rem; flex-shrink: 0; }
+
+.val-int { color: var(--orange); }
+.val-float { color: var(--yellow); }
+.val-string { color: var(--accent2); }
+.val-crc { color: var(--purple); }
+
+.node-desc {
+ font-size: 0.78rem; color: var(--text-muted);
+ padding: 2px 8px 4px;
+ font-style: italic;
+}
+
+/* ── Flag chips ─────────────────────────────────────────────────────────── */
+.flag-chip {
+ display: inline-block; font-size: 0.65rem; font-family: var(--font-mono);
+ padding: 1px 5px; border-radius: 3px; margin-right: 3px; font-weight: 700;
+}
+.flag-set { background: rgba(63,185,80,0.25); color: var(--accent2); }
+.flag-unset { background: var(--bg3); color: var(--text-muted); }
+
+.bytes-preview { color: var(--accent); font-size: 0.72rem; }
+
+/* ── Badges ─────────────────────────────────────────────────────────────── */
+.badge { font-size: 0.65rem; font-weight: 700; padding: 1px 6px; border-radius: 10px; flex-shrink: 0; }
+.badge-ok { background: rgba(63,185,80,0.2); color: var(--accent2); }
+.badge-err { background: rgba(248,81,73,0.2); color: var(--warn); }
+
+/* ── Empty hints ────────────────────────────────────────────────────────── */
+.empty-hint { padding: 24px 16px; color: var(--text-muted); text-align: center; }
+.parse-error { padding: 10px 14px; background: #2d1a1a; color: var(--warn); font-size: 0.82rem; border-bottom: 1px solid var(--warn); }
+
+/* ── Scrollbars ─────────────────────────────────────────────────────────── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--bg0); }
+::-webkit-scrollbar-thumb { background: var(--bg3); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
diff --git a/frontend/src/types/parser.ts b/frontend/src/types/parser.ts
new file mode 100644
index 0000000..aba080d
--- /dev/null
+++ b/frontend/src/types/parser.ts
@@ -0,0 +1,70 @@
+// ── Schema types (mirrors Rust schema.rs) ──────────────────────────────────
+
+export type Endian = 'big' | 'little'
+
+export type FieldKind =
+ | 'uint8' | 'uint16' | 'uint32' | 'uint64'
+ | 'int8' | 'int16' | 'int32'
+ | 'float32' | 'float64'
+ | 'bitflags8' | 'bitflags16'
+ | 'bytes' | 'string_fixed' | 'string_lp8' | 'string_lp16'
+ | 'crc16' | 'crc32'
+ | 'repeated' | 'padding'
+
+export interface FlagDef {
+ bit: number
+ name: string
+ description?: string
+}
+
+export type CountSpec = number | string
+
+export interface FieldDef {
+ name: string
+ type: FieldKind
+ endian?: Endian
+ description?: string
+ flags?: FlagDef[]
+ length?: number
+ checksum_range?: [number, number]
+ count?: CountSpec
+ fields?: FieldDef[]
+}
+
+export interface Schema {
+ name?: string
+ endian?: Endian
+ fields: FieldDef[]
+}
+
+// ── ParseResult types (mirrors Rust parser.rs) ─────────────────────────────
+
+export type ParsedValue =
+ | number
+ | string
+ | number[] // bytes
+ | FlagValue[]
+ | null
+
+export interface FlagValue {
+ bit: number
+ name: string
+ set: boolean
+}
+
+export interface ParsedField {
+ name: string
+ kind: string
+ offset: number
+ size: number
+ value: ParsedValue
+ description?: string
+ children?: ParsedField[]
+ checksum_valid?: boolean
+}
+
+export interface ParseResult {
+ fields: ParsedField[]
+ total_bytes: number
+ consumed_bytes: number
+}
diff --git a/frontend/src/utils/hex.test.ts b/frontend/src/utils/hex.test.ts
new file mode 100644
index 0000000..d9e1901
--- /dev/null
+++ b/frontend/src/utils/hex.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from 'vitest'
+import { parseHexInput, formatHexBytes } from '@/utils/hex'
+
+describe('parseHexInput', () => {
+ it('parses spaced hex pairs', () => {
+ expect(parseHexInput('FF 05 48 65')).toEqual([0xFF, 0x05, 0x48, 0x65])
+ })
+
+ it('ignores newlines and tabs', () => {
+ expect(parseHexInput('FF\n05\t48')).toEqual([0xFF, 0x05, 0x48])
+ })
+
+ it('returns empty array for odd-length input', () => {
+ expect(parseHexInput('ABC')).toEqual([])
+ })
+
+ it('returns empty array for invalid digits', () => {
+ expect(parseHexInput('GG')).toEqual([])
+ })
+})
+
+describe('formatHexBytes', () => {
+ it('formats bytes as uppercase hex', () => {
+ expect(formatHexBytes([0x0a, 0xff])).toBe('0A FF')
+ })
+})
diff --git a/frontend/src/utils/hex.ts b/frontend/src/utils/hex.ts
new file mode 100644
index 0000000..3f635ef
--- /dev/null
+++ b/frontend/src/utils/hex.ts
@@ -0,0 +1,17 @@
+/** Parse a hex dump string into byte values. Whitespace is ignored. */
+export function parseHexInput(hex: string): number[] {
+ const clean = hex.replace(/\s/g, '')
+ if (clean.length % 2 !== 0) return []
+ const out: number[] = []
+ for (let i = 0; i < clean.length; i += 2) {
+ const b = parseInt(clean.slice(i, i + 2), 16)
+ if (Number.isNaN(b)) return []
+ out.push(b)
+ }
+ return out
+}
+
+/** Format bytes as uppercase hex pairs separated by spaces. */
+export function formatHexBytes(bytes: number[]): string {
+ return bytes.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')
+}
diff --git a/frontend/src/utils/schema.test.ts b/frontend/src/utils/schema.test.ts
new file mode 100644
index 0000000..c50dedd
--- /dev/null
+++ b/frontend/src/utils/schema.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest'
+import { validateJsonSchema } from '@/utils/schema'
+
+describe('validateJsonSchema', () => {
+ it('accepts valid JSON', () => {
+ expect(validateJsonSchema('{"fields":[]}')).toBeNull()
+ })
+
+ it('rejects invalid JSON', () => {
+ const err = validateJsonSchema('{invalid')
+ expect(err).not.toBeNull()
+ })
+})
diff --git a/frontend/src/utils/schema.ts b/frontend/src/utils/schema.ts
new file mode 100644
index 0000000..60c581e
--- /dev/null
+++ b/frontend/src/utils/schema.ts
@@ -0,0 +1,9 @@
+/** Validate JSON schema text. Returns an error message or null if valid. */
+export function validateJsonSchema(text: string): string | null {
+ try {
+ JSON.parse(text)
+ return null
+ } catch (e) {
+ return String(e)
+ }
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..a8879a1
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "moduleResolution": "bundler",
+ "strict": true,
+ "jsx": "preserve",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..ecaae2e
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import wasm from 'vite-plugin-wasm'
+import topLevelAwait from 'vite-plugin-top-level-await'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+ plugins: [vue(), wasm(), topLevelAwait()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ server: {
+ port: 5173,
+ fs: { allow: ['..'] },
+ },
+})
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 0000000..711735a
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vitest/config'
+import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ test: {
+ environment: 'node',
+ include: ['src/**/*.test.ts'],
+ },
+})
diff --git a/parser-wasm/Cargo.lock b/parser-wasm/Cargo.lock
new file mode 100644
index 0000000..b17e648
--- /dev/null
+++ b/parser-wasm/Cargo.lock
@@ -0,0 +1,263 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "hex-pigeon-parser"
+version = "0.1.0"
+dependencies = [
+ "crc",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/parser-wasm/Cargo.toml b/parser-wasm/Cargo.toml
new file mode 100644
index 0000000..b66feab
--- /dev/null
+++ b/parser-wasm/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "hex-pigeon-parser"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+wasm-bindgen = "0.2"
+serde = { version = "1.0", features = ["derive"] }
+serde-wasm-bindgen = "0.6"
+serde_json = "1.0"
+crc = "3.2"
+
+[profile.release]
+opt-level = "s"
+lto = true
diff --git a/parser-wasm/src/checksum.rs b/parser-wasm/src/checksum.rs
new file mode 100644
index 0000000..4ad10aa
--- /dev/null
+++ b/parser-wasm/src/checksum.rs
@@ -0,0 +1,37 @@
+use crc::{Crc, CRC_16_IBM_SDLC, CRC_32_ISO_HDLC};
+
+pub fn compute_crc16(data: &[u8]) -> u16 {
+ let crc = Crc::::new(&CRC_16_IBM_SDLC);
+ crc.checksum(data)
+}
+
+pub fn compute_crc32(data: &[u8]) -> u32 {
+ let crc = Crc::::new(&CRC_32_ISO_HDLC);
+ crc.checksum(data)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn crc16_is_deterministic() {
+ let a = compute_crc16(b"123456789");
+ let b = compute_crc16(b"123456789");
+ assert_eq!(a, b);
+ assert_ne!(a, compute_crc16(b"123456788"));
+ }
+
+ #[test]
+ fn crc32_is_deterministic() {
+ let a = compute_crc32(b"123456789");
+ let b = compute_crc32(b"123456789");
+ assert_eq!(a, b);
+ assert_ne!(a, compute_crc32(b"123456788"));
+ }
+
+ #[test]
+ fn empty_input_has_known_crc32() {
+ assert_eq!(compute_crc32(&[]), 0x0000_0000);
+ }
+}
diff --git a/parser-wasm/src/lib.rs b/parser-wasm/src/lib.rs
new file mode 100644
index 0000000..6602125
--- /dev/null
+++ b/parser-wasm/src/lib.rs
@@ -0,0 +1,67 @@
+use wasm_bindgen::prelude::*;
+
+mod parser;
+mod schema;
+pub mod checksum;
+
+pub use parser::*;
+pub use schema::*;
+
+/// Entry point: parse `hex_input` against `schema_json`.
+/// Returns a JSON string of `ParseResult`.
+#[wasm_bindgen]
+pub fn parse(hex_input: &str, schema_json: &str) -> Result {
+ let schema: schema::Schema = serde_json::from_str(schema_json)
+ .map_err(|e| JsValue::from_str(&format!("Schema error: {e}")))?;
+
+ let bytes = hex_to_bytes(hex_input)
+ .map_err(|e| JsValue::from_str(&e))?;
+
+ let result = parser::parse_fields(&bytes, &schema.fields)
+ .map_err(|e| JsValue::from_str(&e))?;
+
+ serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
+}
+
+/// Convert hex string (with optional spaces/newlines) to bytes.
+#[wasm_bindgen]
+pub fn hex_to_bytes_js(hex: &str) -> Result, JsValue> {
+ hex_to_bytes(hex).map_err(|e| JsValue::from_str(&e))
+}
+
+pub fn hex_to_bytes(hex: &str) -> Result, String> {
+ let clean: String = hex.chars().filter(|c| !c.is_whitespace()).collect();
+ if clean.len() % 2 != 0 {
+ return Err(format!("Hex string has odd length: {}", clean.len()));
+ }
+ (0..clean.len())
+ .step_by(2)
+ .map(|i| {
+ u8::from_str_radix(&clean[i..i + 2], 16)
+ .map_err(|_| format!("Invalid hex byte at position {i}: '{}'", &clean[i..i + 2]))
+ })
+ .collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::hex_to_bytes;
+
+ #[test]
+ fn hex_to_bytes_accepts_whitespace() {
+ let bytes = hex_to_bytes("FF 05\n48 65").expect("parse hex");
+ assert_eq!(bytes, vec![0xFF, 0x05, 0x48, 0x65]);
+ }
+
+ #[test]
+ fn hex_to_bytes_rejects_odd_length() {
+ let err = hex_to_bytes("ABC").unwrap_err();
+ assert!(err.contains("odd length"));
+ }
+
+ #[test]
+ fn hex_to_bytes_rejects_invalid_digits() {
+ let err = hex_to_bytes("GG").unwrap_err();
+ assert!(err.contains("Invalid hex byte"));
+ }
+}
diff --git a/parser-wasm/src/parser.rs b/parser-wasm/src/parser.rs
new file mode 100644
index 0000000..7102f80
--- /dev/null
+++ b/parser-wasm/src/parser.rs
@@ -0,0 +1,309 @@
+use std::collections::HashMap;
+use serde::{Deserialize, Serialize};
+use crate::schema::{CountSpec, Endian, FieldDef, FieldKind};
+use crate::checksum::{compute_crc16, compute_crc32};
+
+/// The result of parsing a full frame.
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ParseResult {
+ pub fields: Vec,
+ pub total_bytes: usize,
+ pub consumed_bytes: usize,
+}
+
+/// A parsed field with its byte span for highlighting.
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct ParsedField {
+ pub name: String,
+ pub kind: String,
+ /// Byte offset where this field starts.
+ pub offset: usize,
+ /// Number of bytes consumed.
+ pub size: usize,
+ /// Human-readable value.
+ pub value: ParsedValue,
+ pub description: Option,
+ /// For repeated/compound fields.
+ pub children: Option>,
+ /// For checksum fields: whether it matches the computed value.
+ pub checksum_valid: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(untagged)]
+pub enum ParsedValue {
+ Uint(u64),
+ Int(i64),
+ Float(f64),
+ Bytes(Vec),
+ Str(String),
+ Flags(Vec),
+ None,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct FlagValue {
+ pub bit: u8,
+ pub name: String,
+ pub set: bool,
+}
+
+struct Cursor<'a> {
+ data: &'a [u8],
+ pos: usize,
+}
+
+impl<'a> Cursor<'a> {
+ fn new(data: &'a [u8]) -> Self {
+ Self { data, pos: 0 }
+ }
+
+ fn remaining(&self) -> usize {
+ self.data.len().saturating_sub(self.pos)
+ }
+
+ fn read_bytes(&mut self, n: usize) -> Result<&[u8], String> {
+ if self.pos + n > self.data.len() {
+ return Err(format!(
+ "Not enough bytes at offset {}: need {n}, have {}",
+ self.pos,
+ self.remaining()
+ ));
+ }
+ let start = self.pos;
+ self.pos += n;
+ Ok(&self.data[start..start + n])
+ }
+
+ fn read_u8(&mut self) -> Result {
+ Ok(self.read_bytes(1)?[0])
+ }
+
+ fn read_u16(&mut self, endian: &Endian) -> Result {
+ let b = self.read_bytes(2)?;
+ Ok(match endian {
+ Endian::Big => u16::from_be_bytes([b[0], b[1]]),
+ Endian::Little => u16::from_le_bytes([b[0], b[1]]),
+ })
+ }
+
+ fn read_u32(&mut self, endian: &Endian) -> Result {
+ let b = self.read_bytes(4)?;
+ Ok(match endian {
+ Endian::Big => u32::from_be_bytes([b[0], b[1], b[2], b[3]]),
+ Endian::Little => u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
+ })
+ }
+
+ fn read_u64(&mut self, endian: &Endian) -> Result {
+ let b = self.read_bytes(8)?;
+ let arr: [u8; 8] = b.try_into().unwrap();
+ Ok(match endian {
+ Endian::Big => u64::from_be_bytes(arr),
+ Endian::Little => u64::from_le_bytes(arr),
+ })
+ }
+}
+
+pub fn parse_fields(data: &[u8], fields: &[FieldDef]) -> Result {
+ let mut cursor = Cursor::new(data);
+ let mut parsed = Vec::new();
+ // Track named uint fields for dynamic count resolution.
+ let mut named_uints: HashMap = HashMap::new();
+
+ parse_fields_inner(data, &mut cursor, fields, &mut parsed, &mut named_uints)?;
+
+ Ok(ParseResult {
+ fields: parsed,
+ total_bytes: data.len(),
+ consumed_bytes: cursor.pos,
+ })
+}
+
+fn parse_fields_inner(
+ full_data: &[u8],
+ cursor: &mut Cursor,
+ fields: &[FieldDef],
+ out: &mut Vec,
+ named_uints: &mut HashMap,
+) -> Result<(), String> {
+ // Default endian if not specified in field.
+ let global_endian = Endian::Big;
+
+ for field in fields {
+ let endian = field.endian.as_ref().unwrap_or(&global_endian);
+ let offset = cursor.pos;
+
+ let (value, size, children, checksum_valid) = match &field.kind {
+ FieldKind::Uint8 => {
+ let v = cursor.read_u8()? as u64;
+ named_uints.insert(field.name.clone(), v as usize);
+ (ParsedValue::Uint(v), 1, None, None)
+ }
+ FieldKind::Uint16 => {
+ let v = cursor.read_u16(endian)? as u64;
+ named_uints.insert(field.name.clone(), v as usize);
+ (ParsedValue::Uint(v), 2, None, None)
+ }
+ FieldKind::Uint32 => {
+ let v = cursor.read_u32(endian)? as u64;
+ named_uints.insert(field.name.clone(), v as usize);
+ (ParsedValue::Uint(v), 4, None, None)
+ }
+ FieldKind::Uint64 => {
+ let v = cursor.read_u64(endian)?;
+ (ParsedValue::Uint(v), 8, None, None)
+ }
+ FieldKind::Int8 => {
+ let v = cursor.read_u8()? as i8;
+ (ParsedValue::Int(v as i64), 1, None, None)
+ }
+ FieldKind::Int16 => {
+ let raw = cursor.read_u16(endian)?;
+ let v = raw as i16;
+ (ParsedValue::Int(v as i64), 2, None, None)
+ }
+ FieldKind::Int32 => {
+ let raw = cursor.read_u32(endian)?;
+ let v = raw as i32;
+ (ParsedValue::Int(v as i64), 4, None, None)
+ }
+ FieldKind::Float32 => {
+ let raw = cursor.read_u32(endian)?;
+ let v = f32::from_bits(raw) as f64;
+ (ParsedValue::Float(v), 4, None, None)
+ }
+ FieldKind::Float64 => {
+ let raw = cursor.read_u64(endian)?;
+ let v = f64::from_bits(raw);
+ (ParsedValue::Float(v), 8, None, None)
+ }
+ FieldKind::Bitflags8 => {
+ let byte = cursor.read_u8()?;
+ let flags = resolve_flags(byte as u64, field.flags.as_deref().unwrap_or(&[]));
+ (ParsedValue::Flags(flags), 1, None, None)
+ }
+ FieldKind::Bitflags16 => {
+ let raw = cursor.read_u16(endian)? as u64;
+ let flags = resolve_flags(raw, field.flags.as_deref().unwrap_or(&[]));
+ (ParsedValue::Flags(flags), 2, None, None)
+ }
+ FieldKind::Bytes => {
+ let len = field.length.ok_or_else(|| {
+ format!("Field '{}': 'bytes' type requires 'length'", field.name)
+ })?;
+ let b = cursor.read_bytes(len)?.to_vec();
+ (ParsedValue::Bytes(b), len, None, None)
+ }
+ FieldKind::Padding => {
+ let len = field.length.unwrap_or(1);
+ cursor.read_bytes(len)?;
+ (ParsedValue::None, len, None, None)
+ }
+ FieldKind::StringFixed => {
+ let len = field.length.ok_or_else(|| {
+ format!("Field '{}': 'string_fixed' requires 'length'", field.name)
+ })?;
+ let raw = cursor.read_bytes(len)?.to_vec();
+ let s = String::from_utf8_lossy(&raw)
+ .trim_end_matches('\0')
+ .to_string();
+ (ParsedValue::Str(s), len, None, None)
+ }
+ FieldKind::StringLp8 => {
+ let len = cursor.read_u8()? as usize;
+ let raw = cursor.read_bytes(len)?.to_vec();
+ let s = String::from_utf8_lossy(&raw).into_owned();
+ (ParsedValue::Str(s), 1 + len, None, None)
+ }
+ FieldKind::StringLp16 => {
+ let len = cursor.read_u16(endian)? as usize;
+ let raw = cursor.read_bytes(len)?.to_vec();
+ let s = String::from_utf8_lossy(&raw).into_owned();
+ (ParsedValue::Str(s), 2 + len, None, None)
+ }
+ FieldKind::Crc16 => {
+ let stored = cursor.read_u16(endian)?;
+ let valid = if let Some(range) = field.checksum_range {
+ let slice = full_data
+ .get(range[0]..range[1])
+ .ok_or_else(|| format!("CRC range {:?} out of bounds", range))?;
+ let computed = compute_crc16(slice);
+ Some(computed == stored)
+ } else {
+ None
+ };
+ (ParsedValue::Uint(stored as u64), 2, None, valid)
+ }
+ FieldKind::Crc32 => {
+ let stored = cursor.read_u32(endian)?;
+ let valid = if let Some(range) = field.checksum_range {
+ let slice = full_data
+ .get(range[0]..range[1])
+ .ok_or_else(|| format!("CRC range {:?} out of bounds", range))?;
+ let computed = compute_crc32(slice);
+ Some(computed == stored)
+ } else {
+ None
+ };
+ (ParsedValue::Uint(stored as u64), 4, None, valid)
+ }
+ FieldKind::Repeated => {
+ let inner_fields = field.fields.as_deref().ok_or_else(|| {
+ format!("Field '{}': 'repeated' requires 'fields'", field.name)
+ })?;
+ let count = match field.count.as_ref() {
+ Some(CountSpec::Fixed(n)) => *n,
+ Some(CountSpec::Field(name)) => *named_uints
+ .get(name)
+ .ok_or_else(|| format!("Count field '{}' not found", name))?,
+ None => {
+ return Err(format!(
+ "Field '{}': 'repeated' requires 'count'",
+ field.name
+ ))
+ }
+ };
+
+ let start = cursor.pos;
+ let mut children_out = Vec::new();
+ let mut child_named = named_uints.clone();
+ for _ in 0..count {
+ let mut iteration = Vec::new();
+ parse_fields_inner(
+ full_data,
+ cursor,
+ inner_fields,
+ &mut iteration,
+ &mut child_named,
+ )?;
+ children_out.extend(iteration);
+ }
+ let size = cursor.pos - start;
+ (ParsedValue::None, size, Some(children_out), None)
+ }
+ };
+
+ out.push(ParsedField {
+ name: field.name.clone(),
+ kind: format!("{:?}", field.kind),
+ offset,
+ size,
+ value,
+ description: field.description.clone(),
+ children,
+ checksum_valid,
+ });
+ }
+ Ok(())
+}
+
+fn resolve_flags(raw: u64, defs: &[crate::schema::FlagDef]) -> Vec {
+ defs.iter()
+ .map(|f| FlagValue {
+ bit: f.bit,
+ name: f.name.clone(),
+ set: (raw >> f.bit) & 1 == 1,
+ })
+ .collect()
+}
diff --git a/parser-wasm/src/schema.rs b/parser-wasm/src/schema.rs
new file mode 100644
index 0000000..ffdcbf9
--- /dev/null
+++ b/parser-wasm/src/schema.rs
@@ -0,0 +1,110 @@
+use serde::{Deserialize, Serialize};
+
+/// A full schema describing a binary frame layout.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Schema {
+ /// Human-readable name for this schema.
+ pub name: Option,
+ /// Endianness applied globally unless overridden per field.
+ #[serde(default = "default_endian")]
+ pub endian: Endian,
+ /// Ordered list of fields to parse.
+ pub fields: Vec,
+}
+
+fn default_endian() -> Endian {
+ Endian::Big
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum Endian {
+ Big,
+ Little,
+}
+
+/// A single field definition.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct FieldDef {
+ /// Display name.
+ pub name: String,
+ /// Field type.
+ #[serde(rename = "type")]
+ pub kind: FieldKind,
+ /// Override global endianness for this field.
+ pub endian: Option,
+ /// Description / comment shown in the tree.
+ pub description: Option,
+ /// For bitflags: the flag definitions.
+ pub flags: Option>,
+ /// For `bytes` type: number of bytes (required).
+ /// For `string_lp8` / `string_lp16`: omit (length is read from data).
+ /// For other types: derived from type width.
+ pub length: Option,
+ /// For `crc16` / `crc32`: byte range to checksum [start, end) relative to frame start.
+ pub checksum_range: Option<[usize; 2]>,
+ /// For `repeated`: how many repetitions (constant or from a previously parsed field name).
+ pub count: Option,
+ /// For `repeated`: the inner fields to repeat.
+ pub fields: Option>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum FieldKind {
+ /// 1 byte unsigned.
+ Uint8,
+ /// 2 bytes unsigned.
+ Uint16,
+ /// 4 bytes unsigned.
+ Uint32,
+ /// 8 bytes unsigned.
+ Uint64,
+ /// 1 byte signed.
+ Int8,
+ /// 2 bytes signed.
+ Int16,
+ /// 4 bytes signed.
+ Int32,
+ /// 4 bytes float.
+ Float32,
+ /// 8 bytes float.
+ Float64,
+ /// 1-byte bitfield with named flags.
+ Bitflags8,
+ /// 2-byte bitfield with named flags.
+ Bitflags16,
+ /// Raw bytes of `length`.
+ Bytes,
+ /// Null-terminated string of `length` bytes.
+ StringFixed,
+ /// Pascal-style: 1-byte length prefix, then UTF-8 bytes.
+ StringLp8,
+ /// Pascal-style: 2-byte length prefix (respects endian), then UTF-8 bytes.
+ StringLp16,
+ /// CRC-16/CCITT checksum (2 bytes).
+ Crc16,
+ /// CRC-32 checksum (4 bytes).
+ Crc32,
+ /// Repeat inner `fields` `count` times.
+ Repeated,
+ /// Skip / padding bytes.
+ Padding,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct FlagDef {
+ /// Bit index (0 = LSB).
+ pub bit: u8,
+ pub name: String,
+ pub description: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum CountSpec {
+ /// Literal count.
+ Fixed(usize),
+ /// Name of a previously parsed uint field holding the count.
+ Field(String),
+}
diff --git a/parser-wasm/tests/integration.rs b/parser-wasm/tests/integration.rs
new file mode 100644
index 0000000..a1d0188
--- /dev/null
+++ b/parser-wasm/tests/integration.rs
@@ -0,0 +1,297 @@
+use hex_pigeon_parser::checksum::{compute_crc16, compute_crc32};
+use hex_pigeon_parser::{
+ parse_fields, CountSpec, Endian, FieldDef, FieldKind, FlagDef, ParsedValue, Schema,
+};
+
+fn field(name: &str, kind: FieldKind) -> FieldDef {
+ FieldDef {
+ name: name.to_string(),
+ kind,
+ endian: None,
+ description: None,
+ flags: None,
+ length: None,
+ checksum_range: None,
+ count: None,
+ fields: None,
+ }
+}
+
+fn field_len(name: &str, kind: FieldKind, length: usize) -> FieldDef {
+ let mut f = field(name, kind);
+ f.length = Some(length);
+ f
+}
+
+#[test]
+fn schema_deserializes_from_json() {
+ let json = r#"{
+ "name": "Test",
+ "endian": "little",
+ "fields": [{ "name": "id", "type": "uint8" }]
+ }"#;
+ let schema: Schema = serde_json::from_str(json).expect("schema JSON");
+ assert_eq!(schema.name.as_deref(), Some("Test"));
+ assert_eq!(schema.endian, Endian::Little);
+ assert_eq!(schema.fields.len(), 1);
+ assert_eq!(schema.fields[0].kind, FieldKind::Uint8);
+}
+
+#[test]
+fn parses_integer_types_big_endian() {
+ let data = [
+ 0x12,
+ 0x12, 0x34,
+ 0x12, 0x34, 0x56, 0x78,
+ 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
+ 0xFE,
+ 0xFF, 0xF0,
+ 0x80, 0x00, 0x00, 0x01,
+ ];
+ let fields = vec![
+ field("u8", FieldKind::Uint8),
+ field("u16", FieldKind::Uint16),
+ field("u32", FieldKind::Uint32),
+ field("u64", FieldKind::Uint64),
+ field("i8", FieldKind::Int8),
+ field("i16", FieldKind::Int16),
+ field("i32", FieldKind::Int32),
+ ];
+ let result = parse_fields(&data, &fields).expect("parse");
+
+ assert_eq!(result.consumed_bytes, 22);
+ assert!(matches!(result.fields[0].value, ParsedValue::Uint(0x12)));
+ assert!(matches!(result.fields[1].value, ParsedValue::Uint(0x1234)));
+ assert!(matches!(result.fields[2].value, ParsedValue::Uint(0x12345678)));
+ assert!(matches!(result.fields[3].value, ParsedValue::Uint(0x123456789ABCDEF0)));
+ assert!(matches!(result.fields[4].value, ParsedValue::Int(-2)));
+ assert!(matches!(result.fields[5].value, ParsedValue::Int(-16)));
+ assert!(matches!(result.fields[6].value, ParsedValue::Int(-2147483647)));
+}
+
+#[test]
+fn parses_integer_types_little_endian() {
+ let data = [0x34, 0x12];
+ let mut f = field("u16", FieldKind::Uint16);
+ f.endian = Some(Endian::Little);
+ let result = parse_fields(&data, &[f]).expect("parse");
+ assert!(matches!(result.fields[0].value, ParsedValue::Uint(0x1234)));
+}
+
+#[test]
+fn parses_float_types() {
+ let f32_bits = 1.0f32.to_bits();
+ let f64_bits = 2.5f64.to_bits();
+ let mut data = Vec::new();
+ data.extend_from_slice(&f32_bits.to_be_bytes());
+ data.extend_from_slice(&f64_bits.to_be_bytes());
+
+ let fields = vec![field("f32", FieldKind::Float32), field("f64", FieldKind::Float64)];
+ let result = parse_fields(&data, &fields).expect("parse");
+
+ match &result.fields[0].value {
+ ParsedValue::Float(v) => assert!((v - 1.0).abs() < f64::EPSILON),
+ other => panic!("expected float32, got {other:?}"),
+ }
+ match &result.fields[1].value {
+ ParsedValue::Float(v) => assert!((v - 2.5).abs() < f64::EPSILON),
+ other => panic!("expected float64, got {other:?}"),
+ }
+}
+
+#[test]
+fn parses_bitflags() {
+ let mut f8 = field("flags8", FieldKind::Bitflags8);
+ f8.flags = Some(vec![
+ FlagDef {
+ bit: 0,
+ name: "A".into(),
+ description: None,
+ },
+ FlagDef {
+ bit: 3,
+ name: "D".into(),
+ description: None,
+ },
+ ]);
+ let mut f16 = field("flags16", FieldKind::Bitflags16);
+ f16.flags = Some(vec![FlagDef {
+ bit: 1,
+ name: "B".into(),
+ description: None,
+ }]);
+
+ let result = parse_fields(&[0b0000_0101, 0x00, 0b0000_0010], &[f8, f16]).expect("parse");
+
+ match &result.fields[0].value {
+ ParsedValue::Flags(flags) => {
+ assert!(flags.iter().find(|f| f.name == "A").unwrap().set);
+ assert!(!flags.iter().find(|f| f.name == "D").unwrap().set);
+ }
+ other => panic!("expected flags8, got {other:?}"),
+ }
+ match &result.fields[1].value {
+ ParsedValue::Flags(flags) => assert!(flags[0].set),
+ other => panic!("expected flags16, got {other:?}"),
+ }
+}
+
+#[test]
+fn parses_bytes_padding_and_strings() {
+ let data = [
+ 0xAA, 0xBB, 0xCC, // bytes[3]
+ 0x00, 0x00, // padding[2]
+ b'H', b'i', 0x00, 0x00, // string_fixed[4] -> "Hi"
+ 0x03, b'F', b'O', b'O', // string_lp8
+ 0x00, 0x02, b'B', b'R', // string_lp16 big-endian length
+ ];
+
+ let fields = vec![
+ field_len("raw", FieldKind::Bytes, 3),
+ field_len("pad", FieldKind::Padding, 2),
+ field_len("fixed", FieldKind::StringFixed, 4),
+ field("lp8", FieldKind::StringLp8),
+ field("lp16", FieldKind::StringLp16),
+ ];
+ let result = parse_fields(&data, &fields).expect("parse");
+
+ assert!(matches!(&result.fields[0].value, ParsedValue::Bytes(v) if v == &[0xAA, 0xBB, 0xCC]));
+ assert!(matches!(result.fields[1].value, ParsedValue::None));
+ assert!(matches!(&result.fields[2].value, ParsedValue::Str(s) if s == "Hi"));
+ assert!(matches!(&result.fields[3].value, ParsedValue::Str(s) if s == "FOO"));
+ assert!(matches!(&result.fields[4].value, ParsedValue::Str(s) if s == "BR"));
+}
+
+#[test]
+fn validates_crc16_and_crc32() {
+ let payload = [0x01, 0x02, 0x03, 0x04];
+ let crc16 = compute_crc16(&payload);
+ let crc32 = compute_crc32(&payload);
+
+ let mut data = payload.to_vec();
+ data.extend_from_slice(&crc16.to_be_bytes());
+ data.extend_from_slice(&crc32.to_be_bytes());
+
+ let mut crc16_field = field("c16", FieldKind::Crc16);
+ crc16_field.checksum_range = Some([0, 4]);
+ let mut crc32_field = field("c32", FieldKind::Crc32);
+ crc32_field.checksum_range = Some([0, 4]);
+
+ let fields = vec![
+ field_len("payload", FieldKind::Bytes, 4),
+ crc16_field,
+ crc32_field,
+ ];
+ let result = parse_fields(&data, &fields).expect("parse");
+ assert_eq!(result.fields[1].checksum_valid, Some(true));
+ assert_eq!(result.fields[2].checksum_valid, Some(true));
+
+ let mut bad = data.clone();
+ bad[6] ^= 0xFF;
+ let mut crc32_bad = field("c32", FieldKind::Crc32);
+ crc32_bad.checksum_range = Some([0, 4]);
+ let fields = vec![
+ field_len("payload", FieldKind::Bytes, 4),
+ crc32_bad,
+ ];
+ let result = parse_fields(&bad, &fields).expect("parse");
+ assert_eq!(result.fields[1].checksum_valid, Some(false));
+}
+
+#[test]
+fn parses_repeated_fixed_and_dynamic_count() {
+ let data = [0x02, 0x10, 0x20, 0x11, 0x21];
+
+ let repeated_fixed = FieldDef {
+ name: "items".into(),
+ kind: FieldKind::Repeated,
+ endian: None,
+ description: None,
+ flags: None,
+ length: None,
+ checksum_range: None,
+ count: Some(CountSpec::Fixed(2)),
+ fields: Some(vec![field("val", FieldKind::Uint8)]),
+ };
+ let result = parse_fields(&[0x10, 0x20, 0x11, 0x21], &[repeated_fixed]).expect("parse");
+ let children = result.fields[0].children.as_ref().expect("children");
+ assert_eq!(children.len(), 2);
+ assert!(matches!(children[0].value, ParsedValue::Uint(0x10)));
+ assert!(matches!(children[1].value, ParsedValue::Uint(0x20)));
+
+ let repeated_dynamic = FieldDef {
+ name: "list".into(),
+ kind: FieldKind::Repeated,
+ endian: None,
+ description: None,
+ flags: None,
+ length: None,
+ checksum_range: None,
+ count: Some(CountSpec::Field("count".into())),
+ fields: Some(vec![field("val", FieldKind::Uint8)]),
+ };
+ let result = parse_fields(&data, &[field("count", FieldKind::Uint8), repeated_dynamic]).expect("parse");
+ let children = result.fields[1].children.as_ref().expect("children");
+ assert_eq!(children.len(), 2);
+}
+
+#[test]
+fn errors_on_truncated_buffer() {
+ let result = parse_fields(&[0x01], &[field("u16", FieldKind::Uint16)]);
+ assert!(result.is_err());
+ assert!(result.unwrap_err().contains("Not enough bytes"));
+}
+
+#[test]
+fn errors_on_missing_required_field_options() {
+ let err = parse_fields(&[0x01], &[field("raw", FieldKind::Bytes)]).unwrap_err();
+ assert!(err.contains("requires 'length'"));
+
+ let repeated = FieldDef {
+ name: "r".into(),
+ kind: FieldKind::Repeated,
+ endian: None,
+ description: None,
+ flags: None,
+ length: None,
+ checksum_range: None,
+ count: Some(CountSpec::Fixed(1)),
+ fields: None,
+ };
+ let err = parse_fields(&[0x01], &[repeated]).unwrap_err();
+ assert!(err.contains("requires 'fields'"));
+}
+
+#[test]
+fn parses_example_protocol_frame() {
+ let header = [
+ 0xFF, 0x05, b'H', b'e', b'l', b'l', b'o', 0x00, 0x01, 0x02, 0x03, b'A', b'B', b'C',
+ b'D',
+ ];
+ let crc32 = compute_crc32(&header[..11]);
+ let mut data = header.to_vec();
+ data.extend_from_slice(&crc32.to_be_bytes());
+ data.extend_from_slice(&[0x00, 0x03]); // flags: ACK + SYN
+
+ let schema_json = include_str!("../../frontend/src/fixtures/example-schema.json");
+ let schema: Schema = serde_json::from_str(schema_json).expect("example schema");
+ let result = parse_fields(&data, &schema.fields).expect("example frame");
+
+ assert_eq!(result.total_bytes, data.len());
+ assert_eq!(result.consumed_bytes, data.len());
+ assert_eq!(result.fields.len(), 7);
+
+ assert!(matches!(result.fields[0].value, ParsedValue::Uint(0xFF)));
+ assert!(matches!(&result.fields[2].value, ParsedValue::Str(s) if s == "Hello"));
+ assert!(matches!(&result.fields[4].value, ParsedValue::Str(s) if s == "ABCD"));
+ assert_eq!(result.fields[5].checksum_valid, Some(true));
+
+ match &result.fields[6].value {
+ ParsedValue::Flags(flags) => {
+ assert!(flags.iter().find(|f| f.name == "ACK").unwrap().set);
+ assert!(flags.iter().find(|f| f.name == "SYN").unwrap().set);
+ assert!(!flags.iter().find(|f| f.name == "FIN").unwrap().set);
+ }
+ other => panic!("expected flags, got {other:?}"),
+ }
+}
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..5c33f4d
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,34 @@
+{ pkgs ? import {} }:
+
+pkgs.mkShell {
+ name = "hex-pigeon-dev";
+
+ buildInputs = with pkgs; [
+ # Rust + WASM toolchain
+ rustup
+ wasm-pack
+ binaryen # wasm-opt
+
+ # Node / frontend
+ nodejs_22
+
+ # Utilities
+ pkg-config
+ openssl
+ ];
+
+ shellHook = ''
+ export RUSTUP_HOME="$HOME/.rustup"
+ export CARGO_HOME="$HOME/.cargo"
+ export PATH="$CARGO_HOME/bin:$PATH"
+
+ # Install stable + wasm32 target on first run
+ rustup toolchain install stable 2>/dev/null || true
+ rustup default stable 2>/dev/null || true
+ rustup target add wasm32-unknown-unknown 2>/dev/null || true
+
+ echo "HexPigeon dev shell ready"
+ echo " • Build WASM : cd parser-wasm && wasm-pack build --target web --out-dir ../frontend/src/wasm"
+ echo " • Frontend : cd frontend && npm install && npm run dev"
+ '';
+}