From f07f9eb7b9f088b58dc10835c6769d5e76264890 Mon Sep 17 00:00:00 2001 From: George Stagg Date: Tue, 10 Mar 2026 10:34:30 +0000 Subject: [PATCH 1/7] Create Wasm library and demo --- ggsql-wasm/Cargo.toml | 7 +- ggsql-wasm/demo/build.mjs | 67 ++ ggsql-wasm/demo/package-lock.json | 1659 ++++++++++++++++++++++++++ ggsql-wasm/demo/package.json | 25 + ggsql-wasm/demo/src/context.ts | 57 + ggsql-wasm/demo/src/editor.ts | 179 +++ ggsql-wasm/demo/src/examples.ts | 214 ++++ ggsql-wasm/demo/src/index.html | 351 ++++++ ggsql-wasm/demo/src/main.ts | 199 +++ ggsql-wasm/demo/src/tableManager.ts | 94 ++ ggsql-wasm/demo/tsconfig.json | 17 + ggsql-wasm/library/build.mjs | 27 + ggsql-wasm/library/package-lock.json | 523 ++++++++ ggsql-wasm/library/package.json | 18 + ggsql-wasm/library/src/csv.ts | 181 +++ ggsql-wasm/library/src/index.ts | 22 + ggsql-wasm/library/src/parquet.ts | 158 +++ ggsql-wasm/library/tsconfig.json | 18 + ggsql-wasm/src/lib.rs | 274 ++++- src/naming.rs | 2 +- src/reader/data.rs | 6 +- src/reader/sqlite.rs | 12 + 22 files changed, 4092 insertions(+), 18 deletions(-) create mode 100644 ggsql-wasm/demo/build.mjs create mode 100644 ggsql-wasm/demo/package-lock.json create mode 100644 ggsql-wasm/demo/package.json create mode 100644 ggsql-wasm/demo/src/context.ts create mode 100644 ggsql-wasm/demo/src/editor.ts create mode 100644 ggsql-wasm/demo/src/examples.ts create mode 100644 ggsql-wasm/demo/src/index.html create mode 100644 ggsql-wasm/demo/src/main.ts create mode 100644 ggsql-wasm/demo/src/tableManager.ts create mode 100644 ggsql-wasm/demo/tsconfig.json create mode 100644 ggsql-wasm/library/build.mjs create mode 100644 ggsql-wasm/library/package-lock.json create mode 100644 ggsql-wasm/library/package.json create mode 100644 ggsql-wasm/library/src/csv.ts create mode 100644 ggsql-wasm/library/src/index.ts create mode 100644 ggsql-wasm/library/src/parquet.ts create mode 100644 ggsql-wasm/library/tsconfig.json diff --git a/ggsql-wasm/Cargo.toml b/ggsql-wasm/Cargo.toml index cc7bcf5b..6805a915 100644 --- a/ggsql-wasm/Cargo.toml +++ b/ggsql-wasm/Cargo.toml @@ -13,14 +13,17 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" js-sys = "0.3" csv = "1" -polars = { version = "0.52", default-features = false, features = ["sql", "dtype-full"] } -ggsql = { path = "../src", default-features = false, features = ["polars-sql", "vegalite"] } +polars = { version = "0.52", default-features = false, features = ["dtype-full"] } +ggsql = { path = "../src", default-features = false, features = ["vegalite", "sqlite", "builtin-data"] } +serde_json = "1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.35", features = ["full"] } [target.'cfg(target_arch = "wasm32")'.dependencies] tokio = { version = "1.35", default-features = false } +sqlite-wasm-rs = "0.5.2" diff --git a/ggsql-wasm/demo/build.mjs b/ggsql-wasm/demo/build.mjs new file mode 100644 index 00000000..85d0f8cc --- /dev/null +++ b/ggsql-wasm/demo/build.mjs @@ -0,0 +1,67 @@ +import * as esbuild from "esbuild"; +import { copyFileSync, mkdirSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const isWatch = process.argv.includes("--watch"); +const distDir = join(__dirname, "dist"); + +// Ensure dist/ directory exists +mkdirSync(distDir, { recursive: true }); + +// Copy static files +console.log("Copying static files..."); +copyFileSync(join(__dirname, "src/index.html"), join(distDir, "index.html")); +copyFileSync( + join(__dirname, "../pkg/ggsql_wasm_bg.wasm"), + join(distDir, "ggsql_wasm_bg.wasm"), +); +copyFileSync( + join(__dirname, "node_modules/vscode-oniguruma/release/onig.wasm"), + join(distDir, "onig.wasm"), +); +copyFileSync( + join(__dirname, "../../ggsql-vscode/syntaxes/ggsql.tmLanguage.json"), + join(distDir, "ggsql.tmLanguage.json"), +); + +// Build Monaco editor web worker +console.log("Building Monaco editor worker..."); +await esbuild.build({ + entryPoints: [ + join( + __dirname, + "node_modules/monaco-editor/esm/vs/editor/editor.worker.js", + ), + ], + bundle: true, + outfile: join(distDir, "editor.worker.js"), + format: "iife", +}); + +// Build main application bundle +const buildOptions = { + entryPoints: [join(__dirname, "src/main.ts")], + bundle: true, + outfile: join(distDir, "bundle.js"), + format: "esm", + platform: "browser", + target: "es2020", + sourcemap: true, + nodePaths: [join(__dirname, "node_modules")], + loader: { + ".ttf": "file", + }, +}; + +if (isWatch) { + console.log("Starting watch mode..."); + const ctx = await esbuild.context(buildOptions); + await ctx.watch(); + console.log("Watching for changes..."); +} else { + console.log("Building main bundle..."); + await esbuild.build(buildOptions); + console.log("Build complete!"); +} diff --git a/ggsql-wasm/demo/package-lock.json b/ggsql-wasm/demo/package-lock.json new file mode 100644 index 00000000..7d7920ea --- /dev/null +++ b/ggsql-wasm/demo/package-lock.json @@ -0,0 +1,1659 @@ +{ + "name": "ggsql-wasm-demo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ggsql-wasm-demo", + "version": "0.1.0", + "dependencies": { + "ggsql-wasm": "file:../pkg", + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "monaco-editor": "^0.55.0", + "typescript": "^5.9.0", + "vega": "^6.2.0", + "vega-embed": "^7.1.0", + "vega-lite": "6.4.1", + "vscode-oniguruma": "^2.0.1", + "vscode-textmate": "^9.3.0" + } + }, + "../pkg": { + "name": "ggsql-wasm", + "version": "0.1.0", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ggsql-wasm": { + "resolved": "../pkg", + "link": true + }, + "node_modules/hyparquet": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.25.0.tgz", + "integrity": "sha512-isJx+RplYT3aJc5yhaG5CeOZSBJecHZgYsUi7NE6P/nAbxxA0hZcyul0tUsWCQLc9QXYQ2uFyYBrk61JbJO0cg==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vega": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-6.2.0.tgz", + "integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-crossfilter": "~5.1.0", + "vega-dataflow": "~6.1.0", + "vega-encode": "~5.1.0", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-force": "~5.1.0", + "vega-format": "~2.1.0", + "vega-functions": "~6.1.0", + "vega-geo": "~5.1.0", + "vega-hierarchy": "~5.1.0", + "vega-label": "~2.1.0", + "vega-loader": "~5.1.0", + "vega-parser": "~7.1.0", + "vega-projection": "~2.1.0", + "vega-regression": "~2.1.0", + "vega-runtime": "~7.1.0", + "vega-scale": "~8.1.0", + "vega-scenegraph": "~5.1.0", + "vega-statistics": "~2.0.0", + "vega-time": "~3.1.0", + "vega-transforms": "~5.1.0", + "vega-typings": "~2.1.0", + "vega-util": "~2.1.0", + "vega-view": "~6.1.0", + "vega-view-transforms": "~5.1.0", + "vega-voronoi": "~5.1.0", + "vega-wordcloud": "~5.1.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-crossfilter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", + "integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-dataflow": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.0.tgz", + "integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-format": "^2.1.0", + "vega-loader": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-embed": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-7.1.0.tgz", + "integrity": "sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "fast-json-patch": "^3.1.1", + "json-stringify-pretty-compact": "^4.0.0", + "semver": "^7.7.2", + "tslib": "^2.8.1", + "vega-interpreter": "^2.0.0", + "vega-schema-url-parser": "^3.0.2", + "vega-themes": "3.0.0", + "vega-tooltip": "1.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-encode": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.1.0.tgz", + "integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-event-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-expression": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-force": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.0.tgz", + "integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-force": "^3.0.0", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.0.tgz", + "integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-format": "^3.1.0", + "d3-time-format": "^4.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-functions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.1.1.tgz", + "integrity": "sha512-Due6jP0y0FfsGMTrHnzUGnEwXPu7VwE+9relfo+LjL/tRPYnnKqwWvzt7n9JkeBuZqjkgYjMzm/WucNn6Hkw5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-dataflow": "^6.1.0", + "vega-expression": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-scenegraph": "^5.1.0", + "vega-selections": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-geo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.0.tgz", + "integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-projection": "^2.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-hierarchy": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", + "integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-hierarchy": "^3.1.2", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-interpreter": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vega-interpreter/-/vega-interpreter-2.2.1.tgz", + "integrity": "sha512-o+4ZEme2mdFLewlpF76dwPWW2VkZ3TAF3DMcq75/NzA5KPvnN4wnlCM8At2FVawbaHRyGdVkJSS5ROF5KwpHPQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.0.tgz", + "integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-lite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.1.tgz", + "integrity": "sha512-KO3ybHNouRK4A0al/+2fN9UqgTEfxrd/ntGLY933Hg5UOYotDVQdshR3zn7OfXwQ7uj0W96Vfa5R+QxO8am3IQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-util": "~2.1.0", + "yargs": "~18.0.0" + }, + "bin": { + "vl2pdf": "bin/vl2pdf", + "vl2png": "bin/vl2png", + "vl2svg": "bin/vl2svg", + "vl2vg": "bin/vl2vg" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "^6.0.0" + } + }, + "node_modules/vega-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.0.tgz", + "integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-dsv": "^3.0.1", + "topojson-client": "^3.1.0", + "vega-format": "^2.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.0.tgz", + "integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-event-selector": "^4.0.0", + "vega-functions": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-projection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.0.tgz", + "integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "vega-scale": "^8.1.0" + } + }, + "node_modules/vega-regression": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.0.tgz", + "integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-runtime": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.0.tgz", + "integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scale": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.0.tgz", + "integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scenegraph": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", + "integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "vega-canvas": "^2.0.0", + "vega-loader": "^5.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-schema-url-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", + "integrity": "sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-selections": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.2.tgz", + "integrity": "sha512-xJ+V4qdd46nk2RBdwIRrQm2iSTMHdlu/omhLz1pqRL3jZDrkqNBXimrisci2kIKpH2WBpA1YVagwuZEKBmF2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "3.2.4", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-statistics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4" + } + }, + "node_modules/vega-themes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-3.0.0.tgz", + "integrity": "sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.1.0.tgz", + "integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-tooltip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-1.0.0.tgz", + "integrity": "sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.1.0.tgz", + "integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-typings": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.1.0.tgz", + "integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/geojson": "7946.0.16", + "vega-event-selector": "^4.0.0", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-util": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.0.tgz", + "integrity": "sha512-PGfp0m0QCufDmcxKJCWQy4Ov23FoF8DSXmoJwSezi3itQaa2hbxK0+xwsTMP2vy4PR16Pu25HMzgMwXVW1+33w==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-view": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.1.0.tgz", + "integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-timer": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-format": "^2.1.0", + "vega-functions": "^6.1.0", + "vega-runtime": "^7.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-view-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", + "integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-voronoi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.0.tgz", + "integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-delaunay": "^6.0.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-wordcloud": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", + "integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vscode-oniguruma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz", + "integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} diff --git a/ggsql-wasm/demo/package.json b/ggsql-wasm/demo/package.json new file mode 100644 index 00000000..a145b247 --- /dev/null +++ b/ggsql-wasm/demo/package.json @@ -0,0 +1,25 @@ +{ + "name": "ggsql-wasm-demo", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs", + "dev": "node build.mjs --watch", + "serve": "npx http-server dist -p 8080 -c-1" + }, + "dependencies": { + "ggsql-wasm": "file:../pkg", + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "monaco-editor": "^0.55.0", + "typescript": "^5.9.0", + "vega": "^6.2.0", + "vega-embed": "^7.1.0", + "vega-lite": "6.4.1", + "vscode-oniguruma": "^2.0.1", + "vscode-textmate": "^9.3.0" + } +} diff --git a/ggsql-wasm/demo/src/context.ts b/ggsql-wasm/demo/src/context.ts new file mode 100644 index 00000000..ebad864f --- /dev/null +++ b/ggsql-wasm/demo/src/context.ts @@ -0,0 +1,57 @@ +import init, { GgsqlContext } from "ggsql-wasm"; + +export class WasmContextManager { + private context: GgsqlContext | null = null; + private initialized = false; + + async initialize(): Promise { + if (this.initialized) return; + + await init("./ggsql_wasm_bg.wasm"); + this.context = new GgsqlContext(); + this.initialized = true; + } + + private getContext(): GgsqlContext { + if (!this.context) { + throw new Error("Context not initialized. Call initialize() first."); + } + return this.context; + } + + execute(query: string): string { + return this.getContext().execute(query); + } + + hasVisual(query: string): boolean { + return this.getContext().has_visual(query); + } + + executeSql(query: string): string { + return this.getContext().execute_sql(query); + } + + registerCSV(name: string, data: Uint8Array): void { + this.getContext().register_csv(name, data); + } + + async registerParquet(name: string, data: Uint8Array): Promise { + await this.getContext().register_parquet(name, data); + } + + async registerBuiltinDatasets(): Promise { + await this.getContext().register_builtin_datasets(); + } + + unregister(name: string): void { + this.getContext().unregister(name); + } + + listTables(): string[] { + return Array.from(this.getContext().list_tables() as Iterable); + } + + isInitialized(): boolean { + return this.initialized; + } +} diff --git a/ggsql-wasm/demo/src/editor.ts b/ggsql-wasm/demo/src/editor.ts new file mode 100644 index 00000000..1c0fb03f --- /dev/null +++ b/ggsql-wasm/demo/src/editor.ts @@ -0,0 +1,179 @@ +import * as monaco from "monaco-editor"; +import { createOnigScanner, createOnigString, loadWASM } from "vscode-oniguruma"; +import { Registry, parseRawGrammar, type IGrammar } from "vscode-textmate"; + +// Must be set before any Monaco editor is created +(self as any).MonacoEnvironment = { + getWorkerUrl: (_moduleId: string, _label: string) => "./editor.worker.js", +}; + +// Map TextMate scope names to Monaco theme token colors +const SCOPE_TO_TOKEN: [string, string][] = [ + ["comment", "comment"], + ["string", "string"], + ["constant.numeric", "number"], + ["constant.language", "keyword"], + ["keyword", "keyword"], + ["support.function", "type"], + ["support.type.geom", "type"], + ["support.type.aesthetic", "variable"], + ["support.type.coord", "type"], + ["support.type.theme", "type"], + ["support.type.property", "variable"], + ["constant.language.scale-type", "type"], + ["keyword.operator", "operator"], + ["punctuation", "delimiter"], +]; + +function scopeToMonacoToken(scopes: string[]): string { + // Walk scopes from most specific to least + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]; + for (const [pattern, token] of SCOPE_TO_TOKEN) { + if (scope.startsWith(pattern)) { + return token; + } + } + } + return ""; +} + +async function initTextMateGrammar(): Promise { + // Load oniguruma WASM + const onigWasm = await fetch("./onig.wasm"); + const onigBuffer = await onigWasm.arrayBuffer(); + await loadWASM(onigBuffer); + + // Create the TextMate registry + const registry = new Registry({ + onigLib: Promise.resolve({ + createOnigScanner, + createOnigString, + }), + loadGrammar: async (scopeName: string) => { + if (scopeName === "source.ggsql") { + const response = await fetch("./ggsql.tmLanguage.json"); + const grammarText = await response.text(); + return parseRawGrammar(grammarText, "ggsql.tmLanguage.json"); + } + return null; + }, + }); + + return registry.loadGrammar("source.ggsql"); +} + +export class EditorManager { + private editor: monaco.editor.IStandaloneCodeEditor | null = null; + private onChangeCallback: ((query: string) => void) | null = null; + private changeTimeoutId: number | null = null; + + async initialize( + container: HTMLElement, + initialValue: string, + ): Promise { + // Register ggsql language + monaco.languages.register({ id: "ggsql" }); + + // Apply language configuration from ggsql-vscode/language-configuration.json + monaco.languages.setLanguageConfiguration("ggsql", { + comments: { + lineComment: "--", + blockComment: ["/*", "*/"], + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + autoClosingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "'", close: "'", notIn: ["string", "comment"] }, + { open: '"', close: '"', notIn: ["string", "comment"] }, + ], + surroundingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "'", close: "'" }, + { open: '"', close: '"' }, + ], + }); + + // Load TextMate grammar for tokenization + const grammar = await initTextMateGrammar(); + if (grammar) { + // Create a custom tokens provider using the TextMate grammar + monaco.languages.setTokensProvider("ggsql", { + getInitialState: () => new TMState(null), + tokenize: (line: string, state: TMState): monaco.languages.ILineTokens => { + const result = grammar.tokenizeLine(line, state.ruleStack); + const tokens: monaco.languages.IToken[] = result.tokens.map((t) => ({ + startIndex: t.startIndex, + scopes: scopeToMonacoToken(t.scopes), + })); + return { + tokens, + endState: new TMState(result.ruleStack), + }; + }, + }); + } + + // Create editor + this.editor = monaco.editor.create(container, { + value: initialValue, + language: "ggsql", + theme: "vs", + automaticLayout: true, + minimap: { enabled: false }, + fontSize: 14, + lineNumbers: "on", + scrollBeyondLastLine: false, + wordWrap: "on", + padding: { top: 10 }, + }); + + // Set up change listener with debounce + this.editor.onDidChangeModelContent(() => { + if (this.changeTimeoutId !== null) { + clearTimeout(this.changeTimeoutId); + } + this.changeTimeoutId = window.setTimeout(() => { + if (this.onChangeCallback && this.editor) { + this.onChangeCallback(this.editor.getValue()); + } + }, 100); + }); + } + + getValue(): string { + return this.editor?.getValue() || ""; + } + + setValue(value: string): void { + this.editor?.setValue(value); + } + + onChange(callback: (query: string) => void): void { + this.onChangeCallback = callback; + } +} + +// TextMate state wrapper for Monaco +class TMState implements monaco.languages.IState { + constructor(public ruleStack: any) {} + + clone(): TMState { + return new TMState(this.ruleStack); + } + + equals(other: monaco.languages.IState): boolean { + if (!(other instanceof TMState)) return false; + if (!this.ruleStack && !other.ruleStack) return true; + if (!this.ruleStack || !other.ruleStack) return false; + return this.ruleStack.equals(other.ruleStack); + } +} diff --git a/ggsql-wasm/demo/src/examples.ts b/ggsql-wasm/demo/src/examples.ts new file mode 100644 index 00000000..61b9acba --- /dev/null +++ b/ggsql-wasm/demo/src/examples.ts @@ -0,0 +1,214 @@ +export interface Example { + name: string; + query: string; + section: string; +} + +export const examples: Example[] = [ + // === Layers === + { + section: "Layers", + name: "Area", + query: `VISUALISE FROM ggsql:airquality +DRAW area + MAPPING Date AS x, Wind AS y`, + }, + { + section: "Layers", + name: "Bar", + query: `VISUALISE FROM ggsql:penguins +DRAW bar + MAPPING species AS x`, + }, + { + section: "Layers", + name: "Boxplot", + query: `VISUALISE FROM ggsql:penguins +DRAW boxplot + MAPPING species AS x, bill_len AS y, island AS fill`, + }, + { + section: "Layers", + name: "Density", + query: `VISUALISE bill_dep AS x, species AS colour FROM ggsql:penguins + DRAW density MAPPING body_mass AS weight`, + }, + { + section: "Layers", + name: "Histogram", + query: `VISUALISE FROM ggsql:penguins +DRAW histogram + MAPPING body_mass AS x`, + }, + { + section: "Layers", + name: "Line", + query: `VISUALISE FROM ggsql:airquality +DRAW line + MAPPING Day AS x, Temp AS y, Month AS color`, + }, + { + section: "Layers", + name: "Path", + query: `WITH df(x, y, id) AS (VALUES + (1.0, 1.0, 'A'), + (2.0, 1.0, 'A'), + (1.0, 3.0, 'A'), + (3.0, 1.0, 'B'), + (2.0, 3.0, 'B'), + (3.0, 3.0, 'B') +) +VISUALIZE x, y FROM df +DRAW line + MAPPING id AS colour`, + }, + { + section: "Layers", + name: "Point", + query: `SELECT * FROM ggsql:penguins +VISUALISE +DRAW point MAPPING bill_len AS x, bill_dep AS y, body_mass AS size, species AS color +LABEL title => 'Penguin Measurements', x => 'Bill Length (mm)', y => 'Bill Depth (mm)'`, + }, + { + section: "Layers", + name: "Polygon", + query: `WITH df(x, y, id) AS (VALUES + (1.0, 1.0, 'A'), + (2.0, 1.0, 'A'), + (1.0, 3.0, 'A'), + (3.0, 1.0, 'B'), + (2.0, 3.0, 'B'), + (3.0, 3.0, 'B') +) +VISUALIZE x, y FROM df +DRAW polygon + MAPPING id AS colour`, + }, + { + section: "Layers", + name: "Ribbon", + query: ` VISUALISE FROM ggsql:airquality + DRAW ribbon + MAPPING Date AS x, Wind AS ymin, Temp AS ymax`, + }, + { + section: "Layers", + name: "Violin", + query: `VISUALISE species AS x, bill_dep AS y FROM ggsql:penguins + DRAW violin`, + }, + // === Scales === + { + section: "Scales", + name: "Binned", + query: `VISUALISE bill_len AS x, bill_dep AS y, body_mass AS color FROM ggsql:penguins +DRAW point +SCALE BINNED color TO viridis`, + }, + { + section: "Scales", + name: "Continuous", + query: `VISUALISE bill_len AS x, bill_dep AS y FROM ggsql:penguins +DRAW point +SCALE x FROM [0, null]`, + }, + { + section: "Scales", + name: "Discrete", + query: `VISUALISE bill_len AS x, bill_dep AS y, island AS shape, island AS color FROM ggsql:penguins +DRAW point + SETTING size => 6 +SCALE shape TO ['star', 'circle', 'diamond'] +SCALE color`, + }, + { + section: "Scales", + name: "Identity", + query: `WITH t(category, value, style) AS (VALUES + ('A', 45, 'forestgreen'), + ('B', 72, '#3401e3'), + ('C', 38, 'hsl(150deg 30% 60%)') +) +VISUALISE category AS x, value AS y, style AS fill FROM t +DRAW bar +SCALE IDENTITY fill`, + }, + { + section: "Scales", + name: "Ordinal", + query: `VISUALISE Ozone AS x, Temp AS y FROM ggsql:airquality +DRAW point + MAPPING Month AS color +SCALE ORDINAL color + RENAMING * => '{}th month'`, + }, + { + section: "Scales", + name: "Faceting", + query: `VISUALISE sex AS x FROM ggsql:penguins +DRAW bar +FACET species +SCALE panel FROM ['Adelie', null] + RENAMING null => 'The rest'`, + }, + + // === Aesthetics === + { + section: "Aesthetics", + name: "Position", + query: `SELECT * FROM ggsql:penguins +VISUALISE +DRAW point MAPPING bill_len AS x, bill_dep AS y`, + }, + { + section: "Aesthetics", + name: "Fill", + query: `VISUALISE FROM ggsql:penguins +DRAW point + MAPPING bill_dep AS x, body_mass AS y, species AS fill + SETTING stroke => null +SCALE color TO category10`, + }, + { + section: "Aesthetics", + name: "Opacity", + query: `VISUALISE FROM ggsql:airquality +DRAW area + MAPPING Date AS x, Wind AS y + SETTING opacity => 0.2`, + }, + { + section: "Aesthetics", + name: "Linetype", + query: `VISUALISE FROM ggsql:airquality +DRAW line + MAPPING Day AS x, Temp AS y, Month AS linetype +SCALE ORDINAL linetype`, + }, + { + section: "Aesthetics", + name: "Linewidth", + query: `VISUALISE FROM ggsql:airquality +DRAW line + MAPPING Day AS x, Temp AS y, Month AS colour + SETTING linewidth => 5`, + }, + { + section: "Aesthetics", + name: "Shape", + query: `VISUALISE FROM ggsql:penguins +DRAW point + MAPPING bill_dep AS x, body_mass AS y, species AS shape + SETTING linewidth => 1, size => 5 +SCALE shape TO ['star', 'bowtie', 'square-plus']`, + }, + { + section: "Aesthetics", + name: "Size", + query: `SELECT * FROM ggsql:penguins +VISUALISE +DRAW point MAPPING bill_len AS x, bill_dep AS y, body_mass AS size +LABEL title => 'Penguin Measurements', x => 'Bill Length (mm)', y => 'Bill Depth (mm)'`, + }, +]; diff --git a/ggsql-wasm/demo/src/index.html b/ggsql-wasm/demo/src/index.html new file mode 100644 index 00000000..5798a532 --- /dev/null +++ b/ggsql-wasm/demo/src/index.html @@ -0,0 +1,351 @@ + + + + + + ggsql playground + + + + + + + +
+ + + + +
+
+
+
+
+

