diff --git a/ggsql-vscode/.gitignore b/ggsql-vscode/.gitignore index 1fcb1529..200e9bb8 100644 --- a/ggsql-vscode/.gitignore +++ b/ggsql-vscode/.gitignore @@ -1 +1,2 @@ out +out-test diff --git a/ggsql-vscode/.vscodeignore b/ggsql-vscode/.vscodeignore index 4a7a2ca4..84fdc832 100644 --- a/ggsql-vscode/.vscodeignore +++ b/ggsql-vscode/.vscodeignore @@ -4,14 +4,18 @@ .yarnrc vsc-extension-quickstart.md **/tsconfig.json +**/tsconfig.test.json **/.eslintrc.json +eslint.config.mjs **/*.map **/*.ts !**/*.d.ts node_modules/** .editorconfig src/** +test/** esbuild.js out/**/*.map +out-test/** package-lock.json *.vsix diff --git a/ggsql-vscode/README.md b/ggsql-vscode/README.md index 6f5cca2d..85bb7748 100644 --- a/ggsql-vscode/README.md +++ b/ggsql-vscode/README.md @@ -7,6 +7,7 @@ - Complete syntax highlighting for ggsql queries. - `.ggsql` file extension support. - Language runtime integration for [Positron IDE](https://positron.posit.co). +- Render Plot toolbar command with a Markdown-preview-style chart tab. ## Example diff --git a/ggsql-vscode/esbuild.js b/ggsql-vscode/esbuild.js index c49e0642..276bf8eb 100644 --- a/ggsql-vscode/esbuild.js +++ b/ggsql-vscode/esbuild.js @@ -4,7 +4,7 @@ const production = process.argv.includes('--production'); const watch = process.argv.includes('--watch'); async function main() { - const ctx = await esbuild.context({ + const extensionCtx = await esbuild.context({ entryPoints: ['src/extension.ts'], bundle: true, format: 'cjs', @@ -17,11 +17,26 @@ async function main() { logLevel: 'info', }); + const previewCtx = await esbuild.context({ + entryPoints: ['src/previewWebview.ts'], + bundle: true, + format: 'iife', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'browser', + outfile: 'out/previewWebview.js', + logLevel: 'info', + }); + if (watch) { - await ctx.watch(); + await extensionCtx.watch(); + await previewCtx.watch(); } else { - await ctx.rebuild(); - await ctx.dispose(); + await extensionCtx.rebuild(); + await previewCtx.rebuild(); + await extensionCtx.dispose(); + await previewCtx.dispose(); } } diff --git a/ggsql-vscode/package-lock.json b/ggsql-vscode/package-lock.json index 3bdc77ab..7d541429 100644 --- a/ggsql-vscode/package-lock.json +++ b/ggsql-vscode/package-lock.json @@ -20,7 +20,10 @@ "esbuild": "^0.27.4", "eslint": "^10.0.3", "npm-run-all": "^4.1.5", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vega": "^6.2.0", + "vega-embed": "^7.1.0", + "vega-lite": "^6.4.2" }, "engines": { "vscode": "^1.75.0" @@ -641,6 +644,13 @@ "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/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -938,6 +948,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1114,6 +1137,21 @@ "node": ">=0.8.0" } }, + "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/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1131,6 +1169,16 @@ "dev": true, "license": "MIT" }, + "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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1153,6 +1201,261 @@ "node": ">= 8" } }, + "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/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -1268,6 +1571,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1283,6 +1596,13 @@ "node": ">= 0.4" } }, + "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/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -1471,6 +1791,16 @@ "@esbuild/win32-x64": "0.27.4" } }, + "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/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1679,6 +2009,13 @@ "dev": true, "license": "MIT" }, + "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/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1829,6 +2166,29 @@ "node": ">= 0.4" } }, + "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.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2037,6 +2397,19 @@ "dev": true, "license": "ISC" }, + "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/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2072,6 +2445,16 @@ "node": ">= 0.4" } }, + "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/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -2503,6 +2886,13 @@ "dev": true, "license": "MIT" }, + "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/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3070,6 +3460,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "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/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -3125,6 +3529,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -3349,6 +3760,24 @@ "node": ">= 0.4" } }, + "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/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", @@ -3427,6 +3856,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -3486,6 +3931,28 @@ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, + "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/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -3499,6 +3966,13 @@ "typescript": ">=4.8.4" } }, + "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3649,6 +4123,510 @@ "spdx-expression-parse": "^3.0.0" } }, + "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.2", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.2.tgz", + "integrity": "sha512-Mv2PaRIpijz256LM0NdOJd9Md8cqyrXina54xW6Qp865YfY502zlXGUst+W/XznVwISGfatt0yLZuDqCUbBDuw==", + "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": ">=20" + }, + "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3764,6 +4742,75 @@ "node": ">=0.10.0" } }, + "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/wrap-ansi/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/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" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/ggsql-vscode/package.json b/ggsql-vscode/package.json index 4ad09fc8..1f6141ee 100644 --- a/ggsql-vscode/package.json +++ b/ggsql-vscode/package.json @@ -62,6 +62,15 @@ "title": "Source Current File", "icon": "$(play)" }, + { + "command": "ggsql.renderPlot", + "category": "ggsql", + "title": "Render Plot", + "icon": { + "light": "./resources/render-plot-light.svg", + "dark": "./resources/render-plot-dark.svg" + } + }, { "command": "ggsql.runQuery", "category": "ggsql", @@ -103,6 +112,13 @@ } ], "menus": { + "editor/title": [ + { + "command": "ggsql.renderPlot", + "group": "navigation@-1", + "when": "resourceLangId == ggsql && !isInDiffEditor" + } + ], "editor/title/run": [ { "command": "ggsql.sourceCurrentFile", @@ -123,6 +139,16 @@ "type": "string", "default": "", "description": "Path to the ggsql-jupyter executable. If empty, uses 'ggsql-jupyter' from PATH." + }, + "ggsql.cliPath": { + "type": "string", + "default": "", + "description": "Path to the ggsql CLI executable used by Render Plot. If empty, uses 'ggsql' from PATH." + }, + "ggsql.readerUri": { + "type": "string", + "default": "duckdb://memory", + "description": "Reader connection URI used by Render Plot, such as duckdb://memory or duckdb:///path/to/file.db." } } } @@ -130,6 +156,7 @@ "scripts": { "vscode:prepublish": "npm run package", "compile": "npm run check-types && node esbuild.js", + "test": "tsc --project tsconfig.test.json && node --test out-test/test/*.test.js", "watch": "npm-run-all -p watch:*", "watch:esbuild": "node esbuild.js --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", @@ -149,7 +176,10 @@ "esbuild": "^0.27.4", "eslint": "^10.0.3", "npm-run-all": "^4.1.5", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vega": "^6.2.0", + "vega-embed": "^7.1.0", + "vega-lite": "^6.4.2" }, "overrides": { "uri-js": "npm:uri-js-replace" diff --git a/ggsql-vscode/resources/render-plot-dark.svg b/ggsql-vscode/resources/render-plot-dark.svg new file mode 100644 index 00000000..803979b1 --- /dev/null +++ b/ggsql-vscode/resources/render-plot-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/ggsql-vscode/resources/render-plot-light.svg b/ggsql-vscode/resources/render-plot-light.svg new file mode 100644 index 00000000..b59d9622 --- /dev/null +++ b/ggsql-vscode/resources/render-plot-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/ggsql-vscode/src/extension.ts b/ggsql-vscode/src/extension.ts index 75e27d01..47776a4a 100644 --- a/ggsql-vscode/src/extension.ts +++ b/ggsql-vscode/src/extension.ts @@ -6,6 +6,10 @@ */ import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; import { tryAcquirePositronApi } from '@posit-dev/positron'; import { GgsqlRuntimeManager } from './manager'; import { createConnectionDrivers } from './connections'; @@ -13,6 +17,16 @@ import { GgsqlCodeLensProvider, registerCellCommands } from './codelens'; import { activateDecorations } from './decorations'; import { activateContextKeys } from './context'; import { parseCells } from './cellParser'; +import { + buildPreviewHtml, + buildRenderArgs, + getGgsqlCliPath, + getReaderUri, +} from './preview'; + +const execFile = promisify(cp.execFile); +const INSTALL_URL = 'https://ggsql.org/get_started/installation.html'; +const previewPanels = new Map(); // Output channel for logging const outputChannel = vscode.window.createOutputChannel('ggsql'); @@ -32,6 +46,14 @@ export function activate(context: vscode.ExtensionContext): void { // Try to acquire the Positron API const positronApi = tryAcquirePositronApi(); + context.subscriptions.push( + vscode.commands.registerCommand('ggsql.renderPlot', async () => { + await renderPlot(context); + }) + ); + + void checkCliAvailability(); + if (!positronApi) { // Running in VS Code (not Positron) - syntax highlighting still works // but we don't register the language runtime @@ -99,3 +121,234 @@ export function activate(context: vscode.ExtensionContext): void { export function deactivate(): void { // Nothing to clean up } + +async function renderPlot(context: vscode.ExtensionContext): Promise { + outputChannel.show(true); + log('Render Plot clicked'); + + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.languageId !== 'ggsql') { + log('Render Plot skipped: no active ggsql editor'); + return; + } + + const query = getPreviewQuery(editor); + if (query.trim().length === 0) { + log('Render Plot skipped: query is empty'); + return; + } + + log(`Document: ${editor.document.uri.toString()}`); + log(`Query source: ${editor.selection.isEmpty ? 'current file' : 'selection'}`); + log(`Query size: ${query.length} chars`); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Rendering ggsql plot', + cancellable: false, + }, + async () => { + const config = vscode.workspace.getConfiguration('ggsql'); + const cliPath = getGgsqlCliPath(config); + const readerUri = getReaderUri(config); + const previewDir = path.join(context.globalStorageUri.fsPath, 'preview'); + const runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`; + const queryPath = path.join(previewDir, `${runId}.ggsql`); + const outputPath = path.join(previewDir, `${runId}.vl.json`); + const cwd = getDocumentWorkingDirectory(editor.document); + + log(`CLI path: ${cliPath}`); + log(`Reader URI: ${readerUri}`); + log(`Working directory: ${cwd ?? '(none)'}`); + log(`Preview directory: ${previewDir}`); + log(`Query temp file: ${queryPath}`); + log(`Vega-Lite output file: ${outputPath}`); + + await fs.promises.mkdir(previewDir, { recursive: true }); + await fs.promises.writeFile(queryPath, query, 'utf8'); + log('Wrote query temp file'); + + const args = buildRenderArgs(queryPath, outputPath, readerUri); + log(`Running command: ${formatCommand(cliPath, args)}`); + const startedAt = Date.now(); + + try { + const result = await execFile(cliPath, args, { + cwd, + timeout: 120_000, + maxBuffer: 10 * 1024 * 1024, + }); + log(`ggsql CLI exited in ${Date.now() - startedAt}ms`); + if (result.stdout.trim()) { + log(`stdout:\n${result.stdout.trim()}`); + } + if (result.stderr.trim()) { + log(`stderr:\n${result.stderr.trim()}`); + } + } catch (error) { + log(`ggsql CLI failed after ${Date.now() - startedAt}ms`); + logRenderError(error); + vscode.window.showErrorMessage(renderErrorMessage(error, cliPath)); + return; + } + + let specJson: string; + try { + specJson = await fs.promises.readFile(outputPath, 'utf8'); + } catch (error) { + logRenderError(error); + vscode.window.showErrorMessage(`ggsql did not write a Vega-Lite spec: ${errorMessage(error)}`); + return; + } + + log(`Read Vega-Lite spec (${Buffer.byteLength(specJson, 'utf8')} bytes)`); + showPreview(context, editor.document, specJson); + } + ); +} + +function getPreviewQuery(editor: vscode.TextEditor): string { + if (!editor.selection.isEmpty) { + return editor.document.getText(editor.selection); + } + return editor.document.getText(); +} + +function getDocumentWorkingDirectory(document: vscode.TextDocument): string | undefined { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; + } + + if (document.uri.scheme === 'file') { + return path.dirname(document.uri.fsPath); + } + + return undefined; +} + +function showPreview(context: vscode.ExtensionContext, document: vscode.TextDocument, specJson: string): void { + const key = document.uri.toString(); + const title = `Preview ${path.basename(document.fileName || 'plot')}`; + const existingPanel = previewPanels.get(key); + if (existingPanel) { + log(`Preview panel reused: ${title}`); + existingPanel.reveal(vscode.ViewColumn.Beside); + updatePreviewHtml(context, existingPanel, title, specJson); + return; + } + + log(`Preview panel created: ${title}`); + const panel = vscode.window.createWebviewPanel( + 'ggsql.plotPreview', + title, + vscode.ViewColumn.Beside, + { + enableScripts: true, + retainContextWhenHidden: true, + } + ); + previewPanels.set(key, panel); + panel.onDidDispose(() => { + previewPanels.delete(key); + }); + updatePreviewHtml(context, panel, title, specJson); +} + +function updatePreviewHtml( + context: vscode.ExtensionContext, + panel: vscode.WebviewPanel, + title: string, + specJson: string +): void { + try { + const rendererScriptUri = panel.webview.asWebviewUri( + vscode.Uri.joinPath(context.extensionUri, 'out', 'previewWebview.js') + ); + panel.webview.html = buildPreviewHtml(specJson, { + cspSource: panel.webview.cspSource, + rendererScriptUri: rendererScriptUri.toString(), + title, + }); + log(`Preview HTML updated: ${title}`); + } catch (error) { + panel.dispose(); + logRenderError(error); + vscode.window.showErrorMessage(`Failed to open ggsql preview: ${errorMessage(error)}`); + } +} + +function renderErrorMessage(error: unknown, cliPath: string): string { + const err = error as NodeJS.ErrnoException & { stderr?: string }; + if (err.code === 'ENOENT') { + void showMissingCliMessage(cliPath); + return `Could not find ggsql CLI at '${cliPath}'. Install ggsql, set ggsql.cliPath, or add ggsql to PATH.`; + } + + const stderr = typeof err.stderr === 'string' ? err.stderr.trim() : ''; + return stderr || errorMessage(error); +} + +function logRenderError(error: unknown): void { + log(errorMessage(error)); + const err = error as { stderr?: string; stdout?: string }; + if (typeof err.stdout === 'string' && err.stdout.trim()) { + log(err.stdout.trim()); + } + if (typeof err.stderr === 'string' && err.stderr.trim()) { + log(err.stderr.trim()); + } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function formatCommand(command: string, args: string[]): string { + return [command, ...args].map(formatCommandArg).join(' '); +} + +function formatCommandArg(arg: string): string { + if (/^[A-Za-z0-9_./:=@-]+$/.test(arg)) { + return arg; + } + + return JSON.stringify(arg); +} + +async function checkCliAvailability(): Promise { + const config = vscode.workspace.getConfiguration('ggsql'); + const cliPath = getGgsqlCliPath(config); + try { + const result = await execFile(cliPath, ['--version'], { + timeout: 5000, + maxBuffer: 1024 * 1024, + }); + const version = result.stdout.trim() || result.stderr.trim(); + if (version) { + log(`Found ggsql CLI: ${version}`); + } + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err.code === 'ENOENT') { + await showMissingCliMessage(cliPath); + } else { + log(`Could not verify ggsql CLI '${cliPath}': ${errorMessage(error)}`); + } + } +} + +async function showMissingCliMessage(cliPath: string): Promise { + const selection = await vscode.window.showWarningMessage( + `ggsql CLI not found at '${cliPath}'. Render Plot needs the ggsql CLI.`, + 'Install ggsql', + 'Set CLI Path' + ); + + if (selection === 'Install ggsql') { + await vscode.env.openExternal(vscode.Uri.parse(INSTALL_URL)); + } else if (selection === 'Set CLI Path') { + await vscode.commands.executeCommand('workbench.action.openSettings', 'ggsql.cliPath'); + } +} diff --git a/ggsql-vscode/src/preview.ts b/ggsql-vscode/src/preview.ts new file mode 100644 index 00000000..2045f373 --- /dev/null +++ b/ggsql-vscode/src/preview.ts @@ -0,0 +1,169 @@ +export const DEFAULT_READER_URI = 'duckdb://memory'; + +interface ConfigReader { + get(name: string, defaultValue: T): T; +} + +interface PreviewHtmlOptions { + cspSource: string; + rendererScriptUri: string; + nonce?: string; + title?: string; +} + +export function getGgsqlCliPath(config: ConfigReader): string { + const configuredPath = config.get('cliPath', '').trim(); + return configuredPath || 'ggsql'; +} + +export function getReaderUri(config: ConfigReader): string { + const readerUri = config.get('readerUri', DEFAULT_READER_URI).trim(); + return readerUri || DEFAULT_READER_URI; +} + +export function buildRenderArgs(queryFile: string, outputFile: string, readerUri: string): string[] { + return [ + 'run', + queryFile, + '--reader', + readerUri, + '--writer', + 'vegalite', + '--output', + outputFile, + ]; +} + +export function buildPreviewHtml(specJson: string, options: PreviewHtmlOptions): string { + const parsedSpec = JSON.parse(specJson) as unknown; + const title = options.title ?? 'ggsql Plot'; + const nonce = options.nonce ?? createNonce(); + const encodedSpec = escapeForScript(JSON.stringify(parsedSpec)); + const safeTitle = escapeHtml(title); + + return ` + + + + + + ${safeTitle} + + + +
+
+