Problems

+
+
+
+ + + + diff --git a/ggsql-wasm/demo/src/main.ts b/ggsql-wasm/demo/src/main.ts new file mode 100644 index 00000000..43c5b9cd --- /dev/null +++ b/ggsql-wasm/demo/src/main.ts @@ -0,0 +1,199 @@ +import vegaEmbed from "vega-embed"; +import { Warn } from "vega"; +import { WasmContextManager } from "./context"; +import { EditorManager } from "./editor"; +import { TableManager } from "./tableManager"; +import { examples } from "./examples"; + +// State +const contextManager = new WasmContextManager(); +const editorManager = new EditorManager(); +let tableManager: TableManager; + +// DOM elements +const statusEl = document.getElementById("status")!; +const editorContainer = document.getElementById("editor-container")!; +const vizOutput = document.getElementById("viz-output")!; +const errorMessages = document.getElementById("error-messages")!; +const tableList = document.getElementById("table-list")!; +const csvUpload = document.getElementById("csv-upload") as HTMLInputElement; +const examplesList = document.getElementById("examples-list")!; + +function setStatus(message: string, type: "loading" | "success" | "error") { + statusEl.textContent = message; + statusEl.className = type; +} + +function showProblems( + errors: string[], + warnings: string[], +) { + errorMessages.innerHTML = ""; + for (const msg of errors) { + const div = document.createElement("div"); + div.className = "error-message"; + div.textContent = msg; + errorMessages.appendChild(div); + } + for (const msg of warnings) { + const div = document.createElement("div"); + div.className = "warning-message"; + div.textContent = msg; + errorMessages.appendChild(div); + } +} + +interface SqlResult { + columns: string[]; + rows: string[][]; + total_rows: number; + truncated: boolean; +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderTable(data: SqlResult): string { + const ths = data.columns.map((c) => `${escapeHtml(c)}`).join(""); + const bodyRows = data.rows + .map( + (row) => + `${row.map((v) => `${escapeHtml(v)}`).join("")}`, + ) + .join(""); + const truncationRow = data.truncated + ? `Showing ${data.rows.length} of ${data.total_rows} rows` + : ""; + return `${ths}${bodyRows}${truncationRow}
`; +} + +async function executeQuery(query: string) { + if (!query.trim()) { + showProblems([], []); + vizOutput.innerHTML = + '

Enter a query to visualize

'; + return; + } + + try { + setStatus("Executing query...", "loading"); + + if (contextManager.hasVisual(query)) { + const result = contextManager.execute(query); + const spec = JSON.parse(result); + + vizOutput.innerHTML = ""; + + const warnings: string[] = []; + let _level = Warn; + const logger = { + level(_: number) { if (arguments.length) { _level = _; return this; } return _level; }, + error: (...args: any[]) => { console.error(...args); return logger; }, + warn: (...args: any[]) => { warnings.push(args.map(String).join(" ")); return logger; }, + info: () => logger, + debug: () => logger, + }; + + await vegaEmbed(vizOutput, spec, { + actions: { + export: true, + source: false, + compiled: false, + editor: false, + }, + renderer: "svg", + logger: logger as any, + }); + + showProblems([], warnings); + } else { + const result = JSON.parse(contextManager.executeSql(query)); + vizOutput.innerHTML = renderTable(result); + showProblems([], []); + } + + setStatus("Query executed successfully", "success"); + } catch (error: any) { + console.error("Query execution error:", error); + showProblems([error.toString()], []); + setStatus("Query error", "error"); + } +} + +// File upload handlers +csvUpload.addEventListener("change", async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + try { + setStatus("Uploading data...", "loading"); + await tableManager.uploadFile(file); + setStatus("Uploaded: " + file.name, "success"); + csvUpload.value = ""; + } catch (error: any) { + showProblems(["Upload failed: " + error], []); + setStatus("Upload error", "error"); + } +}); + +function initializeExamples() { + let currentSection = ""; + examples.forEach((example) => { + if (example.section !== currentSection) { + currentSection = example.section; + const header = document.createElement("div"); + header.className = "example-section-header"; + header.textContent = currentSection; + examplesList.appendChild(header); + } + const button = document.createElement("button"); + button.className = "example-button"; + button.textContent = example.name; + button.onclick = () => { + editorManager.setValue(example.query); + //executeQuery(example.query); + }; + examplesList.appendChild(button); + }); +} + +async function main() { + try { + setStatus("Loading WASM module...", "loading"); + await contextManager.initialize(); + + // Load builtin datasets + setStatus("Loading builtin datasets...", "loading"); + await contextManager.registerBuiltinDatasets(); + + setStatus("Initializing editor...", "loading"); + await editorManager.initialize(editorContainer, examples[0].query); + + tableManager = new TableManager(tableList, contextManager); + tableManager.onClickTable((name) => { + editorManager.setValue(`SELECT * FROM ${name}`); + }); + tableManager.refresh(); + + initializeExamples(); + + editorManager.onChange((query) => { + executeQuery(query); + }); + + setStatus("Ready", "success"); + + executeQuery(examples[0].query); + } catch (error: any) { + console.error("Initialization error:", error); + setStatus("Initialization failed", "error"); + showProblems(["Failed to initialize: " + error], []); + } +} + +main(); diff --git a/ggsql-wasm/demo/src/tableManager.ts b/ggsql-wasm/demo/src/tableManager.ts new file mode 100644 index 00000000..4cf70c90 --- /dev/null +++ b/ggsql-wasm/demo/src/tableManager.ts @@ -0,0 +1,94 @@ +import { WasmContextManager } from "./context"; + +export class TableManager { + private listElement: HTMLElement; + private contextManager: WasmContextManager; + private onChangeCallback: (() => void) | null = null; + private onClickCallback: ((tableName: string) => void) | null = null; + + constructor(listElement: HTMLElement, contextManager: WasmContextManager) { + this.listElement = listElement; + this.contextManager = contextManager; + } + + refresh(): void { + const tables = this.contextManager.listTables(); + + this.listElement.innerHTML = ""; + + if (tables.length === 0) { + const emptyItem = document.createElement("li"); + emptyItem.textContent = "No tables registered"; + emptyItem.style.color = "#005F73"; + emptyItem.style.fontSize = "12px"; + emptyItem.style.padding = "6px 8px"; + this.listElement.appendChild(emptyItem); + return; + } + + tables.forEach((tableName) => { + const item = document.createElement("li"); + item.className = "table-item"; + + const nameSpan = document.createElement("span"); + nameSpan.className = "table-item-name"; + nameSpan.textContent = tableName; + + item.appendChild(nameSpan); + item.style.cursor = "pointer"; + item.onclick = () => { + if (this.onClickCallback) this.onClickCallback(tableName); + }; + + if (!tableName.startsWith("ggsql:")) { + const removeBtn = document.createElement("span"); + removeBtn.className = "table-item-remove"; + removeBtn.textContent = "\u00d7"; + removeBtn.title = "Remove table"; + removeBtn.onclick = (e) => { + e.stopPropagation(); + this.removeTable(tableName); + }; + item.appendChild(removeBtn); + } + + this.listElement.appendChild(item); + }); + } + + async uploadFile(file: File): Promise { + const tableName = this.sanitiseTableName(file.name); + const buffer = await file.arrayBuffer(); + const data = new Uint8Array(buffer); + + if (file.name.endsWith(".parquet")) { + await this.contextManager.registerParquet(tableName, data); + } else { + this.contextManager.registerCSV(tableName, data); + } + + this.refresh(); + if (this.onChangeCallback) this.onChangeCallback(); + } + + removeTable(name: string): void { + this.contextManager.unregister(name); + this.refresh(); + if (this.onChangeCallback) this.onChangeCallback(); + } + + onChange(callback: () => void): void { + this.onChangeCallback = callback; + } + + onClickTable(callback: (tableName: string) => void): void { + this.onClickCallback = callback; + } + + private sanitiseTableName(filename: string): string { + return filename + .replace(/\.(csv|parquet|pq)$/i, "") + .replace(/[^a-zA-Z0-9_]/g, "_") + .toLowerCase(); + } +} diff --git a/ggsql-wasm/demo/tsconfig.json b/ggsql-wasm/demo/tsconfig.json new file mode 100644 index 00000000..9322f010 --- /dev/null +++ b/ggsql-wasm/demo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/ggsql-wasm/library/build.mjs b/ggsql-wasm/library/build.mjs new file mode 100644 index 00000000..4a51888c --- /dev/null +++ b/ggsql-wasm/library/build.mjs @@ -0,0 +1,27 @@ +import * as esbuild from "esbuild"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const isWatch = process.argv.includes("--watch"); + +const buildOptions = { + entryPoints: [join(__dirname, "src/index.ts")], + bundle: true, + outfile: join(__dirname, "dist/lib.js"), + format: "esm", + platform: "browser", + target: "es2020", + sourcemap: true, +}; + +if (isWatch) { + console.log("Starting watch mode..."); + const ctx = await esbuild.context(buildOptions); + await ctx.watch(); + console.log("Watching for changes..."); +} else { + console.log("Building library..."); + await esbuild.build(buildOptions); + console.log("Build complete!"); +} diff --git a/ggsql-wasm/library/package-lock.json b/ggsql-wasm/library/package-lock.json new file mode 100644 index 00000000..3e6fc917 --- /dev/null +++ b/ggsql-wasm/library/package-lock.json @@ -0,0 +1,523 @@ +{ + "name": "ggsql-wasm-lib", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ggsql-wasm-lib", + "version": "0.1.0", + "dependencies": { + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "typescript": "^5.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/hyparquet": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.25.1.tgz", + "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/ggsql-wasm/library/package.json b/ggsql-wasm/library/package.json new file mode 100644 index 00000000..b29653c1 --- /dev/null +++ b/ggsql-wasm/library/package.json @@ -0,0 +1,18 @@ +{ + "name": "ggsql-wasm-lib", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs", + "dev": "node build.mjs --watch", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "typescript": "^5.9.0" + } +} diff --git a/ggsql-wasm/library/src/csv.ts b/ggsql-wasm/library/src/csv.ts new file mode 100644 index 00000000..68162075 --- /dev/null +++ b/ggsql-wasm/library/src/csv.ts @@ -0,0 +1,181 @@ +import type { ColumnDescriptor, ColumnType } from "./index"; + +/** + * Convert CSV bytes to column descriptors. + * Synchronous — no external dependencies. + */ +export function convert_csv(bytes: Uint8Array): ColumnDescriptor[] { + const text = new TextDecoder().decode(bytes); + const lines = parseCSVLines(text); + + if (lines.length < 2) return []; + + const headers = lines[0]; + const nCols = headers.length; + const nRows = lines.length - 1; + + // Collect raw string values per column + const rawCols: (string | null)[][] = []; + for (let c = 0; c < nCols; c++) { + rawCols.push(new Array(nRows)); + } + + for (let r = 0; r < nRows; r++) { + const row = lines[r + 1]; + for (let c = 0; c < nCols; c++) { + const val = c < row.length ? row[c] : ""; + rawCols[c][r] = val === "" ? null : val; + } + } + + // Per-column type inference and conversion + const columns: ColumnDescriptor[] = []; + for (let c = 0; c < nCols; c++) { + const raw = rawCols[c]; + const colType = inferCSVColumnType(raw); + columns.push(buildCSVColumn(headers[c], raw, colType)); + } + + return columns; +} + +function inferCSVColumnType(values: (string | null)[]): ColumnType { + let allInt = true; + let allNum = true; + let allBool = true; + + for (let i = 0; i < values.length; i++) { + const v = values[i]; + if (v === null) continue; + + const lower = v.toLowerCase(); + if (lower !== "true" && lower !== "false") allBool = false; + + const num = Number(v); + if (v === "" || isNaN(num)) { + allNum = false; + allInt = false; + } else { + if (!Number.isSafeInteger(num)) allInt = false; + } + } + + if (allBool) return "bool"; + if (allInt) return "i64"; + if (allNum) return "f64"; + return "string"; +} + +function buildCSVColumn( + name: string, + rawStrings: (string | null)[], + colType: ColumnType, +): ColumnDescriptor { + const len = rawStrings.length; + const nulls = new Uint8Array(len); + + if (colType === "f64" || colType === "i64") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + if (rawStrings[i] === null) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = Number(rawStrings[i]); + nulls[i] = 1; + } + } + return { name, type: colType, values, nulls }; + } + + if (colType === "bool") { + const values = new Uint8Array(len); + for (let i = 0; i < len; i++) { + if (rawStrings[i] === null) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = rawStrings[i]!.toLowerCase() === "true" ? 1 : 0; + nulls[i] = 1; + } + } + return { name, type: colType, values, nulls }; + } + + // string + const values: string[] = []; + for (let i = 0; i < len; i++) { + if (rawStrings[i] === null) { + values.push(""); + nulls[i] = 0; + } else { + values.push(rawStrings[i]!); + nulls[i] = 1; + } + } + return { name, type: "string", values, nulls }; +} + +/** + * Parse CSV text into an array of rows (each row is an array of fields). + * Handles quoted fields, embedded commas, and embedded newlines. + */ +function parseCSVLines(text: string): string[][] { + const rows: string[][] = []; + let row: string[] = []; + let field = ""; + let inQuotes = false; + let i = 0; + + while (i < text.length) { + const ch = text[i]; + + if (inQuotes) { + if (ch === '"') { + if (i + 1 < text.length && text[i + 1] === '"') { + field += '"'; + i += 2; + } else { + inQuotes = false; + i++; + } + } else { + field += ch; + i++; + } + } else { + if (ch === '"') { + inQuotes = true; + i++; + } else if (ch === ",") { + row.push(field); + field = ""; + i++; + } else if (ch === "\r") { + row.push(field); + field = ""; + rows.push(row); + row = []; + i++; + if (i < text.length && text[i] === "\n") i++; + } else if (ch === "\n") { + row.push(field); + field = ""; + rows.push(row); + row = []; + i++; + } else { + field += ch; + i++; + } + } + } + + // Handle last field/row + if (field.length > 0 || row.length > 0) { + row.push(field); + rows.push(row); + } + + return rows; +} diff --git a/ggsql-wasm/library/src/index.ts b/ggsql-wasm/library/src/index.ts new file mode 100644 index 00000000..3b130354 --- /dev/null +++ b/ggsql-wasm/library/src/index.ts @@ -0,0 +1,22 @@ +// Converters +export { convert_csv } from "./csv"; +export { convert_parquet } from "./parquet"; + +// Types +export interface ColumnDescriptor { + name: string; + type: ColumnType; + values: Float64Array | Uint8Array | string[]; + nulls: Uint8Array; +} + +export type ColumnType = + | "f64" + | "i64" + | "bool" + | "date" + | "datetime" + | "string"; + +export const EPOCH = Date.UTC(1970, 0, 1); +export const MS_PER_DAY = 86400000; diff --git a/ggsql-wasm/library/src/parquet.ts b/ggsql-wasm/library/src/parquet.ts new file mode 100644 index 00000000..7789161b --- /dev/null +++ b/ggsql-wasm/library/src/parquet.ts @@ -0,0 +1,158 @@ +import type { ColumnDescriptor, ColumnType } from "./index"; +import { EPOCH, MS_PER_DAY } from "./index"; +import { parquetReadObjects } from "hyparquet"; + +/** + * Convert Parquet bytes to column descriptors. + * Dynamically imports hyparquet. + */ +export async function convert_parquet( + bytes: Uint8Array, +): Promise { + const buffer = bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength, + ); + + const asyncBuffer = { + byteLength: buffer.byteLength, + slice: (start: number, end: number) => + Promise.resolve(buffer.slice(start, end) as ArrayBuffer), + }; + + const rows: Record[] = await parquetReadObjects({ + file: asyncBuffer, + }); + + if (rows.length === 0) return []; + + const colNames = Object.keys(rows[0]); + const columns: ColumnDescriptor[] = []; + + for (const colName of colNames) { + const rawValues = rows.map((row) => row[colName]); + columns.push(buildColumn(colName, rawValues)); + } + + return columns; +} + +function inferColumnType(values: unknown[]): ColumnType { + let hasNumber = false; + let hasBool = false; + let hasDate = false; + let allSafeInt = true; + let allMidnight = true; + + for (let i = 0; i < values.length; i++) { + const v = values[i]; + if (v === null || v === undefined) continue; + + if (v instanceof Date) { + hasDate = true; + if ( + v.getUTCHours() !== 0 || + v.getUTCMinutes() !== 0 || + v.getUTCSeconds() !== 0 || + v.getUTCMilliseconds() !== 0 + ) { + allMidnight = false; + } + } else if (typeof v === "boolean") { + hasBool = true; + } else if (typeof v === "number") { + hasNumber = true; + if (!Number.isSafeInteger(v)) allSafeInt = false; + } else if (typeof v === "bigint") { + hasNumber = true; + // bigint values will be converted to Number, keep as i64 + } else { + return "string"; + } + } + + if (hasDate) return allMidnight ? "date" : "datetime"; + if (hasBool && !hasNumber) return "bool"; + if (hasNumber) return allSafeInt ? "i64" : "f64"; + return "string"; +} + +function buildColumn(name: string, rawValues: unknown[]): ColumnDescriptor { + const len = rawValues.length; + const nulls = new Uint8Array(len); + const type = inferColumnType(rawValues); + + if (type === "f64" || type === "i64") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i]; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = Number(v); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + if (type === "bool") { + const values = new Uint8Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i]; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = v ? 1 : 0; + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + if (type === "date") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i] as Date | null | undefined; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = Math.floor((v.getTime() - EPOCH) / MS_PER_DAY); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + if (type === "datetime") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i] as Date | null | undefined; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = v.getTime(); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + // string + const values: string[] = []; + for (let i = 0; i < len; i++) { + const v = rawValues[i]; + if (v === null || v === undefined) { + values.push(""); + nulls[i] = 0; + } else { + values.push(String(v)); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; +} diff --git a/ggsql-wasm/library/tsconfig.json b/ggsql-wasm/library/tsconfig.json new file mode 100644 index 00000000..b2699a94 --- /dev/null +++ b/ggsql-wasm/library/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/ggsql-wasm/src/lib.rs b/ggsql-wasm/src/lib.rs index 3cb1d1bc..d007a6e3 100644 --- a/ggsql-wasm/src/lib.rs +++ b/ggsql-wasm/src/lib.rs @@ -1,16 +1,166 @@ -use ggsql::reader::{PolarsReader, Reader}; +use ggsql::naming::DATA_PREFIX; +use ggsql::reader::sqlite::SqliteReader; +use ggsql::reader::Reader; +use ggsql::validate::validate; use ggsql::writer::{VegaLiteWriter, Writer}; +use ggsql::DataFrame; +use polars::prelude::IntoColumn; +use polars::prelude::*; +use serde_json::json; use std::cell::RefCell; use wasm_bindgen::prelude::*; +// ============================================================================ +// JS bridge declarations — CSV and Parquet parsing only +// ============================================================================ + +#[wasm_bindgen(module = "/library/dist/lib.js")] +extern "C" { + #[wasm_bindgen(catch)] + async fn convert_parquet(data: &[u8]) -> Result; + + #[wasm_bindgen(catch)] + fn convert_csv(data: &[u8]) -> Result; +} + +// ============================================================================ +// SQLite VFS initialization (wasm32 only) +// ============================================================================ + +#[cfg(target_arch = "wasm32")] +fn ensure_vfs_initialized() { + use std::sync::Once; + static INIT: Once = Once::new(); + INIT.call_once(|| { + let _ = sqlite_wasm_rs::MemVfsUtil::::new(); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +fn ensure_vfs_initialized() { + // No VFS initialization needed on native targets +} + +// ============================================================================ +// Column descriptor → DataFrame conversion (for JS CSV/Parquet parsing) +// ============================================================================ + +/// Convert JS column descriptors to a Polars DataFrame. +fn columns_js_to_dataframe(columns_js: JsValue) -> Result { + let columns = js_sys::Array::from(&columns_js); + let len = columns.length(); + + if len == 0 { + return Ok(DataFrame::empty()); + } + + let mut series_vec: Vec = Vec::with_capacity(len as usize); + + for i in 0..len { + let col = columns.get(i); + let col_name = js_sys::Reflect::get(&col, &"name".into()) + .map_err(|_| JsValue::from_str("Missing column name"))? + .as_string() + .ok_or_else(|| JsValue::from_str("Column name is not a string"))?; + let col_type = js_sys::Reflect::get(&col, &"type".into()) + .map_err(|_| JsValue::from_str("Missing column type"))? + .as_string() + .ok_or_else(|| JsValue::from_str("Column type is not a string"))?; + let values_js = js_sys::Reflect::get(&col, &"values".into()) + .map_err(|_| JsValue::from_str("Missing column values"))?; + let nulls_js = js_sys::Reflect::get(&col, &"nulls".into()) + .map_err(|_| JsValue::from_str("Missing column nulls"))?; + + let nulls = js_sys::Uint8Array::new(&nulls_js).to_vec(); + + let series = match col_type.as_str() { + "f64" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v) } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "i64" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v as i64) } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "bool" => { + let raw = js_sys::Uint8Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v != 0) } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "string" => { + let arr = js_sys::Array::from(&values_js); + let values: Vec> = (0..arr.length()) + .zip(nulls.iter()) + .map(|(j, &n)| if n != 0 { arr.get(j).as_string() } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "date" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v as i32) } else { None }) + .collect(); + let s = Series::new(col_name.as_str().into(), values); + s.cast(&DataType::Date).map_err(|e| { + JsValue::from_str(&format!("Date cast error for '{}': {}", col_name, e)) + })? + } + "datetime" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v as i64) } else { None }) + .collect(); + let s = Series::new(col_name.as_str().into(), values); + s.cast(&DataType::Datetime(TimeUnit::Milliseconds, None)) + .map_err(|e| { + JsValue::from_str(&format!("Datetime cast error for '{}': {}", col_name, e)) + })? + } + other => { + return Err(JsValue::from_str(&format!( + "Unknown column type: '{}'", + other + ))); + } + }; + + series_vec.push(series.into_column()); + } + + DataFrame::new(series_vec) + .map_err(|e| JsValue::from_str(&format!("DataFrame creation error: {}", e))) +} + +// ============================================================================ +// GgsqlContext - public WASM API +// ============================================================================ + /// Persistent ggsql context for WASM /// /// Create once and reuse for multiple queries to avoid memory issues. /// Uses interior mutability to avoid wasm_bindgen's &mut self aliasing issues. #[wasm_bindgen] pub struct GgsqlContext { - reader: RefCell, + reader: RefCell, writer: VegaLiteWriter, } @@ -19,8 +169,10 @@ impl GgsqlContext { /// Create a new ggsql context #[wasm_bindgen(constructor)] pub fn new() -> Result { - let reader = PolarsReader::from_connection_string("polars://memory") - .map_err(|e| JsValue::from_str(&format!("Reader error: {:?}", e)))?; + ensure_vfs_initialized(); + + let reader = SqliteReader::new() + .map_err(|e| JsValue::from_str(&format!("Failed to create SQLite reader: {:?}", e)))?; let writer = VegaLiteWriter::new(); Ok(GgsqlContext { reader: RefCell::new(reader), @@ -30,9 +182,8 @@ impl GgsqlContext { /// Execute a ggsql query and return Vega-Lite JSON pub fn execute(&self, query: &str) -> Result { - // Scope the mutable borrow to avoid aliasing issues let spec = { - let reader = self.reader.borrow_mut(); + let reader = self.reader.borrow(); reader .execute(query) .map_err(|e| JsValue::from_str(&format!("Execute error: {:?}", e)))? @@ -46,9 +197,99 @@ impl GgsqlContext { Ok(result) } - // TODO: Register a table from binary data (e.g. CSV, Parquet) - pub fn register(&self, _name: &str) -> Result<(), JsValue> { - Err(JsValue::from_str("Registration not yet implemented.")) + /// Check whether a query contains a VISUALISE clause + pub fn has_visual(&self, query: &str) -> bool { + match validate(query) { + Ok(v) => v.has_visual(), + Err(_) => false, + } + } + + /// Execute SQL-only query and return JSON with columns/rows + pub fn execute_sql(&self, query: &str) -> Result { + let df = { + let reader = self.reader.borrow(); + reader + .execute_sql(query) + .map_err(|e| JsValue::from_str(&format!("SQL error: {:?}", e)))? + }; + + let max_rows = 100usize; + let total_rows = df.height(); + let truncated = total_rows > max_rows; + let df = if truncated { + df.head(Some(max_rows)) + } else { + df + }; + + let columns: Vec = df + .get_column_names() + .into_iter() + .map(|s| s.to_string()) + .collect(); + let mut rows: Vec> = Vec::with_capacity(df.height()); + + for i in 0..df.height() { + let mut row = Vec::with_capacity(columns.len()); + for col in df.get_columns() { + let val = col + .get(i) + .map_err(|e| JsValue::from_str(&format!("Error reading row {}: {}", i, e)))?; + row.push(format!("{}", val)); + } + rows.push(row); + } + + let result = json!({ + "columns": columns, + "rows": rows, + "total_rows": total_rows, + "truncated": truncated, + }); + + serde_json::to_string(&result).map_err(|e| JsValue::from_str(&format!("JSON error: {}", e))) + } + + /// Register a CSV file as a table from raw bytes + pub fn register_csv(&self, name: &str, data: &[u8]) -> Result<(), JsValue> { + let columns_js = convert_csv(data) + .map_err(|e| JsValue::from_str(&format!("CSV parse error: {:?}", e)))?; + let df = columns_js_to_dataframe(columns_js)?; + let reader = self.reader.borrow(); + reader + .register(name, df, true) + .map_err(|e| JsValue::from_str(&format!("Registration error: {:?}", e))) + } + + /// Register a Parquet file as a table from raw bytes + pub async fn register_parquet(&self, name: &str, data: &[u8]) -> Result<(), JsValue> { + let columns_js = convert_parquet(data) + .await + .map_err(|e| JsValue::from_str(&format!("Parquet parse error: {:?}", e)))?; + let df = columns_js_to_dataframe(columns_js)?; + let reader = self.reader.borrow(); + reader + .register(name, df, true) + .map_err(|e| JsValue::from_str(&format!("Registration error: {:?}", e))) + } + + /// Register all known builtin datasets (e.g. ggsql:penguins) + pub async fn register_builtin_datasets(&self) -> Result<(), JsValue> { + for &name in ggsql::reader::data::KNOWN_DATASETS { + if let Some(bytes) = ggsql::reader::data::builtin_parquet_bytes(name) { + let table_name = ggsql::naming::builtin_data_table(name); + let columns_js = convert_parquet(bytes).await.map_err(|e| { + JsValue::from_str(&format!("Parquet error for '{}': {:?}", name, e)) + })?; + let df = columns_js_to_dataframe(columns_js)?; + let reader = self.reader.borrow(); + reader.register(&table_name, df, true).map_err(|e| { + JsValue::from_str(&format!("Registration error for '{}': {:?}", name, e)) + })?; + } + } + Ok(()) } /// Unregister a table @@ -56,9 +297,7 @@ impl GgsqlContext { let reader = self.reader.borrow(); reader .unregister(name) - .map_err(|e| JsValue::from_str(&format!("Unregister error: {:?}", e)))?; - - Ok(()) + .map_err(|e| JsValue::from_str(&format!("Unregister error: {:?}", e))) } /// List all registered tables @@ -70,6 +309,17 @@ impl GgsqlContext { for table in tables { array.push(&JsValue::from_str(&table)); } + + // Builtin datasets (translate internal name → ggsql:name) + for table in reader.list_tables(true) { + if let Some(name) = table + .strip_prefix(DATA_PREFIX) + .and_then(|s| s.strip_suffix("__")) + { + array.push(&JsValue::from_str(&format!("ggsql:{}", name))); + } + } + array.into() } } diff --git a/src/naming.rs b/src/naming.rs index b80d5942..882f40dc 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -70,7 +70,7 @@ const LAYER_PREFIX: &str = concatcp!(GGSQL_PREFIX, "layer_"); const AES_PREFIX: &str = concatcp!(GGSQL_PREFIX, "aes_"); /// Full prefix for builtin data tables: `__ggsql_data_` -const DATA_PREFIX: &str = concatcp!(GGSQL_PREFIX, "data_"); +pub const DATA_PREFIX: &str = concatcp!(GGSQL_PREFIX, "data_"); /// Key for global data in the layer data HashMap. /// Used as the key in PreparedData.data to store global data that applies to all layers. diff --git a/src/reader/data.rs b/src/reader/data.rs index fac5639a..99b528e9 100644 --- a/src/reader/data.rs +++ b/src/reader/data.rs @@ -28,7 +28,7 @@ static AIRQUALITY: &[u8] = include_bytes!(concat!( /// Get the embedded parquet bytes for a known builtin dataset. #[cfg(feature = "builtin-data")] -fn builtin_parquet_bytes(name: &str) -> Option<&'static [u8]> { +pub fn builtin_parquet_bytes(name: &str) -> Option<&'static [u8]> { match name { "penguins" => Some(PENGUINS), "airquality" => Some(AIRQUALITY), @@ -86,7 +86,7 @@ pub fn register_builtin_datasets_duckdb( // Polars-based builtin data loading // ============================================================================= -#[cfg(feature = "builtin-data")] +#[cfg(feature = "parquet")] pub fn load_builtin_dataframe(name: &str) -> Result { use polars::prelude::*; use std::io::Cursor; @@ -109,7 +109,7 @@ pub fn load_builtin_dataframe(name: &str) -> Result bool { diff --git a/src/reader/sqlite.rs b/src/reader/sqlite.rs index 96981bea..09d8b015 100644 --- a/src/reader/sqlite.rs +++ b/src/reader/sqlite.rs @@ -97,6 +97,18 @@ impl SqliteReader { &self.conn } + /// List table names known to this reader. + /// + /// When `internal` is false, filters out internal tables (prefixed with `__ggsql_`). + pub fn list_tables(&self, internal: bool) -> Vec { + self.registered_tables + .borrow() + .iter() + .filter(|name| internal || !name.starts_with("__ggsql_")) + .cloned() + .collect() + } + /// Check if a table is registered fn table_exists(&self, name: &str) -> bool { let sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1"; From 3bcd09466ac26b33450bc73a86dfdfedc7bfa776 Mon Sep 17 00:00:00 2001 From: George Stagg Date: Wed, 11 Mar 2026 09:34:24 +0000 Subject: [PATCH 2/7] Tweak demo website style --- ggsql-wasm/demo/src/index.html | 300 +---------------------------- ggsql-wasm/demo/src/main.ts | 30 +++ ggsql-wasm/demo/src/styles.css | 333 +++++++++++++++++++++++++++++++++ 3 files changed, 369 insertions(+), 294 deletions(-) create mode 100644 ggsql-wasm/demo/src/styles.css diff --git a/ggsql-wasm/demo/src/index.html b/ggsql-wasm/demo/src/index.html index 5798a532..2aa62f85 100644 --- a/ggsql-wasm/demo/src/index.html +++ b/ggsql-wasm/demo/src/index.html @@ -11,301 +11,7 @@ href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet" /> -
@@ -317,6 +23,12 @@

ggsql playground

Initializing...
+
+ +
+