${safeTitle}

+
+
+
+

+        
+
+ + + +`; +} + +function createNonce(): string { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let nonce = ''; + for (let i = 0; i < 32; i++) { + nonce += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); + } + return nonce; +} + +function escapeForScript(value: string): string { + return value + .replace(//g, '\\u003e') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/ggsql-vscode/src/previewWebview.ts b/ggsql-vscode/src/previewWebview.ts new file mode 100644 index 00000000..263303c4 --- /dev/null +++ b/ggsql-vscode/src/previewWebview.ts @@ -0,0 +1,88 @@ +type VegaLiteSpec = Record & { + autosize?: unknown; + height?: unknown; + width?: unknown; +}; + +type EmbedPlot = ( + selector: string, + spec: VegaLiteSpec, + options: { actions: boolean; ast: boolean; renderer: 'svg' } +) => Promise; + +async function renderPlot(): Promise { + const specElement = document.getElementById('ggsql-spec'); + if (!specElement?.textContent) { + throw new Error('Missing Vega-Lite spec.'); + } + + const { default: embed } = await import('vega-embed'); + const spec = sizeSpec(JSON.parse(specElement.textContent) as VegaLiteSpec); + const embedPlot = embed as unknown as EmbedPlot; + resetRenderTarget(); + await embedPlot('#vis', spec, { + actions: true, + ast: true, + renderer: 'svg', + }); +} + +function sizeSpec(spec: VegaLiteSpec): VegaLiteSpec { + const { width, height } = getChartSize(); + const resolvedWidth = spec.width ?? width; + const resolvedHeight = spec.height ?? height; + + return { + ...spec, + autosize: spec.autosize ?? { type: 'fit', contains: 'padding', resize: true }, + width: resolveDimension(resolvedWidth, width), + height: resolveDimension(resolvedHeight, height), + }; +} + +function resolveDimension(dimension: unknown, fallback: number): unknown { + return dimension === 'container' ? fallback : dimension; +} + +function getChartSize(): { width: number; height: number } { + const chartView = document.getElementById('chart-view'); + const rect = chartView?.getBoundingClientRect(); + const width = Math.max(320, Math.floor((rect?.width ?? window.innerWidth) - 32)); + const height = Math.max(280, Math.floor((rect?.height ?? window.innerHeight) - 32)); + + return { width, height }; +} + +function resetRenderTarget(): void { + const vis = document.getElementById('vis'); + if (vis) { + vis.replaceChildren(); + } + + const errorEl = document.getElementById('render-error'); + if (errorEl) { + errorEl.style.display = 'none'; + errorEl.textContent = ''; + } +} + +function showError(error: unknown): void { + const errorEl = document.getElementById('render-error'); + if (!errorEl) { + return; + } + errorEl.style.display = 'block'; + errorEl.textContent = error instanceof Error ? error.message : String(error); +} + +let resizeHandle: ReturnType | undefined; +window.addEventListener('resize', () => { + if (resizeHandle) { + clearTimeout(resizeHandle); + } + resizeHandle = setTimeout(() => { + renderPlot().catch(showError); + }, 150); +}); + +renderPlot().catch(showError); diff --git a/ggsql-vscode/test/preview.test.ts b/ggsql-vscode/test/preview.test.ts new file mode 100644 index 00000000..fbd24a44 --- /dev/null +++ b/ggsql-vscode/test/preview.test.ts @@ -0,0 +1,134 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; +import { + buildPreviewHtml, + buildRenderArgs, + getGgsqlCliPath, + getReaderUri, +} from '../src/preview'; + +test('buildRenderArgs runs ggsql with file input and Vega-Lite output', () => { + assert.deepEqual( + buildRenderArgs('/tmp/query.ggsql', '/tmp/plot.vl.json', 'duckdb://memory'), + [ + 'run', + '/tmp/query.ggsql', + '--reader', + 'duckdb://memory', + '--writer', + 'vegalite', + '--output', + '/tmp/plot.vl.json', + ], + ); +}); + +test('preview settings fall back to ggsql binary and in-memory DuckDB', () => { + const emptyConfig = { + get(_name: string, fallback: T): T { + return fallback; + }, + }; + + assert.equal(getGgsqlCliPath(emptyConfig), 'ggsql'); + assert.equal(getReaderUri(emptyConfig), 'duckdb://memory'); +}); + +test('buildPreviewHtml embeds a Vega-Lite spec without raw script-breaking text', () => { + const spec = JSON.stringify({ + title: '', + data: { values: [{ x: 1, y: 2 }] }, + mark: 'point', + encoding: { + x: { field: 'x', type: 'quantitative' }, + y: { field: 'y', type: 'quantitative' }, + }, + }); + + const html = buildPreviewHtml(spec, { + cspSource: 'vscode-resource:', + rendererScriptUri: 'vscode-resource:/out/previewWebview.js', + nonce: 'test-nonce', + title: 'ggsql test plot', + }); + + assert.match(html, /script-src 'nonce-test-nonce' vscode-resource:/); + assert.match(html, /ggsql test plot/); + assert(!html.includes(' { + const html = buildPreviewHtml('{"mark":"point","data":{"values":[]}}', { + cspSource: 'vscode-resource:', + rendererScriptUri: 'vscode-resource:/out/previewWebview.js', + nonce: 'test-nonce', + title: 'ggsql test plot', + }); + + assert.match(html, /id="chart-view"/); + assert(!html.includes('data-mode="code"')); + assert(!html.includes('data-mode="both"')); + assert(!html.includes('Generated Vega-Lite JSON')); +}); + +test('extension uses real SVG icon files for top-level render action', () => { + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const renderCommand = packageJson.contributes.commands.find( + (command: { command: string }) => command.command === 'ggsql.renderPlot' + ); + const renderMenu = packageJson.contributes.menus['editor/title'].find( + (item: { command: string }) => item.command === 'ggsql.renderPlot' + ); + + assert.deepEqual(renderCommand.icon, { + light: './resources/render-plot-light.svg', + dark: './resources/render-plot-dark.svg', + }); + assert.equal(fs.existsSync('resources/render-plot-light.svg'), true); + assert.equal(fs.existsSync('resources/render-plot-dark.svg'), true); + assert.equal(renderMenu.group, 'navigation@-1'); +}); + +test('webview renderer uses Vega AST interpreter for CSP-safe rendering', () => { + const source = fs.readFileSync('src/previewWebview.ts', 'utf8'); + + assert.match(source, /ast:\s*true/); + assert(!source.includes('unsafe-eval')); +}); + +test('webview renderer fits chart to available preview area', () => { + const source = fs.readFileSync('src/previewWebview.ts', 'utf8'); + + assert.match(source, /getBoundingClientRect/); + assert.match(source, /autosize:\s*spec\.autosize\s*\?\?/); + assert.match(source, /type:\s*'fit'/); + assert.match(source, /resize:\s*true/); + assert.match(source, /const resolvedWidth = spec\.width \?\? width/); + assert.match(source, /const resolvedHeight = spec\.height \?\? height/); + assert.match(source, /width:\s*resolveDimension\(resolvedWidth, width\)/); + assert.match(source, /height:\s*resolveDimension\(resolvedHeight, height\)/); +}); + +test('webview renderer resolves Vega-Lite container dimensions to preview pixels', () => { + const source = fs.readFileSync('src/previewWebview.ts', 'utf8'); + + assert.match(source, /resolveDimension/); + assert.match(source, /dimension === 'container'/); +}); + +test('render command writes detailed diagnostics to the ggsql output channel', () => { + const source = fs.readFileSync('src/extension.ts', 'utf8'); + + assert.match(source, /outputChannel\.show\(true\)/); + assert.match(source, /Render Plot clicked/); + assert.match(source, /Working directory:/); + assert.match(source, /Query temp file:/); + assert.match(source, /Vega-Lite output file:/); + assert.match(source, /ggsql CLI exited/); + assert.match(source, /Preview panel/); +}); diff --git a/ggsql-vscode/tsconfig.json b/ggsql-vscode/tsconfig.json index bbbb17e4..a176b01c 100644 --- a/ggsql-vscode/tsconfig.json +++ b/ggsql-vscode/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "module": "Node16", "target": "ES2022", - "lib": ["ES2022"], + "lib": ["ES2022", "DOM"], "moduleResolution": "Node16", "outDir": "out", "sourceMap": true, diff --git a/ggsql-vscode/tsconfig.test.json b/ggsql-vscode/tsconfig.test.json new file mode 100644 index 00000000..03577848 --- /dev/null +++ b/ggsql-vscode/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "out-test", + "rootDir": ".", + "sourceMap": false + }, + "include": ["src/preview.ts", "test/**/*.ts"] +}