diff --git a/bases/rsptx/interactives/CONTRIBUTING.rst b/bases/rsptx/interactives/CONTRIBUTING.rst index 30e2dc5cf..ddcf7dcac 100644 --- a/bases/rsptx/interactives/CONTRIBUTING.rst +++ b/bases/rsptx/interactives/CONTRIBUTING.rst @@ -19,8 +19,11 @@ Coding Standards * We use black to format all of our Python code. * No Tabs in Javascript files (4 spaces = 1 indentation) * We use Prettier to format our Javascript - please make sure you configure prettier to use 4 spaces as the tab size. -* Avoid proliferation of jQuery versions. Make your stuff compatible with the version - of jQuery in the common folder. +* Do not use jQuery in new code, and do not add new jQuery plugins. We are + actively migrating components to plain DOM APIs (activecode and + runestonebase are already jQuery-free). The helpers in + ``runestone/common/js/domutil.js`` (``getDataValue``, ``toggleDisplay``) + cover the common jQuery idioms. * Avoid proliferation of additional third party javascript modules. We are already out of control in this regard and it would be nice to rein it in. * To add JavaScript for a new component, follow the directions in `webpack.index.js` for dynamically-loaded components. @@ -36,7 +39,12 @@ Coding Standards Unit Testing ------------ -We are using Selenium to create unit tests for each of the components. Nearly every component has selenium tests now. If you add a feature or a new component, please make sure to include a selenium test that verifies it works. +JavaScript unit tests use `vitest `_ with a jsdom +environment; run them with ``npm test`` from ``bases/rsptx/interactives``. +Full instructions for running and writing tests are in the *Unit tests* +section of ``docs/source/javascript_feature.rst``. If you add a feature or +a new component, please include tests that verify it works. There are also +older Selenium-based tests for many components; new tests should use vitest. Provide an example ------------------ diff --git a/bases/rsptx/interactives/package-lock.json b/bases/rsptx/interactives/package-lock.json index 50bd10e17..3a4e86d83 100644 --- a/bases/rsptx/interactives/package-lock.json +++ b/bases/rsptx/interactives/package-lock.json @@ -36,11 +36,14 @@ "css-minimizer-webpack-plugin": "^3.0.0", "html-loader": "^3.0.0", "html-webpack-plugin": "^5.0.0", + "jquery": "^3.7.1", + "jsdom": "^29.1.1", "less": "^4.1.3", "less-loader": "^11.1.0", "mini-css-extract-plugin": "^2.0.0", "ts-loader": "^9.5.7", "typescript": "^6.0.3", + "vitest": "^4.1.9", "webpack": "^5.97.1", "webpack-bundle-analyzer": "^4.0.0", "webpack-cli": "^4.10.0" @@ -64,6 +67,72 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -1934,6 +2003,147 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@bramus/specificity/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@bramus/specificity/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1943,6 +2153,54 @@ "node": ">=10.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@handsontable/formulajs": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@handsontable/formulajs/-/formulajs-2.0.2.tgz", @@ -1998,9 +2256,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -2258,6 +2516,24 @@ "node": ">=10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2293,99 +2569,471 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10.13.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-stable-stringify": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz", - "integrity": "sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==" - }, - "node_modules/@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", - "dev": true - }, - "node_modules/@types/pikaday": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@types/pikaday/-/pikaday-1.6.0.tgz", - "integrity": "sha512-cnKjF7i6oA1ADxQdSWHcEStLZeiH8qbf6l7B9O88PhLgnmbUMM62ali0/PaDtINm6ezpNcqtERWL6Y+pAgHKQQ==", - "dependencies": { - "moment": ">=2.14.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "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/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-stable-stringify": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz", + "integrity": "sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==" + }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/pikaday": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/pikaday/-/pikaday-1.6.0.tgz", + "integrity": "sha512-cnKjF7i6oA1ADxQdSWHcEStLZeiH8qbf6l7B9O88PhLgnmbUMM62ali0/PaDtINm6ezpNcqtERWL6Y+pAgHKQQ==", + "dependencies": { + "moment": ">=2.14.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", @@ -2746,6 +3394,15 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -2891,6 +3548,28 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true + }, "node_modules/bessel": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz", @@ -2899,6 +3578,15 @@ "node": ">=0.8" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3018,6 +3706,35 @@ "node": ">=12" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3091,6 +3808,32 @@ "resolved": "https://registry.npmjs.org/btm-expressions/-/btm-expressions-0.1.12.tgz", "integrity": "sha512-X/IK6RxA+vm9XPvoweK2AYFLxpHcpfExM15v12eA4+0ibf4irI/FBz2iDcllOIWnIBmRVeoXZww72GHQXTiqOw==" }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3155,18 +3898,19 @@ "license": "CC-BY-4.0" }, "node_modules/canvas": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-1.6.13.tgz", - "integrity": "sha512-XAfzfEOHZ3JIPjEV+WSI6PpISgUta3dgmndWbsajotz+0TQOX/jDpp2kawjRERatOGv9sMMzk5auB3GKEKA6hg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "peer": true, "dependencies": { - "nan": "^2.10.0" + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" }, "engines": { - "node": ">=0.8.0" + "node": "^18.12.0 || >= 20.9.0" } }, "node_modules/canvas-prebuilt": { @@ -3181,6 +3925,15 @@ "node-pre-gyp": "^0.10.0" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3953,6 +4706,54 @@ "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -3967,7 +4768,30 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/deep-extend": { @@ -4235,6 +5059,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4331,6 +5164,26 @@ "which": "bin/which" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4455,6 +5308,14 @@ "node": ">=8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -4484,6 +5345,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -4559,6 +5434,14 @@ "node": ">=6" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4713,6 +5596,18 @@ "tiny-emitter": "^2.1.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-loader": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-3.1.2.tgz", @@ -4852,6 +5747,28 @@ "postcss": "^8.1.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true + }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -5013,6 +5930,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -5071,6 +5994,12 @@ "jsuites": "^2.7.1" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true + }, "node_modules/jquery-ui": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.10.4.tgz", @@ -5082,6 +6011,157 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5263,7 +6343,265 @@ "needle": "bin/needle" }, "engines": { - "node": ">= 4.4.x" + "node": ">= 4.4.x" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/lilconfig": { @@ -5363,6 +6701,15 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -5498,6 +6845,20 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", @@ -5584,6 +6945,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/moment": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", @@ -5608,18 +6977,23 @@ "optional": true }, "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", - "license": "MIT", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", "optional": true, "peer": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5627,6 +7001,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/needle": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", @@ -5667,6 +7049,28 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5869,6 +7273,19 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6076,6 +7493,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6126,9 +7549,9 @@ } }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -6138,12 +7561,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -6632,6 +8059,46 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -6667,9 +8134,9 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -6936,6 +8403,39 @@ "rimraf": "bin.js" } }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6994,6 +8494,18 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -7081,11 +8593,66 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sirv": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -7124,9 +8691,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7154,6 +8721,18 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -7302,6 +8881,12 @@ "node": ">= 10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -7331,6 +8916,54 @@ "node": ">=4.5" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7393,17 +9026,104 @@ } } }, - "node_modules/terser/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==", + "node_modules/terser/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 + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.6.tgz", + "integrity": "sha512-rbP0Gyx8b3Ae9yO//CU2wbSnQNoQ66m1nJdSbSHmnwKwzkkz/u8mERYU8T2rmlmy+bJvRNn84yNCW8gYqox44Q==", + "dev": true, + "dependencies": { + "tldts-core": "^7.4.6" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.6.tgz", + "integrity": "sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==", "dev": true }, - "node_modules/tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -7452,6 +9172,18 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7497,6 +9229,20 @@ "resolved": "https://registry.npmjs.org/tspan/-/tspan-0.4.0.tgz", "integrity": "sha512-0ELL9tpLpTqLliFyQySaxgCO43buCML+j3TI4E1LuSI8wkzITGEVhZCyMvv/A+3ek9KpgALhhgnZESRLTbN+iw==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -7510,6 +9256,21 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -8157,6 +9918,259 @@ "vega-util": "^1.7.0" } }, + "node_modules/vega/node_modules/canvas": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-1.6.13.tgz", + "integrity": "sha512-XAfzfEOHZ3JIPjEV+WSI6PpISgUta3dgmndWbsajotz+0TQOX/jDpp2kawjRERatOGv9sMMzk5auB3GKEKA6hg==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "nan": "^2.10.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -8348,6 +10362,15 @@ "node": ">=10.13.0" } }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -8377,6 +10400,22 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -8448,6 +10487,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -8604,6 +10658,62 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "dependencies": { + "css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "requires": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + } + }, + "mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + } + } + }, + "@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true + }, + "@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -9917,12 +12027,113 @@ "to-fast-properties": "^2.0.0" } }, + "@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "requires": { + "css-tree": "^3.0.0" + }, + "dependencies": { + "css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "requires": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + } + }, + "mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + } + } + }, + "@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true + }, + "@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "requires": {} + }, + "@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "requires": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + } + }, + "@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "requires": {} + }, "@handsontable/formulajs": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@handsontable/formulajs/-/formulajs-2.0.2.tgz", @@ -9966,9 +12177,9 @@ } }, "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "@jridgewell/trace-mapping": { @@ -10161,6 +12372,16 @@ } } }, + "@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "optional": true, + "requires": { + "@tybys/wasm-util": "^0.10.3" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10187,18 +12408,172 @@ "fastq": "^1.6.0" } }, + "@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true + }, "@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "dev": true, + "optional": true + }, + "@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "dev": true, + "optional": true + }, + "@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "dev": true, + "optional": true + }, + "@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "dev": true, + "optional": true + }, + "@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "dev": true, + "optional": true + }, + "@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "dev": true, + "optional": true + }, + "@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + } + }, + "@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "dev": true, + "optional": true + }, + "@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "dev": true, + "optional": true + }, + "@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true + }, + "@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true }, + "@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "requires": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -10243,10 +12618,13 @@ "integrity": "sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==" }, "@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", - "dev": true + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "requires": { + "undici-types": "~8.3.0" + } }, "@types/pikaday": { "version": "1.6.0", @@ -10256,6 +12634,68 @@ "moment": ">=2.14.0" } }, + "@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "requires": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + } + }, + "@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "requires": { + "tinyrainbow": "^3.1.0" + } + }, + "@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "requires": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + } + }, + "@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "requires": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + } + }, + "@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true + }, + "@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "requires": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + } + }, "@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -10549,6 +12989,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, "babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -10657,11 +13103,28 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "optional": true, + "peer": true + }, "bessel": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz", "integrity": "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==" }, + "bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "requires": { + "require-from-string": "^2.0.2" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -10750,6 +13213,34 @@ } } }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -10796,6 +13287,18 @@ "resolved": "https://registry.npmjs.org/btm-expressions/-/btm-expressions-0.1.12.tgz", "integrity": "sha512-X/IK6RxA+vm9XPvoweK2AYFLxpHcpfExM15v12eA4+0ibf4irI/FBz2iDcllOIWnIBmRVeoXZww72GHQXTiqOw==" }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -10842,13 +13345,15 @@ "dev": true }, "canvas": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-1.6.13.tgz", - "integrity": "sha512-XAfzfEOHZ3JIPjEV+WSI6PpISgUta3dgmndWbsajotz+0TQOX/jDpp2kawjRERatOGv9sMMzk5auB3GKEKA6hg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "dev": true, "optional": true, "peer": true, "requires": { - "nan": "^2.10.0" + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" } }, "canvas-prebuilt": { @@ -10861,6 +13366,12 @@ "node-pre-gyp": "^0.10.0" } }, + "chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11452,6 +13963,44 @@ "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" }, + "data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "requires": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "dependencies": { + "tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true + }, + "whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + } + } + } + }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -11466,6 +14015,23 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" }, + "decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -11662,6 +14228,15 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -11733,6 +14308,20 @@ } } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true, + "peer": true + }, + "expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -11831,6 +14420,14 @@ "path-exists": "^4.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "optional": true, + "peer": true + }, "fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -11856,6 +14453,13 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -11917,6 +14521,14 @@ "pump": "^3.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "optional": true, + "peer": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -12032,6 +14644,15 @@ "tiny-emitter": "^2.1.0" } }, + "html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.6.0" + } + }, "html-loader": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-3.1.2.tgz", @@ -12121,6 +14742,14 @@ "dev": true, "requires": {} }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "optional": true, + "peer": true + }, "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -12238,6 +14867,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -12286,6 +14921,12 @@ "jsuites": "^2.7.1" } }, + "jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true + }, "jquery-ui": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.10.4.tgz", @@ -12297,6 +14938,107 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "requires": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "dependencies": { + "@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "requires": {} + }, + "css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "requires": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + } + }, + "entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true + }, + "lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true + }, + "mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, + "parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "requires": { + "entities": "^8.0.0" + } + }, + "tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true + }, + "whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + } + } + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -12429,6 +15171,111 @@ "klona": "^2.0.4" } }, + "lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "requires": { + "detect-libc": "^2.0.3", + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + }, + "dependencies": { + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true + } + } + }, + "lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "dev": true, + "optional": true + }, + "lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "dev": true, + "optional": true + }, + "lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "dev": true, + "optional": true + }, + "lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "dev": true, + "optional": true + }, + "lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "dev": true, + "optional": true + }, + "lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "dev": true, + "optional": true + }, + "lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "dev": true, + "optional": true + }, + "lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "dev": true, + "optional": true + }, "lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -12511,6 +15358,15 @@ "yallist": "^4.0.0" } }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -12609,6 +15465,14 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true, + "peer": true + }, "mini-css-extract-plugin": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", @@ -12673,6 +15537,14 @@ "minimist": "^1.2.6" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "optional": true, + "peer": true + }, "moment": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", @@ -12691,18 +15563,26 @@ "optional": true }, "nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", "optional": true, "peer": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "optional": true, + "peer": true + }, "needle": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", @@ -12736,6 +15616,25 @@ "tslib": "^2.0.3" } }, + "node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true, + "peer": true + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -12885,6 +15784,12 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -13040,6 +15945,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13077,14 +15988,14 @@ } }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" } }, "postcss-calc": { @@ -13390,6 +16301,38 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "optional": true, + "peer": true + } + } + }, "pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -13424,9 +16367,9 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "queue-microtask": { @@ -13629,6 +16572,31 @@ "glob": "^7.1.3" } }, + "rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "requires": { + "@oxc-project/types": "=0.138.0", + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4", + "@rolldown/pluginutils": "^1.0.0" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13659,6 +16627,15 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -13722,11 +16699,38 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "optional": true, + "peer": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "sirv": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -13756,9 +16760,9 @@ "dev": true }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true }, "source-map-support": { @@ -13782,6 +16786,18 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "dev": true }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13890,6 +16906,12 @@ } } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -13921,6 +16943,50 @@ } } }, + "tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "terser": { "version": "5.37.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", @@ -13959,6 +17025,64 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, + "tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true + }, + "tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true + } + } + }, + "tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true + }, + "tldts": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.6.tgz", + "integrity": "sha512-rbP0Gyx8b3Ae9yO//CU2wbSnQNoQ66m1nJdSbSHmnwKwzkkz/u8mERYU8T2rmlmy+bJvRNn84yNCW8gYqox44Q==", + "dev": true, + "requires": { + "tldts-core": "^7.4.6" + } + }, + "tldts-core": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.6.tgz", + "integrity": "sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -13995,6 +17119,15 @@ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "dev": true }, + "tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "requires": { + "tldts": "^7.0.5" + } + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14032,12 +17165,35 @@ "resolved": "https://registry.npmjs.org/tspan/-/tspan-0.4.0.tgz", "integrity": "sha512-0ELL9tpLpTqLliFyQySaxgCO43buCML+j3TI4E1LuSI8wkzITGEVhZCyMvv/A+3ek9KpgALhhgnZESRLTbN+iw==" }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true }, + "undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true + }, + "undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -14113,6 +17269,18 @@ "vega-parser": "3.9.0", "vega-typings": "*", "yargs": "12" + }, + "dependencies": { + "canvas": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-1.6.13.tgz", + "integrity": "sha512-XAfzfEOHZ3JIPjEV+WSI6PpISgUta3dgmndWbsajotz+0TQOX/jDpp2kawjRERatOGv9sMMzk5auB3GKEKA6hg==", + "optional": true, + "peer": true, + "requires": { + "nan": "^2.10.0" + } + } } }, "vega-canvas": { @@ -14600,6 +17768,90 @@ "vega-util": "^1.7.0" } }, + "vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "requires": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "dependencies": { + "@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "requires": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + } + }, + "es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true + }, + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true + }, + "vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "requires": { + "fsevents": "~2.3.3", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + } + }, + "yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, "watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -14728,6 +17980,12 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true }, + "whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -14751,6 +18009,16 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" }, + "why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -14801,6 +18069,18 @@ "dev": true, "requires": {} }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/bases/rsptx/interactives/package.json b/bases/rsptx/interactives/package.json index 94e867d5b..0135058b7 100644 --- a/bases/rsptx/interactives/package.json +++ b/bases/rsptx/interactives/package.json @@ -3,7 +3,8 @@ "version": "2.0.0", "description": "", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "vitest run", + "test:watch": "vitest", "check-env": "node -e 'console.log(process.env)' | grep npm", "build": "webpack --mode=development", "buildj": "webpack --mode=development --profile --json > stats.json", @@ -26,11 +27,14 @@ "css-minimizer-webpack-plugin": "^3.0.0", "html-loader": "^3.0.0", "html-webpack-plugin": "^5.0.0", + "jquery": "^3.7.1", + "jsdom": "^29.1.1", "less": "^4.1.3", "less-loader": "^11.1.0", "mini-css-extract-plugin": "^2.0.0", "ts-loader": "^9.5.7", "typescript": "^6.0.3", + "vitest": "^4.1.9", "webpack": "^5.97.1", "webpack-bundle-analyzer": "^4.0.0", "webpack-cli": "^4.10.0" diff --git a/bases/rsptx/interactives/runestone/activecode/css/activecode.css b/bases/rsptx/interactives/runestone/activecode/css/activecode.css index c3cdd9725..d75eac8a6 100644 --- a/bases/rsptx/interactives/runestone/activecode/css/activecode.css +++ b/bases/rsptx/interactives/runestone/activecode/css/activecode.css @@ -147,6 +147,15 @@ height: auto; } +/* Editors are user-resizable via the native CSS resize handle (a + * ResizeObserver in activecode.js refreshes CodeMirror on change). + * Once the user drags the handle the browser sets an explicit + * height, which overrides height: auto above. */ +.CodeMirror.ac-resizable { + resize: both; + overflow: hidden; +} + .CodeMirror-scroll { /* calculated height is designed so editor panel * will fit on screen (with space for navbar and diff --git a/bases/rsptx/interactives/runestone/activecode/js/acfactory.js b/bases/rsptx/interactives/runestone/activecode/js/acfactory.js index bde8d34e4..ee34679df 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/acfactory.js +++ b/bases/rsptx/interactives/runestone/activecode/js/acfactory.js @@ -10,7 +10,6 @@ import { TimedHTMLActiveCode, TimedSQLActiveCode, } from "./timed_activecode"; -import "../../common/js/jquery.highlight.js"; export default class ACFactory { constructor() { diff --git a/bases/rsptx/interactives/runestone/activecode/js/activecode.js b/bases/rsptx/interactives/runestone/activecode/js/activecode.js index a4f735001..914dbcc52 100755 --- a/bases/rsptx/interactives/runestone/activecode/js/activecode.js +++ b/bases/rsptx/interactives/runestone/activecode/js/activecode.js @@ -9,6 +9,7 @@ import RunestoneBase from "../../common/js/runestonebase.js"; import { t } from "../../common/js/rsi18n.js"; +import { getDataValue, toggleDisplay } from "../../common/js/domutil.js"; import AudioTour from "./audiotour.js"; import "./activecode-i18n.en.js"; import "./activecode-i18n.pt-br.js"; @@ -72,7 +73,7 @@ const codeIndent = () => export class ActiveCode extends RunestoneBase { constructor(opts) { super(opts); - var orig = $(opts.orig).find("textarea")[0]; + var orig = opts.orig.querySelector("textarea"); this.containerDiv = opts.orig; this.useRunestoneServices = opts.useRunestoneServices; this.python3 = true; @@ -81,25 +82,25 @@ export class ActiveCode extends RunestoneBase { this.codeCoachList = []; //list of CodeCoaches that will be used to provide feedback this.divid = opts.orig.id; this.problem_description = - $(opts.orig).find(".ac_question").text() || ""; - this.code = $(orig).text() || "\n\n\n\n\n"; - this.parsonspersonalize = $(orig).data("parsonspersonalize"); // CodeTailor: Allows the instructor to choose personalization level: solution-level or block-and-solution level - this.parsonsexample = $(orig).data("parsonsexample"); // CodeTailor: Allows the instructor to choose an example Parsons problem or LLM-example (default auto-fill, means the example is generated by LLM) + opts.orig.querySelector(".ac_question")?.textContent || ""; + this.code = orig.textContent || "\n\n\n\n\n"; + this.parsonspersonalize = getDataValue(orig, "parsonspersonalize"); // CodeTailor: Allows the instructor to choose personalization level: solution-level or block-and-solution level + this.parsonsexample = getDataValue(orig, "parsonsexample"); // CodeTailor: Allows the instructor to choose an example Parsons problem or LLM-example (default auto-fill, means the example is generated by LLM) // true by default; only false when explicitly set this.parsonsPersonalized = - $(orig).data("parsons-personalized") !== false && - $(orig).data("parsons-personalized") !== "false"; + getDataValue(orig, "parsons-personalized") !== false && + getDataValue(orig, "parsons-personalized") !== "false"; this.puzzleScaffoldingDivid = `help_puzzle_${this.parsonspersonalize}_${this.divid}`; // CodeTailor: the div id for the puzzle scaffolding help window - this.language = $(orig).data("lang"); - this.timelimit = $(orig).data("timelimit"); - this.highlightLines = $(orig).data("highlight-lines"); - this.includes = $(orig).data("include"); - this.hidecode = $(orig).data("hidecode"); - this.chatcodes = $(orig).data("chatcodes"); - this.hidehistory = $(orig).data("hidehistory"); - this.question = $(opts.orig).find(`#${this.divid}_question`)[0]; - this.tie = $(orig).data("tie"); - this.dburl = $(orig).data("dburl"); + this.language = getDataValue(orig, "lang"); + this.timelimit = getDataValue(orig, "timelimit"); + this.highlightLines = getDataValue(orig, "highlight-lines"); + this.includes = getDataValue(orig, "include"); + this.hidecode = getDataValue(orig, "hidecode"); + this.chatcodes = getDataValue(orig, "chatcodes"); + this.hidehistory = getDataValue(orig, "hidehistory"); + this.question = document.getElementById(`${this.divid}_question`); + this.tie = getDataValue(orig, "tie"); + this.dburl = getDataValue(orig, "dburl"); this.runButton = null; this.enabledownload = this.parseBooleanAttribute( orig, @@ -224,8 +225,8 @@ export class ActiveCode extends RunestoneBase { this.createEditor(); this.createOutput(); this.createControls(); - if ($(orig).data("caption")) { - this.caption = $(orig).data("caption"); + if (getDataValue(orig, "caption")) { + this.caption = getDataValue(orig, "caption"); } else { this.caption = "ActiveCode"; } @@ -246,7 +247,12 @@ export class ActiveCode extends RunestoneBase { this.decorateStatus(); if (this.autorun) { // Simulate pressing the run button, since this will also prevent the user from clicking it until the initial run is complete, and also help the user understand why they're waiting. - $(this.runButtonHandler.bind(this)); + const autorunHandler = this.runButtonHandler.bind(this); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", autorunHandler); + } else { + setTimeout(autorunHandler, 0); + } } this.indicate_component_ready(); if (typeof Prism !== "undefined") { @@ -321,14 +327,16 @@ export class ActiveCode extends RunestoneBase { }); }); - // Make the editor resizable - let ac = this; - $(editor.getWrapperElement()).resizable({ - resize: function () { - editor.setSize($(this).width(), $(this).height()); + // Make the editor resizable via the CSS resize property (see + // activecode.less); CodeMirror just needs a refresh when its + // wrapper changes size. + let wrapper = editor.getWrapperElement(); + wrapper.classList.add("ac-resizable"); + if (typeof ResizeObserver !== "undefined") { + new ResizeObserver(() => { editor.refresh(); - }, - }); + }).observe(wrapper); + } editor.on("keydown", (cm, event) => { // give the user a visual cue that they have changed but not saved editor.getWrapperElement().style.borderTopColor = "#b43232"; @@ -448,7 +456,7 @@ export class ActiveCode extends RunestoneBase { this.setHighlightLines(); if (this.hidecode) { - $(this.codeDiv).css("display", "none"); + this.codeDiv.style.display = "none"; } } @@ -750,13 +758,23 @@ export class ActiveCode extends RunestoneBase { const loadingBarDiv = document.getElementById( "scaffolding-loading-prompt-" + this.divid, ); - $(loadingBarDiv).find("#loading-spinner").remove(); - $(loadingBarDiv) - .find("#encouragement-text") - .text("⚠️ " + timeoutMessage); - $(loadingBarDiv) - .find("#countdown-text") - .text("🫶 Please contact the teaching staff for help!"); + if (loadingBarDiv) { + loadingBarDiv + .querySelector("#loading-spinner") + ?.remove(); + const encouragement = loadingBarDiv.querySelector( + "#encouragement-text", + ); + if (encouragement) { + encouragement.textContent = "⚠️ " + timeoutMessage; + } + const countdown = + loadingBarDiv.querySelector("#countdown-text"); + if (countdown) { + countdown.textContent = + "🫶 Please contact the teaching staff for help!"; + } + } reject("Error in Parsons Scaffolding: " + err); }); }); @@ -817,7 +835,9 @@ export class ActiveCode extends RunestoneBase { } if (window.latestParsonsHelpID == this.divid) { - $(`#scaffolding-container-${this.divid}`).removeClass("hidden"); + document + .getElementById(`scaffolding-container-${this.divid}`) + ?.classList.remove("hidden"); let reopen_act = { type: "receive_help", reopen: true, @@ -931,7 +951,10 @@ export class ActiveCode extends RunestoneBase { // Log the data in parts logActInParts(act); - let probDescHTML = $(this.outerDiv).find(".ac_question").last().html(); + let questionDivs = this.outerDiv.querySelectorAll(".ac_question"); + let probDescHTML = questionDivs.length + ? questionDivs[questionDivs.length - 1].innerHTML + : undefined; if (puzzle_rst !== "correctCode" && puzzle_rst !== "emptyHelpParsons") { var puzzleCode = ` @@ -1010,7 +1033,9 @@ export class ActiveCode extends RunestoneBase { act: JSON.stringify({ ...act }), // Clone to avoid future changes affecting the log div_id: close_divId, }); - $(`#scaffolding-container-${this.divid}`).addClass("hidden"); + document + .getElementById(`scaffolding-container-${this.divid}`) + ?.classList.add("hidden"); }; scaffoldingContainer.appendChild(closeScaffoldingButton); @@ -1035,7 +1060,9 @@ export class ActiveCode extends RunestoneBase { scaffoldingContainer.appendChild(parsonsDiv); } else { // if already exists: remove hidden - $(`#scaffolding-container-${this.divid}`).removeClass("hidden"); + document + .getElementById(`scaffolding-container-${this.divid}`) + ?.classList.remove("hidden"); } if ( @@ -1099,15 +1126,20 @@ export class ActiveCode extends RunestoneBase { act: JSON.stringify({ ...act }), // Clone to avoid future changes affecting the log div_id: this.divid, }); - $(`#copy-answer-button-${this.puzzleScaffoldingDivid}`) - .text("Copied!") - .prop("disabled", true); + let copyButton = document.getElementById( + `copy-answer-button-${this.puzzleScaffoldingDivid}`, + ); + if (copyButton) { + copyButton.textContent = "Copied!"; + copyButton.disabled = true; + } // Set it back to "Copy Answers to Clipboard" after 2 seconds setTimeout(() => { - $(`#copy-answer-button-${this.puzzleScaffoldingDivid}`) - .text("Copy Answer to Clipboard") - .prop("disabled", false); + if (copyButton) { + copyButton.textContent = "Copy Answer to Clipboard"; + copyButton.disabled = false; + } }, 2000); } catch (err) { alert( @@ -1168,12 +1200,9 @@ export class ActiveCode extends RunestoneBase { this.prevHelpedCode = this.editor.getValue(); // Remove any existing loading prompt to reset the countdown - let existingLoadingPrompt = $(this.outerDiv).find( - "#scaffolding-loading-prompt-" + this.divid, - ); - if (existingLoadingPrompt.length) { - existingLoadingPrompt.remove(); - } + this.outerDiv + .querySelector(`[id="scaffolding-loading-prompt-${this.divid}"]`) + ?.remove(); // Create a new loading prompt let loadingPrompt = document.createElement("div"); @@ -1198,7 +1227,7 @@ export class ActiveCode extends RunestoneBase { countdownText.id = "countdown-text"; loadingPrompt.appendChild(countdownText); - $(loadingPrompt).addClass("loading"); + loadingPrompt.classList.add("loading"); loadingPrompt.scrollIntoView(); // Display encouragement message @@ -1240,22 +1269,26 @@ export class ActiveCode extends RunestoneBase { } this.helpText = await this.getParsonsCodeTailor(); - $(this.outerDiv) - .find("#scaffolding-loading-prompt-" + this.divid) - .removeClass("loading"); + this.outerDiv + .querySelector(`[id="scaffolding-loading-prompt-${this.divid}"]`) + ?.classList.remove("loading"); window.latestParsonsHelpID = ""; this.reopenHelpBtnHandler(true); // check whether hold the regenerating button if (!this.helpLoaded) { this.helpLoaded = true; - $(this.outerDiv).find(".reopen-help-btn").removeClass("hide"); + this.outerDiv + .querySelectorAll(".reopen-help-btn") + .forEach((el) => el.classList.remove("hide")); if ( this.parsonspersonalize === "movable" || this.parsonspersonalize === "partial" ) { - $(this.outerDiv) - .find(".parsons-scaffolding-btn") - .text("Regenerate Help"); + this.outerDiv + .querySelectorAll(".parsons-scaffolding-btn") + .forEach((el) => { + el.textContent = "Regenerate Help"; + }); } } } @@ -1264,17 +1297,17 @@ export class ActiveCode extends RunestoneBase { createControls() { var ctrlDiv = document.createElement("div"); var butt; - $(ctrlDiv).addClass("ac_actions"); + ctrlDiv.classList.add("ac_actions"); // Run butt = document.createElement("button"); butt.title = t("msg_activecode_run_button_title"); - $(butt).text(t("msg_activecode_run_code")); - $(butt).addClass("btn btn-success run-button"); + butt.textContent = t("msg_activecode_run_code"); + butt.classList.add("btn", "btn-success", "run-button"); ctrlDiv.appendChild(butt); this.runButton = butt; // console.log("adding click function for run"); this.runButton.onclick = this.runButtonHandler.bind(this); - $(butt).attr("type", "button"); + butt.setAttribute("type", "button"); if (this.enabledownload || eBookConfig.downloadsEnabled) { this.addDownloadButton(ctrlDiv); @@ -1331,7 +1364,7 @@ export class ActiveCode extends RunestoneBase { } // Audio Tour - if ($(this.origElem).data("audio")) { + if (getDataValue(this.origElem, "audio")) { this.enableAudioTours(ctrlDiv); } if (eBookConfig.isInstructor) { @@ -1344,12 +1377,12 @@ export class ActiveCode extends RunestoneBase { this.enableChatCodes(ctrlDiv); } - $(this.outerDiv).prepend(ctrlDiv); + this.outerDiv.prepend(ctrlDiv); if (this.question) { - if ($(this.question).html().match(/^\s+$/)) { - $(this.question).remove(); + if (this.question.innerHTML.match(/^\s+$/)) { + this.question.remove(); } else { - $(this.outerDiv).prepend(this.question); + this.outerDiv.prepend(this.question); } } this.controlDiv = ctrlDiv; @@ -1357,92 +1390,98 @@ export class ActiveCode extends RunestoneBase { addFeedbackButton(ctrlDiv) { let butt = document.createElement("button"); - $(butt).addClass("ac_opt btn btn-default"); - $(butt).text(t("msg_activecode_show_feedback")); - $(butt).css("margin-left", "10px"); - $(butt).attr("type", "button"); + butt.classList.add("ac_opt", "btn", "btn-default"); + butt.textContent = t("msg_activecode_show_feedback"); + butt.style.marginLeft = "10px"; + butt.setAttribute("type", "button"); this.gradeButton = butt; ctrlDiv.appendChild(butt); - $(butt).click(this.createGradeSummary.bind(this)); + butt.addEventListener("click", this.createGradeSummary.bind(this)); } addDownloadButton(ctrlDiv) { let butt = document.createElement("button"); - $(butt).text("Download"); - $(butt).addClass("btn save-button"); + butt.textContent = "Download"; + butt.classList.add("btn", "save-button"); ctrlDiv.appendChild(butt); this.downloadButton = butt; - $(butt).click(this.downloadFile.bind(this, this.language)); - $(butt).attr("type", "button"); + butt.addEventListener( + "click", + this.downloadFile.bind(this, this.language), + ); + butt.setAttribute("type", "button"); } enableHideShow(ctrlDiv) { - $(this.runButton).attr("disabled", "disabled"); + this.runButton.disabled = true; let butt = document.createElement("button"); - $(butt).addClass("ac_opt btn btn-default"); - $(butt).text(t("msg_activecode_show_code")); - $(butt).css("margin-left", "10px"); - $(butt).attr("type", "button"); + butt.classList.add("ac_opt", "btn", "btn-default"); + butt.textContent = t("msg_activecode_show_code"); + butt.style.marginLeft = "10px"; + butt.setAttribute("type", "button"); this.showHideButt = butt; ctrlDiv.appendChild(butt); - $(butt).click( + butt.addEventListener( + "click", function () { - $(this.codeDiv).toggle(); + toggleDisplay(this.codeDiv); if (this.historyScrubber == null) { this.addHistoryScrubber(true); } else { - $(this.historyScrubber.parentElement).toggle(); + toggleDisplay(this.historyScrubber.parentElement); } if ( - $(this.showHideButt).text() == t("msg_activecode_show_code") + this.showHideButt.textContent == + t("msg_activecode_show_code") ) { - $(this.showHideButt).text(t("msg_activecode_hide_code")); - } else { - $(this.showHideButt).text(t("msg_activecode_show_code")); - } - if ($(this.runButton).attr("disabled")) { - $(this.runButton).removeAttr("disabled"); + this.showHideButt.textContent = t( + "msg_activecode_hide_code", + ); } else { - $(this.runButton).attr("disabled", "disabled"); + this.showHideButt.textContent = t( + "msg_activecode_show_code", + ); } + this.runButton.disabled = !this.runButton.disabled; }.bind(this), ); } enableCodeLens(ctrlDiv) { let butt = document.createElement("button"); - $(butt).addClass("ac_opt btn btn-default"); - $(butt).text(t("msg_activecode_show_codelens")); - $(butt).css("margin-left", "10px"); + butt.classList.add("ac_opt", "btn", "btn-default"); + butt.textContent = t("msg_activecode_show_codelens"); + butt.style.marginLeft = "10px"; this.clButton = butt; ctrlDiv.appendChild(butt); - $(butt).click(this.showCodelens.bind(this)); + butt.addEventListener("click", this.showCodelens.bind(this)); } enableReformat(ctrlDiv) { let butt = document.createElement("button"); - $(butt).addClass("ac_opt btn btn-default"); - $(butt).text(t("msg_activecode_reformat")); - $(butt).css("margin-left", "10px"); + butt.classList.add("ac_opt", "btn", "btn-default"); + butt.textContent = t("msg_activecode_reformat"); + butt.style.marginLeft = "10px"; this.reformatButton = butt; ctrlDiv.appendChild(butt); - $(butt).click(this.reformat.bind(this)); + butt.addEventListener("click", this.reformat.bind(this)); } enableAudioTours(ctrlDiv) { let butt = document.createElement("button"); - $(butt).addClass("ac_opt btn btn-default"); - $(butt).text(t("msg_activecode_audio_tour")); - $(butt).css("margin-left", "10px"); + butt.classList.add("ac_opt", "btn", "btn-default"); + butt.textContent = t("msg_activecode_audio_tour"); + butt.style.marginLeft = "10px"; this.atButton = butt; ctrlDiv.appendChild(butt); - $(butt).click( + butt.addEventListener( + "click", function () { new AudioTour( this.divid, this.code, 1, - $(this.origElem).data("audio"), + getDataValue(this.origElem, "audio"), ); }.bind(this), ); @@ -1450,12 +1489,13 @@ export class ActiveCode extends RunestoneBase { enableInstructorSharing(ctrlDiv) { let butt = document.createElement("button"); - $(butt).addClass("btn btn-info"); - $(butt).text("Share Code"); - $(butt).css("margin-left", "10px"); + butt.classList.add("btn", "btn-info"); + butt.textContent = "Share Code"; + butt.style.marginLeft = "10px"; this.shareButt = butt; ctrlDiv.appendChild(butt); - $(butt).click( + butt.addEventListener( + "click", async function () { if ( !confirm( @@ -1508,16 +1548,17 @@ export class ActiveCode extends RunestoneBase { ctrlDiv.appendChild(checkPartner); var plabel = document.createElement("label"); plabel.for = `${this.divid}_part`; - $(plabel).text("Pair?"); + plabel.textContent = "Pair?"; ctrlDiv.appendChild(plabel); - $(checkPartner).click( + checkPartner.addEventListener( + "click", function () { if (this.partner) { this.partner = false; - $(partnerTextBox).hide(); + partnerTextBox.style.display = "none"; this.partner = ""; partnerTextBox.value = ""; - $(plabel).text("Pair?"); + plabel.textContent = "Pair?"; } else { let didAgree = localStorage.getItem("partnerAgree"); if (!didAgree) { @@ -1533,16 +1574,17 @@ export class ActiveCode extends RunestoneBase { } } this.partner = true; - $(plabel).text("with: "); - $(partnerTextBox).show(); + plabel.textContent = "with: "; + partnerTextBox.style.display = ""; } }.bind(this), ); var partnerTextBox = document.createElement("input"); partnerTextBox.type = "text"; ctrlDiv.appendChild(partnerTextBox); - $(partnerTextBox).hide(); - $(partnerTextBox).change( + partnerTextBox.style.display = "none"; + partnerTextBox.addEventListener( + "change", function () { this.partner = partnerTextBox.value; }.bind(this), @@ -1556,47 +1598,46 @@ export class ActiveCode extends RunestoneBase { var channels = document.createElement("span"); var topic = window.location.host + "-" + this.divid; ctrlDiv.appendChild(chatBar); - $(chatBar).text("Chat: "); - $(chatBar).append(channels); + chatBar.textContent = "Chat: "; + chatBar.appendChild(channels); let butt = document.createElement("a"); - $(butt).addClass("ac_opt btn btn-default"); - $(butt).text("Create Channel"); - $(butt).css("margin-left", "10px"); - $(butt).attr("type", "button"); - $(butt).attr("target", "_blank"); - $(butt).attr( + butt.classList.add("ac_opt", "btn", "btn-default"); + butt.textContent = "Create Channel"; + butt.style.marginLeft = "10px"; + butt.setAttribute("type", "button"); + butt.setAttribute("target", "_blank"); + butt.setAttribute( "href", "http://" + chatcodesServer + "/new?" + - $.param({ + new URLSearchParams({ topic: window.location.host + "-" + this.divid, code: this.editor.getValue(), lang: "Python", - }), + }).toString(), ); this.chatButton = butt; chatBar.appendChild(butt); var updateChatCodesChannels = function () { var data = doc.data; var i = 1; - $(channels).html(""); + channels.innerHTML = ""; data["channels"].forEach(function (channel) { if (!channel.archived && topic === channel.topic) { - var link = $(""); - var href = + var link = document.createElement("a"); + link.href = "http://" + chatcodesServer + "/" + channel.channelName; - link.attr({ - href: href, - target: "_blank", - }); - link.text(" " + channel.channelName + "(" + i + ") "); - $(channels).append(link); + link.target = "_blank"; + link.textContent = + " " + channel.channelName + "(" + i + ") "; + channels.appendChild(link); i++; } }); if (i === 1) { - $(channels).text("(no active converstations on this problem)"); + channels.textContent = + "(no active converstations on this problem)"; } }; doc.subscribe(updateChatCodesChannels); @@ -1604,7 +1645,7 @@ export class ActiveCode extends RunestoneBase { } enableSaveLoad() { - $(this.runButton).text(t("msg_activecode_save_run")); + this.runButton.textContent = t("msg_activecode_save_run"); } // _`addHistoryScrubber` @@ -1668,27 +1709,35 @@ export class ActiveCode extends RunestoneBase { renderScrubber(pos_last) { console.log("making a new scrubber"); var scrubberDiv = document.createElement("div"); - $(scrubberDiv).css("display", "inline-block"); - $(scrubberDiv).css("margin-left", "10px"); - $(scrubberDiv).css("margin-right", "10px"); - $(scrubberDiv).css({ - "min-width": "200px", - "max-width": "300px", - }); - var scrubber = document.createElement("div"); + scrubberDiv.classList.add("ac_history_scrubber_box"); + scrubberDiv.style.display = "inline-block"; + scrubberDiv.style.marginLeft = "10px"; + scrubberDiv.style.marginRight = "10px"; + scrubberDiv.style.minWidth = "200px"; + scrubberDiv.style.maxWidth = "300px"; + // A native range input replaces the old jQuery UI slider. + var scrubber = document.createElement("input"); + scrubber.type = "range"; + scrubber.min = 0; + scrubber.max = this.history.length - 1; + scrubber.value = this.history.length - 1; + scrubber.step = 1; + scrubber.classList.add("ac_history_scrubber"); + scrubber.setAttribute("aria-label", "History slider"); this.timestampP = document.createElement("span"); - this.slideit = function (ev, el) { - let submittedCode = this.history[$(scrubber).slider("value")]; + this.slideit = function (ev) { + let pos = Number(scrubber.value); + let submittedCode = this.history[pos]; let code = this.readdLockedCode(submittedCode); this.editor.setValue(code); this.setLockedRegions(); - var curVal = this.timestamps[$(scrubber).slider("value")]; - let pos = $(scrubber).slider("value"); + var curVal = this.timestamps[pos]; let outOf = this.history.length; - $(this.timestampP).text(`${curVal} - ${pos + 1} of ${outOf}`); - // a slide will also result in a slidechange event we don't want to double - // log these events. So do not log the slide until it stops and creates the changed - if (ev !== null && ev.type != "slide") { + this.timestampP.textContent = `${curVal} - ${pos + 1} of ${outOf}`; + // "input" events fire continuously while dragging; only log once + // the position settles (the "change" event, or a programmatic + // call passing null). + if (ev !== null && ev.type != "input") { this.logBookEvent({ event: "activecode", act: "slide:" + curVal, @@ -1697,16 +1746,10 @@ export class ActiveCode extends RunestoneBase { } this.setHighlightLines(); }; - $(scrubber).slider({ - max: this.history.length - 1, - value: this.history.length - 1, - }); - $(scrubber).css("margin", "10px"); - $(scrubber).on("slide", this.slideit.bind(this)); - $(scrubber).on("slidechange", this.slideit.bind(this)); + scrubber.style.margin = "10px"; + scrubber.addEventListener("input", this.slideit.bind(this)); + scrubber.addEventListener("change", this.slideit.bind(this)); scrubberDiv.appendChild(scrubber); - // Add aria-label to the otherwise empty child for scrubber: - scrubber.childNodes[0].setAttribute("aria-label", "History slider"); scrubberDiv.appendChild(this.timestampP); // If there is a deadline set then position the scrubber at the last submission // prior to the deadline @@ -1722,14 +1765,13 @@ export class ActiveCode extends RunestoneBase { } i = i - 1; scrubber.value = Math.max(i, 0); - let submittedCode = this.history[scrubber.value]; + let submittedCode = this.history[Number(scrubber.value)]; let code = this.readdLockedCode(submittedCode); this.editor.setValue(code); this.setLockedRegions(); - $(scrubber).slider("value", scrubber.value); } else if (pos_last) { scrubber.value = this.history.length - 1; - let submittedCode = this.history[scrubber.value]; + let submittedCode = this.history[Number(scrubber.value)]; let code = this.readdLockedCode(submittedCode); this.editor.setValue(code); this.setLockedRegions(); @@ -1737,12 +1779,12 @@ export class ActiveCode extends RunestoneBase { scrubber.value = 0; } this.setHighlightLines(); - let pos = $(scrubber).slider("value"); + let pos = Number(scrubber.value); let outOf = this.history.length; - let ts = this.timestamps[$(scrubber).slider("value")]; - $(this.timestampP).text(`${ts} - ${pos + 1} of ${outOf}`); + let ts = this.timestamps[pos]; + this.timestampP.textContent = `${ts} - ${pos + 1} of ${outOf}`; this.historyScrubber = scrubber; - $(scrubberDiv).insertAfter(this.runButton); + this.runButton.insertAdjacentElement("afterend", scrubberDiv); } // end definition of helper createOutput() { @@ -1750,7 +1792,7 @@ export class ActiveCode extends RunestoneBase { // to hold turtle graphics output. We use a div in case the turtle changes from // using a canvas to using some other element like svg in the future. var outDiv = document.createElement("div"); - $(outDiv).addClass("ac_output"); + outDiv.classList.add("ac_output"); this.outDiv = outDiv; this.output = document.createElement("pre"); this.output.id = this.divid + "_stdout"; @@ -1759,28 +1801,30 @@ export class ActiveCode extends RunestoneBase { this.output.setAttribute("aria-atomic", "true"); this.output.setAttribute("role", "log"); this.output.innerHTML = ""; - $(this.output).css("max-height", "400px"); - $(this.output).css("overflow", "auto"); + this.output.style.maxHeight = "400px"; + this.output.style.overflow = "auto"; this.graphics = document.createElement("div"); this.graphics.id = this.divid + "_graphics"; - $(this.graphics).addClass("ac-canvas"); - // This bit of magic adds an event which waits for a canvas child to be created on our - // newly created div. When a canvas child is added we add a new class so that the visible - // canvas can be styled in CSS. Which a the moment means just adding a border. - $(this.graphics).on( - "DOMNodeInserted", - "canvas", - function () { - $(this.graphics).addClass("visible-ac-canvas"); - }.bind(this), - ); + this.graphics.classList.add("ac-canvas"); + // Watch for a canvas child to be created on our newly created div. + // When a canvas child is added we add a new class so that the visible + // canvas can be styled in CSS. Which at the moment means just adding a border. + const canvasObserver = new MutationObserver(() => { + if (this.graphics.querySelector("canvas")) { + this.graphics.classList.add("visible-ac-canvas"); + } + }); + canvasObserver.observe(this.graphics, { + childList: true, + subtree: true, + }); var coachDiv = document.createElement("div"); coachDiv.classList.add("alert", "alert-warning", "codecoach"); coachDiv.setAttribute("aria-live", "polite"); coachDiv.setAttribute("aria-atomic", "true"); coachDiv.setAttribute("role", "log"); - $(coachDiv).css("display", "none"); + coachDiv.style.display = "none"; let coachHead = coachDiv.appendChild(document.createElement("h3")); coachHead.textContent = t("msg_activecode_code_coach"); this.outerDiv.appendChild(coachDiv); @@ -1792,7 +1836,7 @@ export class ActiveCode extends RunestoneBase { var lensDiv = document.createElement("div"); lensDiv.classList.add("codelens"); lensDiv.id = `${this.divid}_codelens`; - $(lensDiv).css("display", "none"); + lensDiv.style.display = "none"; this.codelens = lensDiv; this.outerDiv.appendChild(lensDiv); // Put the outer container for error messages in the dom @@ -1807,10 +1851,14 @@ export class ActiveCode extends RunestoneBase { } disableSaveLoad() { - $(this.saveButton).addClass("disabled"); - $(this.saveButton).attr("title", "Login to save your code"); - $(this.loadButton).addClass("disabled"); - $(this.loadButton).attr("title", "Login to load your code"); + if (this.saveButton) { + this.saveButton.classList.add("disabled"); + this.saveButton.title = "Login to save your code"; + } + if (this.loadButton) { + this.loadButton.classList.add("disabled"); + this.loadButton.title = "Login to load your code"; + } } computeChangesPerSecond() { @@ -1965,21 +2013,27 @@ export class ActiveCode extends RunestoneBase { } else { body = "

The server did not return any grade information

"; } - var html = ``; + dialog + .querySelector("button.close") + .addEventListener("click", () => dialog.close()); + dialog.addEventListener("close", () => dialog.remove()); + document.body.appendChild(dialog); + if (typeof dialog.showModal === "function") { + dialog.showModal(); + } else { + dialog.setAttribute("open", ""); + } return response; } @@ -2022,7 +2076,7 @@ export class ActiveCode extends RunestoneBase { myVars.codeDivWidth = 350; myVars.codeDivHeight = 400; var srcURL = "https://pythontutor.com/iframe-embed.html"; - var srcVars = $.param(myVars); + var srcVars = new URLSearchParams(myVars).toString(); var embedUrlStr = `${srcURL}#${srcVars}`; var myIframe = document.createElement("iframe"); myIframe.setAttribute("id", this.divid + "_codelens"); @@ -2055,8 +2109,9 @@ export class ActiveCode extends RunestoneBase { // Add the error message this.errLastRun = true; console.log(err); - var errHead = $("

").html("Error"); - this.eContainer.appendChild(errHead[0]); + var errHead = document.createElement("h3"); + errHead.innerHTML = "Error"; + this.eContainer.appendChild(errHead); var errText = this.eContainer.appendChild( document.createElement("pre"), ); @@ -2086,10 +2141,10 @@ Yet another is that there is an internal error. The internal error message is: var to = errString.indexOf(":"); var errName = errString.substring(0, to); errText.innerHTML = errString; - $(this.eContainer).append("

Description

"); + this.eContainer.insertAdjacentHTML("beforeend", "

Description

"); var errDesc = this.eContainer.appendChild(document.createElement("p")); errDesc.innerHTML = errorText[errName]; - $(this.eContainer).append("

To Fix

"); + this.eContainer.insertAdjacentHTML("beforeend", "

To Fix

"); var errFix = this.eContainer.appendChild(document.createElement("p")); errFix.innerHTML = errorText[errName + "Fix"]; var moreInfo = "../ErrorHelp/" + errName.toLowerCase() + ".html"; @@ -2179,13 +2234,22 @@ Yet another is that there is an internal error. The internal error message is: // have these extensions. We don't want to try to load them from the DB. if (!(fileName.endsWith(".js") || fileName.endsWith(".py"))) { let result = null; - $.ajax({ - async: false, - url: `/ns/logger/get_source_code?course_id=${eBookConfig.course}&filename=${fileName}`, - success: function (data) { - result = data.detail.file_contents; - }, - }); + // Skulpt's read hook is synchronous, so this request must be + // synchronous as well (as the old $.ajax async:false call was). + let xhr = new XMLHttpRequest(); + xhr.open( + "GET", + `/ns/logger/get_source_code?course_id=${eBookConfig.course}&filename=${fileName}`, + false, + ); + try { + xhr.send(); + if (xhr.status >= 200 && xhr.status < 300) { + result = JSON.parse(xhr.responseText).detail.file_contents; + } + } catch (e) { + console.log(`failed to fetch ${fileName}: ${e}`); + } if (result) { return result; } else { @@ -2210,12 +2274,18 @@ Yet another is that there is an internal error. The internal error message is: setTimeout( function () { if (this.outputLineCount < 1000) { - $(this.output).append(text); + this.output.insertAdjacentHTML( + "beforeend", + text, + ); this.outputLineCount += 1; resolve(Sk.builtin.none.none$); } else { if (this.outputLineCount == 1000) { - $(this.output).append("Too Much output"); + this.output.insertAdjacentHTML( + "beforeend", + "Too Much output", + ); this.outputLineCount += 1; stopExecution = true; resolve(Sk.builtin.none.none$); @@ -2233,28 +2303,28 @@ Yet another is that there is an internal error. The internal error message is: let filecomponent = document.getElementById(fobj.name); if (!filecomponent) { let container = document.createElement("div"); - $(container).addClass("runestone"); + container.classList.add("runestone"); let tab = document.createElement("div"); - $(tab).addClass("datafile_caption"); + tab.classList.add("datafile_caption"); tab.innerHTML = `Data file: ${fobj.name}`; filecomponent = document.createElement("textarea"); filecomponent.rows = 10; filecomponent.cols = 50; filecomponent.id = fobj.name; - $(filecomponent).css("margin-bottom", "5px"); - $(filecomponent).addClass("ac_output"); + filecomponent.style.marginBottom = "5px"; + filecomponent.classList.add("ac_output"); container.appendChild(tab); container.appendChild(filecomponent); this.outerDiv.appendChild(container); } else { if (fobj.pos$ == 0) { - $(filecomponent).val(""); + filecomponent.value = ""; } } - let current = $(filecomponent).val(); + let current = filecomponent.value; current = current + bytes.v; - $(filecomponent).val(current); - $(filecomponent).css("display", "block"); + filecomponent.value = current; + filecomponent.style.display = "block"; fobj.pos$ = current.length; return current.length; } @@ -2333,24 +2403,16 @@ Yet another is that there is an internal error. The internal error message is: let userCode = this.trimLockedCode(this.editor.getValue()); if ( this.historyScrubber && - this.history[$(this.historyScrubber).slider("value")] != userCode + this.history[Number(this.historyScrubber.value)] != userCode ) { saveCode = "True"; this.history.push(userCode); this.timestamps.push(new Date().toLocaleString()); - $(this.historyScrubber).slider( - "option", - "max", - this.history.length - 1, - ); - $(this.historyScrubber).slider( - "option", - "value", - this.history.length - 1, - ); - // is this needed? changing value in previous statement - // already triggers slideit function - //this.slideit(null); + this.historyScrubber.max = this.history.length - 1; + this.historyScrubber.value = this.history.length - 1; + // Unlike the old jQuery UI slider, setting .value does not fire + // an event, so update the editor and label explicitly. + this.slideit(null); } else { saveCode = "False"; } @@ -2407,8 +2469,10 @@ Yet another is that there is an internal error. The internal error message is: //Run all available code coaches and update code coach div //clear anything after header in codecoach div and hide it - $(this.codecoach).children().slice(1).remove(); - $(this.codecoach).css("display", "none"); + while (this.codecoach.children.length > 1) { + this.codecoach.lastElementChild.remove(); + } + this.codecoach.style.display = "none"; //get code, run coaches //let code = await this.buildProg(false); @@ -2432,7 +2496,7 @@ Yet another is that there is an internal error. The internal error message is: document.createElement("pre"), ); checkPre.textContent = p.value; - $(this.codecoach).css("display", "block"); + this.codecoach.style.display = "block"; // screenreaders seem to miss error message without the delay setTimeout(() => { this.codecoach.append(checkDiv); @@ -2451,12 +2515,13 @@ Yet another is that there is an internal error. The internal error message is: let urDivid = `${this.divid}_offscreen_unit_results`; let unitFeedback = document.getElementById(urDivid); let tmp = document.body.removeChild(unitFeedback); - if ($(this.outerDiv).find(`#${urDivid}`).length > 0) { - tmp = $(this.outerDiv).find(`#${urDivid}`)[0]; + let existing = this.outerDiv.querySelector(`[id="${urDivid}"]`); + if (existing) { + tmp = existing; } else { this.outerDiv.appendChild(tmp); } - $(tmp).show(); + tmp.style.display = ""; } else { let urDivid = this.divid + "_unit_results"; @@ -2475,9 +2540,10 @@ Yet another is that there is an internal error. The internal error message is: } if ( - $(this.outerDiv).find(`#${urDivid}`).length == 0 && - $(this.outerDiv).find(`#${urDivid}_offscreen_unit_results`) - .length == 0 + !this.outerDiv.querySelector(`[id="${urDivid}"]`) && + !this.outerDiv.querySelector( + `[id="${urDivid}_offscreen_unit_results"]`, + ) ) { let urResults = document.getElementById(urDivid); // why do we do this? This doesn't seem to matter for grading... @@ -2536,7 +2602,7 @@ Yet another is that there is an internal error. The internal error message is: var prog = await this.buildProg(true); this.coachCode = prog; this.saveCode = "True"; - $(this.output).text(""); + this.output.textContent = ""; if (this.unit_results_divid) { let urdiv = document.getElementById( this.unit_results_divid + "_unit_results", @@ -2581,9 +2647,10 @@ Yet another is that there is an internal error. The internal error message is: Sk.canvas = this.graphics.id; //todo: get rid of this here and in image if (!noUI) { this.saveCode = await this.manage_scrubber(this.saveCode); - $(this.runButton).attr("disabled", "disabled"); - $(this.historyScrubber).off("slidechange"); - $(this.historyScrubber).slider("disable"); + this.runButton.disabled = true; + if (this.historyScrubber) { + this.historyScrubber.disabled = true; + } this.outDiv.style.visibility = "visible"; } try { @@ -2602,23 +2669,17 @@ Yet another is that there is an internal error. The internal error message is: }, ); if (!noUI) { - if (this.slideit) { - $(this.historyScrubber).on( - "slidechange", - this.slideit.bind(this), - ); + if (this.historyScrubber) { + this.historyScrubber.disabled = false; } - $(this.historyScrubber).slider("enable"); } this.errLastRun = false; this.errinfo = "success"; } catch (err) { if (!noUI) { - $(this.historyScrubber).on( - "slidechange", - this.slideit.bind(this), - ); - $(this.historyScrubber).slider("enable"); + if (this.historyScrubber) { + this.historyScrubber.disabled = false; + } } this.errinfo = err.toString(); this.eContainer.style.visibility = "visible"; @@ -2626,20 +2687,20 @@ Yet another is that there is an internal error. The internal error message is: this.addErrorMessage(err); }, 10); } finally { - $(this.runButton).removeAttr("disabled"); + this.runButton.disabled = false; this.firstAfterRun = true; if (typeof window.allVisualizers != "undefined") { - $.each(window.allVisualizers, function (i, e) { + for (const e of Object.values(window.allVisualizers)) { e.redrawConnectors(); - }); + } } this.editor.acEditEvent = false; } } disableInteraction() { - $(this.runButton).hide(); - $(this.codeDiv).addClass("ac-disabled"); + this.runButton.style.display = "none"; + this.codeDiv.classList.add("ac-disabled"); } } diff --git a/bases/rsptx/interactives/runestone/activecode/js/activecode_html.js b/bases/rsptx/interactives/runestone/activecode/js/activecode_html.js index deecec280..caa576955 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/activecode_html.js +++ b/bases/rsptx/interactives/runestone/activecode/js/activecode_html.js @@ -4,11 +4,11 @@ import { t } from "../../common/js/rsi18n.js"; export default class HTMLActiveCode extends ActiveCode { constructor(opts) { super(opts); - // The base class reads code via $(orig).text() which already decodes HTML entities + // The base class reads code via textContent which already decodes HTML entities // (the browser HTML parser decodes < etc. in textarea content). // We must NOT re-read from origElem.innerHTML here — that would re-include the // suffix that the base class already stripped into this.suffix. - $(this.runButton).text("Render"); + this.runButton.textContent = "Render"; this.editor.setValue(this.code); this._messageHandler = null; } @@ -53,7 +53,7 @@ export default class HTMLActiveCode extends ActiveCode { prog; } - $(this.output).text(""); + this.output.textContent = ""; this.output.srcdoc = prog; if (this.unit_results) { @@ -309,13 +309,13 @@ export default class HTMLActiveCode extends ActiveCode { createOutput() { var outDiv = document.createElement("div"); - $(outDiv).addClass("ac_output"); + outDiv.classList.add("ac_output"); this.outDiv = outDiv; this.output = document.createElement("iframe"); - $(this.output).css("background-color", "white"); - $(this.output).css("position", "relative"); - $(this.output).css("height", "400px"); - $(this.output).css("width", "100%"); + this.output.style.backgroundColor = "white"; + this.output.style.position = "relative"; + this.output.style.height = "400px"; + this.output.style.width = "100%"; outDiv.appendChild(this.output); this.testResultsDiv = document.createElement("div"); @@ -325,11 +325,11 @@ export default class HTMLActiveCode extends ActiveCode { this.outerDiv.appendChild(outDiv); var clearDiv = document.createElement("div"); - $(clearDiv).css("clear", "both"); // needed to make parent div resize properly + clearDiv.style.clear = "both"; // needed to make parent div resize properly this.outerDiv.appendChild(clearDiv); } enableSaveLoad() { - $(this.runButton).text(t("msg_activecode_render")); + this.runButton.textContent = t("msg_activecode_render"); } } diff --git a/bases/rsptx/interactives/runestone/activecode/js/activecode_js.js b/bases/rsptx/interactives/runestone/activecode/js/activecode_js.js index 6a2a3c8be..748b4e868 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/activecode_js.js +++ b/bases/rsptx/interactives/runestone/activecode/js/activecode_js.js @@ -5,7 +5,7 @@ export default class JSActiveCode extends ActiveCode { super(opts); } outputfun(a) { - $(this.output).css("visibility", "visible"); + this.output.style.visibility = "visible"; var str = "["; if (typeof a == "object" && a.length) { for (var i = 0; i < a.length; i++) @@ -39,9 +39,12 @@ export default class JSActiveCode extends ActiveCode { _this.output.innerHTML += _this.outputfun(str) + "
"; }; this.saveCode = await this.manage_scrubber(saveCode); - $(this.eContainer).remove(); - $(this.output).text(""); - $(this.outDiv).show({ duration: 700, queue: false }); + if (this.eContainer) { + this.eContainer.remove(); + } + this.output.textContent = ""; + this.outDiv.style.display = ""; + this.outDiv.style.visibility = "visible"; try { eval(prog); this.errinfo = "success"; @@ -54,7 +57,8 @@ export default class JSActiveCode extends ActiveCode { addErrorMessage(err) { // Add the error message this.errLastRun = true; - var errHead = $("

").html("Error"); + var errHead = document.createElement("h3"); + errHead.innerHTML = "Error"; this.eContainer = this.outerDiv.appendChild( document.createElement("div"), ); @@ -63,7 +67,7 @@ export default class JSActiveCode extends ActiveCode { this.eContainer.setAttribute("role", "log"); this.eContainer.className = "error alert alert-danger"; this.eContainer.id = this.divid + "_errinfo"; - this.eContainer.appendChild(errHead[0]); + this.eContainer.appendChild(errHead); var errText = this.eContainer.appendChild( document.createElement("pre"), ); diff --git a/bases/rsptx/interactives/runestone/activecode/js/activecode_sql.js b/bases/rsptx/interactives/runestone/activecode/js/activecode_sql.js index dbd289fac..5b2a55f04 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/activecode_sql.js +++ b/bases/rsptx/interactives/runestone/activecode/js/activecode_sql.js @@ -1,5 +1,6 @@ import { ActiveCode } from "./activecode.js"; import { t } from "../../common/js/rsi18n.js"; +import { getDataValue } from "../../common/js/domutil.js"; import Handsontable from "handsontable"; import "handsontable/dist/handsontable.full.css"; import initSqlJs from "sql.js/dist/sql-wasm.js"; @@ -27,7 +28,7 @@ export default class SQLActiveCode extends ActiveCode { this.config = { locateFile: (filename) => `${fnprefix}/${filename}`, }; - this.showLast = $(this.origElem).data("showlastsql"); + this.showLast = getDataValue(this.origElem, "showlastsql"); var self = this; initSqlJs(this.config).then(function (SQL) { // set up call to load database asynchronously if given @@ -48,26 +49,33 @@ export default class SQLActiveCode extends ActiveCode { self.dburl = `${self.dburl}`; } } - $(self.runButton).attr("disabled", "disabled"); - let buttonText = $(self.runButton).text(); - $(self.runButton).text(t("msg_activecode_load_db")); + self.runButton.disabled = true; + let buttonText = self.runButton.textContent; + self.runButton.textContent = t("msg_activecode_load_db"); if (!(self.dburl in allDburls)) { + // an externally-resolvable promise other instances can + // wait on while the first instance loads the database + let resolveWait; + let waitFor = new Promise((resolve) => { + resolveWait = resolve; + }); allDburls[self.dburl] = { status: "loading", - xWaitFor: jQuery.Deferred(), + xWaitFor: waitFor, + resolveWait: resolveWait, }; } else { if (allDburls[self.dburl].status == "loading") { - allDburls[self.dburl].xWaitFor.done(function () { + allDburls[self.dburl].xWaitFor.then(function () { self.db = allDburls[self.dburl].dbObject; - $(self.runButton).removeAttr("disabled"); - $(self.runButton).text(buttonText); + self.runButton.disabled = false; + self.runButton.textContent = buttonText; }); return; } self.db = allDburls[self.dburl].dbObject; - $(self.runButton).removeAttr("disabled"); - $(self.runButton).text(buttonText); + self.runButton.disabled = false; + self.runButton.textContent = buttonText; return; } var xhr = new XMLHttpRequest(); @@ -78,11 +86,11 @@ export default class SQLActiveCode extends ActiveCode { var uInt8Array = new Uint8Array(xhr.response); self.db = new SQL.Database(uInt8Array); allDburls[self.dburl].dbObject = self.db; - $(self.runButton).text(buttonText); - $(self.runButton).removeAttr("disabled"); + self.runButton.textContent = buttonText; + self.runButton.disabled = false; allDburls[self.dburl].db = uInt8Array; allDburls[self.dburl].status = "ready"; - allDburls[self.dburl].xWaitFor.resolve(); + allDburls[self.dburl].resolveWait(); // contents is now [{columns:['col1','col2',...], values:[[first row], [second row], ...]}] }; xhr.send(); @@ -107,13 +115,11 @@ export default class SQLActiveCode extends ActiveCode { if (respDiv) { respDiv.parentElement.removeChild(respDiv); } - $(this.output).text(""); + this.output.textContent = ""; // Run this query let query = await this.buildProg(false); // false --> Do not include suffix if (!this.db) { - $(this.output).text( - `Error: Database not initialized! DBURL: ${this.dburl}`, - ); + this.output.textContent = `Error: Database not initialized! DBURL: ${this.dburl}`; return; } @@ -174,13 +180,9 @@ export default class SQLActiveCode extends ActiveCode { try { this.saveCode = await this.manage_scrubber(this.saveCode); - if (this.slideit) { - $(this.historyScrubber).on( - "slidechange", - this.slideit.bind(this), - ); + if (this.historyScrubber) { + this.historyScrubber.disabled = false; } - $(this.historyScrubber).slider("enable"); } catch (e) { console.log(`Failed to update scrubber ${e}`); } @@ -252,7 +254,7 @@ export default class SQLActiveCode extends ActiveCode { this.results[this.results.length - 1], ); } else { - $(this.output).css("visibility", "hidden"); + this.output.style.visibility = "hidden"; } return Promise.resolve("done"); @@ -290,8 +292,8 @@ export default class SQLActiveCode extends ActiveCode { renderFeedback() { if (this.testResult) { - $(this.output).text(this.testResult); - $(this.output).css("visibility", "visible"); + this.output.textContent = this.testResult; + this.output.style.visibility = "visible"; } } diff --git a/bases/rsptx/interactives/runestone/activecode/js/audiotour.js b/bases/rsptx/interactives/runestone/activecode/js/audiotour.js index 7b825ad5f..24659d4b3 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/audiotour.js +++ b/bases/rsptx/interactives/runestone/activecode/js/audiotour.js @@ -105,7 +105,7 @@ export default class AudioTour extends RunestoneBase { this.audio_code = document.createElement("p"); this.windowcode = document.createElement("div"); this.windowcode.align = "left"; - $(this.windowcode).html(first); + this.windowcode.innerHTML = first; this.first_audio = document.createElement("button"); this.prev_audio = document.createElement("button"); this.pause_audio = document.createElement("button"); @@ -169,7 +169,7 @@ export default class AudioTour extends RunestoneBase { this.stop_button = document.createElement("button"); this.stop_button.className = "btn btn-default"; this.stop_button.innerHTML = "Stop tour"; - $(this.audio_tour).append( + this.audio_tour.append( this.audio_code, this.windowcode, document.createElement("br"), @@ -181,14 +181,25 @@ export default class AudioTour extends RunestoneBase { document.createElement("br"), this.status, document.createElement("br"), - this.tourButtons, + ...this.tourButtons, this.stop_button, ); - $("#" + divid + " .ac_code_div").append(this.audio_tour); - $("#" + divid + " .ac_code_div").css("width", "100%"); - $("#" + divid + " .CodeMirror.cm-s-default.ui-resizable").hide(); - $("#" + divid + " .ac_opt.btn.btn-default:last-child").hide(); - $(this.stop_button).click( + let acContainer = document.getElementById(divid); + let codeDiv = acContainer.querySelector(".ac_code_div"); + let editorEl = acContainer.querySelector(".CodeMirror.ac-resizable"); + let lastOptButton = acContainer.querySelector( + ".ac_opt.btn.btn-default:last-child", + ); + codeDiv.appendChild(this.audio_tour); + codeDiv.style.width = "100%"; + if (editorEl) { + editorEl.style.display = "none"; + } + if (lastOptButton) { + lastOptButton.style.display = "none"; + } + this.stop_button.addEventListener( + "click", function () { if (this.playing) { this.elem.pause(); @@ -199,75 +210,67 @@ export default class AudioTour extends RunestoneBase { act: "closeWindow", div_id: divid, }); - $(this.audio_tour).remove(); - $( - "#" + divid + " .CodeMirror.cm-s-default.ui-resizable", - ).show(); - $("#" + divid + " .ac_opt.btn.btn-default:last-child").show(); - $("#" + divid + " .ac_code_div").css("width", ""); - }.bind(this), - ); - $(this.tourButtons[0]).click( - function () { - this.tour(divid, audio_hash[0], bcount); - }.bind(this), - ); - $(this.tourButtons[1]).click( - function () { - this.tour(divid, audio_hash[1], bcount); - }.bind(this), - ); - $(this.tourButtons[2]).click( - function () { - this.tour(divid, audio_hash[2], bcount); - }.bind(this), - ); - $(this.tourButtons[3]).click( - function () { - this.tour(divid, audio_hash[3], bcount); - }.bind(this), - ); - $(this.tourButtons[4]).click( - function () { - this.tour(divid, audio_hash[4], bcount); + this.audio_tour.remove(); + if (editorEl) { + editorEl.style.display = ""; + } + if (lastOptButton) { + lastOptButton.style.display = ""; + } + codeDiv.style.width = ""; }.bind(this), ); - // handle the click to go to the next audio - $(this.first_audio).click( + for (let i = 0; i < 5; i++) { + if (this.tourButtons[i]) { + this.tourButtons[i].addEventListener( + "click", + function () { + this.tour(divid, audio_hash[i], bcount); + }.bind(this), + ); + } + } + // handle the click to go to the first audio + this.first_audio.addEventListener( + "click", function () { this.firstAudio(); }.bind(this), ); - // handle the click to go to the next audio - $(this.prev_audio).click( + // handle the click to go to the previous audio + this.prev_audio.addEventListener( + "click", function () { this.prevAudio(); }.bind(this), ); // handle the click to pause or play the audio - $(this.pause_audio).click( + this.pause_audio.addEventListener( + "click", function () { this.pauseAndPlayAudio(divid); }.bind(this), ); // handle the click to go to the next audio - $(this.next_audio).click( + this.next_audio.addEventListener( + "click", function () { this.nextAudio(); }.bind(this), ); - // handle the click to go to the next audio - $(this.last_audio).click( + // handle the click to go to the last audio + this.last_audio.addEventListener( + "click", function () { this.lastAudio(); }.bind(this), ); // make the image buttons look disabled - $(this.first_audio).css("opacity", 0.25); - $(this.prev_audio).css("opacity", 0.25); - $(this.pause_audio).css("opacity", 0.25); - $(this.next_audio).css("opacity", 0.25); - $(this.last_audio).css("opacity", 0.25); + this.first_audio.style.opacity = 0.25; + this.prev_audio.style.opacity = 0.25; + this.pause_audio.style.opacity = 0.25; + this.next_audio.style.opacity = 0.25; + this.last_audio.style.opacity = 0.25; } tour(divid, audio_type, bcount) { // set globals @@ -278,23 +281,22 @@ export default class AudioTour extends RunestoneBase { "display: inline-block; margin-top: 7px; margin-bottom: 3px;", ); // enable prev, pause/play and next buttons and make visible - $(this.first_audio).removeAttr("disabled"); - $(this.prev_audio).removeAttr("disabled"); - $(this.pause_audio).removeAttr("disabled"); - $(this.next_audio).removeAttr("disabled"); - $(this.last_audio).removeAttr("disabled"); - $(this.first_audio).css("opacity", 1.0); - $(this.prev_audio).css("opacity", 1.0); - $(this.pause_audio).css("opacity", 1.0); - $(this.next_audio).css("opacity", 1.0); - $(this.last_audio).css("opacity", 1.0); + this.first_audio.disabled = false; + this.prev_audio.disabled = false; + this.pause_audio.disabled = false; + this.next_audio.disabled = false; + this.last_audio.disabled = false; + this.first_audio.style.opacity = 1.0; + this.prev_audio.style.opacity = 1.0; + this.pause_audio.style.opacity = 1.0; + this.next_audio.style.opacity = 1.0; + this.last_audio.style.opacity = 1.0; // disable tour buttons - for (var i = 0; i < bcount; i++) - $(this.tourButtons[i]).attr("disabled", "disabled"); + for (var i = 0; i < bcount; i++) this.tourButtons[i].disabled = true; var atype = audio_type.split(";"); var name = atype[0].replaceAll('"', " "); this.tourName = name; - $(this.status).html(t("msg_activecode_starting", name)); + this.status.innerHTML = t("msg_activecode_starting", name); //log tour type to db this.logBookEvent({ event: "Audio", act: name, div_id: divid }); var max = atype.length; @@ -325,7 +327,7 @@ export default class AudioTour extends RunestoneBase { this.ahash[akey] = lnums; this.aname.push(akey); } - $(this.audio_code).html(str); + this.audio_code.innerHTML = str; this.len = this.aname.length; // set the number of audio file in the tour this.currIndex = 0; this.playCurrIndexAudio(); @@ -333,7 +335,7 @@ export default class AudioTour extends RunestoneBase { handlePlaying() { this.elem.pause(); // unbind current ended - $("#" + this.afile).unbind("ended"); + this.elem.onended = null; // unhighlight the prev lines this.unhighlightLines( this.theDivid, @@ -412,28 +414,30 @@ export default class AudioTour extends RunestoneBase { } // handle the end of the tour handleTourEnd() { - $(this.status).html("The " + this.tourName + " has ended."); + this.status.innerHTML = "The " + this.tourName + " has ended."; this.pause_audio.className = "btn-default glyphicon glyphicon-pause"; this.pause_audio.title = "Pause audio"; this.pause_audio.setAttribute("aria-label", "Pause audio"); - $(this.first_audio).attr("disabled", "disabled"); - $(this.prev_audio).attr("disabled", "disabled"); - $(this.pause_audio).attr("disabled", "disabled"); - $(this.next_audio).attr("disabled", "disabled"); - $(this.last_audio).attr("disabled", "disabled"); - $(this.first_audio).css("opacity", 0.25); - $(this.prev_audio).css("opacity", 0.25); - $(this.pause_audio).css("opacity", 0.25); - $(this.next_audio).css("opacity", 0.25); - $(this.last_audio).css("opacity", 0.25); + this.first_audio.disabled = true; + this.prev_audio.disabled = true; + this.pause_audio.disabled = true; + this.next_audio.disabled = true; + this.last_audio.disabled = true; + this.first_audio.style.opacity = 0.25; + this.prev_audio.style.opacity = 0.25; + this.pause_audio.style.opacity = 0.25; + this.next_audio.style.opacity = 0.25; + this.last_audio.style.opacity = 0.25; // enable the tour buttons for (var j = 0; j < this.buttonCount; j++) - $(this.tourButtons[j]).removeAttr("disabled"); + this.tourButtons[j].disabled = false; } // only call this one after the first time outerAudio() { // unbind ended - $("#" + this.afile).unbind("ended"); + if (this.elem) { + this.elem.onended = null; + } // set this.playing to false this.playing = false; // unhighlight previous lines from the last audio @@ -456,30 +460,20 @@ export default class AudioTour extends RunestoneBase { // play the audio now that it is ready playWhenReady(afile, divid, ahash) { // unbind current - $("#" + afile).unbind("canplaythrough"); + this.elem.oncanplaythrough = null; this.elem.currentTime = 0; this.playing = true; //console.log("in playWhenReady " + elem.duration); this.highlightLines(divid, ahash[afile]); + this.elem.onended = function () { + this.outerAudio(); + }.bind(this); if ( this.pause_audio.className === "btn-default glyphicon glyphicon-pause" ) { - $(this.status).html(t("msg_activecode_playing", this.tourName)); - $("#" + afile).bind( - "ended", - function () { - this.outerAudio(); - }.bind(this), - ); + this.status.innerHTML = t("msg_activecode_playing", this.tourName); this.elem.play(); - } else { - $("#" + afile).bind( - "ended", - function () { - this.outerAudio(); - }.bind(this), - ); } } // play the audio at the specified index i and set the duration and highlight the lines @@ -490,13 +484,10 @@ export default class AudioTour extends RunestoneBase { //console.log("in playaudio " + elem.duration); if (isNaN(this.elem.duration) || this.elem.duration == 0) { // set the status - $(this.status).html(t("msg_activecode_loading_audio")); - $("#" + this.afile).bind( - "canplaythrough", - function () { - this.playWhenReady(this.afile, divid, ahash); - }.bind(this), - ); + this.status.innerHTML = t("msg_activecode_loading_audio"); + this.elem.oncanplaythrough = function () { + this.playWhenReady(this.afile, divid, ahash); + }.bind(this); } // otherwise it is ready so play it else { @@ -518,7 +509,7 @@ export default class AudioTour extends RunestoneBase { "aria-label", t("msg_activecode_pause_audio"), ); - $(this.status).html(t("msg_activecode_playing", this.tourName)); + this.status.innerHTML = t("msg_activecode_playing", this.tourName); //log change to db this.logBookEvent({ event: "Audio", @@ -535,8 +526,9 @@ export default class AudioTour extends RunestoneBase { "aria-label", t("msg_activecode_play_paused_audio"), ); - $(this.status).html( - t("msg_activecode_audio_paused", this.tourName), + this.status.innerHTML = t( + "msg_activecode_audio_paused", + this.tourName, ); //log change to db this.logBookEvent({ @@ -574,18 +566,16 @@ export default class AudioTour extends RunestoneBase { var start = parseInt(hyphen[0]); var end = parseInt(hyphen[1]) + 1; for (var k = start; k < end; k++) { - //alert(k); - str = "#" + divid + "_l" + k; - if ($(str).text() != "") { - $(str).css("background-color", color); + let line = document.getElementById(divid + "_l" + k); + if (line && line.textContent != "") { + line.style.backgroundColor = color; } - //$(str).effect("highlight",{},(dur*1000)+4500); } } else { - //alert(lnum); - str = "#" + divid + "_l" + lnum; - $(str).css("background-color", color); - //$(str).effect("highlight",{},(dur*1000)+4500); + let line = document.getElementById(divid + "_l" + lnum); + if (line) { + line.style.backgroundColor = color; + } } } } diff --git a/bases/rsptx/interactives/runestone/activecode/js/livecode.js b/bases/rsptx/interactives/runestone/activecode/js/livecode.js index fbfae391e..d61fa6652 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/livecode.js +++ b/bases/rsptx/interactives/runestone/activecode/js/livecode.js @@ -1,5 +1,6 @@ import { ActiveCode } from "./activecode.js"; import { t } from "../../common/js/rsi18n.js"; +import { getDataValue } from "../../common/js/domutil.js"; import MD5 from "./md5.js"; import JUnitTestParser from "./extractUnitResults-JUnit.js"; import DoctestTestParser from "./extractUnitResults-Doctest.js"; @@ -8,18 +9,21 @@ import { base64encode } from "byte-base64"; export default class LiveCode extends ActiveCode { constructor(opts) { - var orig = $(opts.orig).find("textarea")[0]; + var orig = opts.orig.querySelector("textarea"); super(opts); - this.stdin = $(orig).data("stdin"); - this.additional_files = $(orig).data("add-files"); + this.stdin = getDataValue(orig, "stdin"); + this.additional_files = getDataValue(orig, "add-files"); // Accept older "datafile" attribute for backwards compatibility - this.datafiles = $(orig).data("datafile"); - this.sourcefile = $(orig).data("sourcefile"); - this.compileargs = unescapeHtml($(orig).data("compileargs")) || ""; - this.compileAlso = unescapeHtml($(orig).data("compile-also")); - this.linkargs = unescapeHtml($(orig).data("linkargs")); - this.runargs = unescapeHtml($(orig).data("runargs")); - this.interpreterargs = unescapeHtml($(orig).data("interpreterargs")); + this.datafiles = getDataValue(orig, "datafile"); + this.sourcefile = getDataValue(orig, "sourcefile"); + this.compileargs = + unescapeHtml(getDataValue(orig, "compileargs")) || ""; + this.compileAlso = unescapeHtml(getDataValue(orig, "compile-also")); + this.linkargs = unescapeHtml(getDataValue(orig, "linkargs")); + this.runargs = unescapeHtml(getDataValue(orig, "runargs")); + this.interpreterargs = unescapeHtml( + getDataValue(orig, "interpreterargs"), + ); this.API_KEY = "67033pV7eUUvqo07OJDIV8UZ049aLEK1"; this.USE_API_KEY = true; this.JOBE_SERVER = eBookConfig.jobehost || eBookConfig.host; @@ -40,7 +44,7 @@ export default class LiveCode extends ActiveCode { let inputContainer = document.createElement("div"); var label = document.createElement("label"); label.for = this.divid + "_stdin"; - $(label).text(t("msg_activecode_input_prg")); + label.textContent = t("msg_activecode_input_prg"); var input = document.createElement("textarea"); input.id = this.divid + "_stdin"; input.classList.add("activecode__stdin"); @@ -99,7 +103,7 @@ export default class LiveCode extends ActiveCode { this.addJobeErrorMessage( t(`Server Error: ${res.statusText}`), ); - $(this.runButton).removeAttr("disabled"); + this.runButton.disabled = false; return "fail"; } let runResults = await res.json(); @@ -120,7 +124,7 @@ export default class LiveCode extends ActiveCode { this.addJobeErrorMessage( t(`Server Error: ${iores.statusText}`), ); - $(this.runButton).removeAttr("disabled"); + this.runButton.disabled = false; return "fail"; } let result = await iores.json(); @@ -134,7 +138,7 @@ export default class LiveCode extends ActiveCode { this.addJobeErrorMessage( t("msg_activecode_server_comm_err") + e.toString(), ); - $(this.runButton).removeAttr("disabled"); + this.runButton.disabled = false; return `fail: ${e}`; } return "success"; @@ -239,13 +243,13 @@ export default class LiveCode extends ActiveCode { } if (this.stdin) { - stdin = $(this.stdin_el).val(); + stdin = this.stdin_el.value; } if (!this.sourcefile) { this.sourcefile = sfilemap[this.language]; } - $(this.output).html(t("msg_activecode_compiling_running")); + this.output.innerHTML = t("msg_activecode_compiling_running"); // Need to handle additional files // that come from additional_files (based on ids) and datafiles (based on filenames) @@ -337,14 +341,17 @@ export default class LiveCode extends ActiveCode { // do nothing, we will just use the original code } } - // Now make request for source_code - $.ajax({ - async: false, - url: url, - success: function (data) { - result = data.detail; - }, - }); + // Now make request for source_code. This lookup was + // historically synchronous ($.ajax async:false), but we are + // in an async function, so a normal fetch works here. + try { + let response = await fetch(url); + if (response.ok) { + result = (await response.json()).detail; + } + } catch (e) { + console.log(`failed to fetch ${url}: ${e}`); + } if (result) { // favor student code if it exists content = studentCode || result.file_contents; @@ -476,10 +483,13 @@ export default class LiveCode extends ActiveCode { async submitToJobe() { var data = this.json_runspec; let host = this.JOBE_SERVER + this.resource; - $(this.runButton).attr("disabled", "disabled"); - $(this.outDiv).show({ duration: 700, queue: false }); - $(this.errDiv).remove(); - $(this.output).css("visibility", "visible"); + this.runButton.disabled = true; + this.outDiv.style.display = ""; + this.outDiv.style.visibility = "visible"; + if (this.errDiv) { + this.errDiv.remove(); + } + this.output.style.visibility = "visible"; let headers = new Headers({ "Content-type": "application/json; charset=utf-8", @@ -501,7 +511,7 @@ export default class LiveCode extends ActiveCode { var logresult; var odiv = this.output; this.parsedOutput = {}; - $(this.runButton).removeAttr("disabled"); + this.runButton.disabled = false; if (result.outcome === 15) { logresult = "success"; } else { @@ -515,7 +525,7 @@ export default class LiveCode extends ActiveCode { result.stdout, this.divid, ); - $(odiv).html(this.parsedOutput.stdout); + odiv.innerHTML = this.parsedOutput.stdout; } else if ( this.language === "cpp" && result.stdout.includes("[doctest]") @@ -524,10 +534,10 @@ export default class LiveCode extends ActiveCode { result.stdout, this.divid, ); - $(odiv).html(this.parsedOutput.stdout); + odiv.innerHTML = this.parsedOutput.stdout; } else { let output = result.stdout ? result.stdout : ""; - $(odiv).html(output); + odiv.innerHTML = output; } if (this.hasUnitTests() || this.iotests) { if (this.parsedOutput.pct === undefined) { @@ -541,7 +551,7 @@ export default class LiveCode extends ActiveCode { break; } case 11: // compiler error - $(odiv).html(t("msg_activecode_were_compiling_err")); + odiv.innerHTML = t("msg_activecode_were_compiling_err"); this.addJobeErrorMessage(result.cmpinfo); this.errinfo = result.cmpinfo; break; @@ -553,22 +563,25 @@ export default class LiveCode extends ActiveCode { this.compileargs && this.compileargs.includes("-c") ) { - $(odiv).html(t("msg_activecode_compile_only")); + odiv.innerHTML = t("msg_activecode_compile_only"); } else { // any other case is real run time error - $(odiv).html(result.stdout.replace(/\n/g, "
")); + odiv.innerHTML = result.stdout.replace(/\n/g, "
"); if (result.stderr) { this.addJobeErrorMessage(result.stderr); } } break; case 13: // time limit - $(odiv).html(escapeHtml(result.stdout).replace(/\n/g, "
")); + odiv.innerHTML = escapeHtml(result.stdout).replace( + /\n/g, + "
", + ); this.addJobeErrorMessage(t("msg_activecode_time_limit_exc")); break; default: if (result.stderr) { - $(odiv).html(result.stderr.replace(/\n/g, "
")); + odiv.innerHTML = result.stderr.replace(/\n/g, "
"); } else { this.addJobeErrorMessage(t("msg_activecode_server_err")); } @@ -580,8 +593,8 @@ export default class LiveCode extends ActiveCode { processJobeIOResponses(resultList) { //process a series of IO test results into one unittest-like result - $(this.output).html(t("msg_activecode_iotest_results")); - $(this.runButton).removeAttr("disabled"); + this.output.innerHTML = t("msg_activecode_iotest_results"); + this.runButton.disabled = false; const odiv = this.output; this.parsedOutput = {}; @@ -651,20 +664,21 @@ export default class LiveCode extends ActiveCode { } break; case 11: // compiler error - $(odiv).html(result.cmpinfo.replace(/\n/g, "
")); + odiv.innerHTML = result.cmpinfo.replace(/\n/g, "
"); td4.innerHTML = t("msg_activecode_test_compile_error"); td4.classList.add("ac-feedback-fail"); this.errinfo = result.cmpinfo; break; case 12: // run time error - $(odiv).html(result.stderr.replace(/\n/g, "
")); + odiv.innerHTML = result.stderr.replace(/\n/g, "
"); td4.innerHTML = t("msg_activecode_test_run_error"); td4.classList.add("ac-feedback-fail"); this.errinfo = result.stderr; break; case 13: // time limit - $(odiv).html( - escapeHtml(result.stdout).replace(/\n/g, "
"), + odiv.innerHTML = escapeHtml(result.stdout).replace( + /\n/g, + "
", ); td4.innerHTML = t("msg_activecode_time_limit_exc"); td4.classList.add("ac-feedback-fail"); @@ -672,7 +686,7 @@ export default class LiveCode extends ActiveCode { break; default: if (result.stderr) { - $(odiv).html(result.stderr.replace(/\n/g, "
")); + odiv.innerHTML = result.stderr.replace(/\n/g, "
"); } td4.innerHTML = t("msg_activecode_server_err"); td4.classList.add("ac-feedback-fail"); @@ -714,7 +728,8 @@ export default class LiveCode extends ActiveCode { if (this.errDiv) { this.errDiv.remove(); } - var errHead = $("

").html("Error"); + var errHead = document.createElement("h3"); + errHead.innerHTML = "Error"; var eContainer = this.outerDiv.appendChild( document.createElement("div"), ); @@ -724,7 +739,7 @@ export default class LiveCode extends ActiveCode { eContainer.setAttribute("role", "log"); eContainer.className = "error alert alert-danger"; eContainer.id = this.divid + "_errinfo"; - eContainer.appendChild(errHead[0]); + eContainer.appendChild(errHead); var errText = eContainer.appendChild(document.createElement("pre")); // screenreaders seem to miss error message without the delay setTimeout(() => { @@ -851,7 +866,7 @@ export default class LiveCode extends ActiveCode { myVars.code = code; myVars.lang = this.language; if (this.stdin) { - myVars.stdin = $(this.stdin_el).val(); + myVars.stdin = this.stdin_el.value; } var targetDiv = this.codelens.id; diff --git a/bases/rsptx/interactives/runestone/activecode/js/timed_activecode.js b/bases/rsptx/interactives/runestone/activecode/js/timed_activecode.js index 56ae9e565..72d576071 100644 --- a/bases/rsptx/interactives/runestone/activecode/js/timed_activecode.js +++ b/bases/rsptx/interactives/runestone/activecode/js/timed_activecode.js @@ -30,7 +30,7 @@ var TimedActiveCodeMixin = { ]; for (var i = 0; i < buttonList.length; i++) { if (buttonList[i] !== undefined && buttonList[i] !== null) - $(buttonList[i]).hide(); + buttonList[i].style.display = "none"; } }, @@ -40,14 +40,12 @@ var TimedActiveCodeMixin = { // is the element that the icon should be appended to. var timeIconDiv = document.createElement("div"); var timeIcon = document.createElement("img"); - $(timeIcon).attr({ - src: "../_static/clock.png", - style: "width:15px;height:15px", - }); + timeIcon.setAttribute("src", "../_static/clock.png"); + timeIcon.setAttribute("style", "width:15px;height:15px"); timeIconDiv.className = "timeTip"; timeIconDiv.title = ""; timeIconDiv.appendChild(timeIcon); - $(component).prepend(timeIconDiv); + component.prepend(timeIconDiv); }, checkCorrectTimed: function () { @@ -65,7 +63,7 @@ var TimedActiveCodeMixin = { }, hideFeedback: function () { - $(this.output).css("visibility", "hidden"); + this.output.style.visibility = "hidden"; }, reinitializeListeners: function (taken) { @@ -73,20 +71,23 @@ var TimedActiveCodeMixin = { console.log("reattaching runbuttonhandler"); this.runButton.onclick = this.runButtonHander.bind(this); } - $(this.codeDiv).show(); + this.codeDiv.style.display = ""; this.runButton.disabled = false; - $(this.codeDiv).removeClass("ac-disabled"); + this.codeDiv.classList.remove("ac-disabled"); this.editor.refresh(); if (this.historyScrubber !== null) { - $(this.historyScrubber).slider({ - max: this.history.length - 1, - value: this.history.length - 1, - slide: this.slideit.bind(this), - change: this.slideit.bind(this), - }); + // the scrubber is a range input; reset its bounds and position + this.historyScrubber.max = this.history.length - 1; + this.historyScrubber.value = this.history.length - 1; + this.slideit(null); } if (taken) { - $(`#${this.divid}_unit_results`).show(); + let unitResults = document.getElementById( + `${this.divid}_unit_results`, + ); + if (unitResults) { + unitResults.style.display = ""; + } } }, }; diff --git a/bases/rsptx/interactives/runestone/activecode/test/activecode.test.js b/bases/rsptx/interactives/runestone/activecode/test/activecode.test.js new file mode 100644 index 000000000..a3d8453a2 --- /dev/null +++ b/bases/rsptx/interactives/runestone/activecode/test/activecode.test.js @@ -0,0 +1,296 @@ +// Characterization tests for the ActiveCode component. These describe the +// behavior of the component as observed on a book page. Note: deliberately +// NO jquery-globals import here -- activecode must work without jQuery. +import { describe, it, expect, beforeEach } from "vitest"; +import { ActiveCode } from "../js/activecode.js"; + +// Build the same DOM a book page provides: a div.runestone wrapper around the +// [data-component=activecode] div containing a textarea with the starter code. +function makeFixture({ + id = "test_ac_1", + code = "print('hello world')", + lang = "python", + attrs = "", + question = "", +} = {}) { + document.body.innerHTML = ` +
+
+ ${question} + +
+
`; + return document.getElementById(id); +} + +function makeActiveCode(fixtureOpts = {}, acOpts = {}) { + const orig = makeFixture(fixtureOpts); + return new ActiveCode({ + orig: orig, + useRunestoneServices: false, + python3: true, + ...acOpts, + }); +} + +beforeEach(() => { + document.body.innerHTML = ""; + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); +}); + +describe("construction", () => { + it("creates a CodeMirror editor holding the starter code", () => { + const ac = makeActiveCode({ code: "x = 40 + 2\nprint(x)" }); + expect(ac.divid).toBe("test_ac_1"); + expect(ac.language).toBe("python"); + expect(ac.editor.getValue()).toBe("x = 40 + 2\nprint(x)"); + // The editor lives inside the component's outer div. + expect(ac.outerDiv.querySelector(".CodeMirror")).toBeTruthy(); + expect(ac.codeDiv.classList.contains("ac_code_div")).toBe(true); + }); + + it("marks the component ready on the container div", () => { + const ac = makeActiveCode(); + expect( + ac.containerDiv.classList.contains("runestone-component-ready"), + ).toBe(true); + return ac.component_ready_promise; + }); + + it("adds a caption below the component", () => { + const ac = makeActiveCode(); + const cap = ac.containerDiv.querySelector("p.runestone_caption"); + expect(cap).toBeTruthy(); + expect(cap.textContent).toContain("ActiveCode"); + }); + + it("registers itself for the CSS status decoration", () => { + const ac = makeActiveCode(); + const rsDiv = ac.containerDiv.closest("div.runestone"); + expect(rsDiv.classList.contains("notAnswered")).toBe(true); + }); +}); + +describe("data attribute parsing", () => { + it("coerces numeric attributes the way jQuery .data() did", () => { + const ac = makeActiveCode({ attrs: 'data-timelimit="30000"' }); + expect(ac.timelimit).toBe(30000); + }); + + it("splits data-include on whitespace", () => { + const ac = makeActiveCode({ attrs: 'data-include="inc_1 inc_2"' }); + expect(ac.includes).toEqual(["inc_1", "inc_2"]); + }); + + it("keeps parsonsPersonalized true unless explicitly false", () => { + let ac = makeActiveCode(); + expect(ac.parsonsPersonalized).toBe(true); + ac = makeActiveCode({ + id: "test_ac_2", + attrs: 'data-parsons-personalized="false"', + }); + expect(ac.parsonsPersonalized).toBe(false); + }); +}); + +describe("prefix/suffix handling", () => { + it("strips an invisible suffix (====) from the editor and stores it", () => { + const ac = makeActiveCode({ + code: "print('visible')\n====\nprint('hidden test')\n", + }); + expect(ac.editor.getValue()).toBe("print('visible')\n"); + expect(ac.suffix).toBe("print('hidden test')\n"); + }); + + it("strips an invisible prefix (^^^^) from the editor and stores it", () => { + const ac = makeActiveCode({ + code: "print('hidden setup')\n^^^^\nprint('visible')\n", + }); + expect(ac.editor.getValue()).toBe("print('visible')\n"); + expect(ac.prefix).toBe("print('hidden setup')\n"); + }); + + it("keeps a visible suffix (===!) in the editor, marked read-only", () => { + const ac = makeActiveCode({ + code: "print('editable')\n===!\nprint('locked')\n", + }); + expect(ac.editor.getValue()).toBe( + "print('editable')\nprint('locked')\n", + ); + expect(ac.visibleSuffix).toBe("print('locked')\n"); + expect(ac.lockTextMarkers.length).toBeGreaterThan(0); + }); + + it("assembles prefix + editor + suffix in buildProg", async () => { + const ac = makeActiveCode({ + code: "setup()\n^^^^\nuser_code()\n====\ntests()\n", + }); + const prog = await ac.buildProg(true); + expect(prog).toBe("setup()\nuser_code()\n\ntests()\n"); + }); +}); + +describe("controls", () => { + it("creates a Run button wired to the runButtonHandler", () => { + const ac = makeActiveCode(); + expect(ac.runButton).toBeTruthy(); + expect(ac.runButton.textContent).toBe("Run"); + expect(ac.runButton.title).toBe("Save & Run (ctrl-s)"); + expect(ac.runButton.getAttribute("type")).toBe("button"); + expect(ac.runButton.classList.contains("run-button")).toBe(true); + expect(ac.controlDiv.classList.contains("ac_actions")).toBe(true); + }); + + it("adds a Download button when data-enabledownload is set", () => { + const ac = makeActiveCode({ attrs: "data-enabledownload" }); + expect(ac.downloadButton).toBeTruthy(); + expect(ac.downloadButton.textContent).toBe("Download"); + }); + + it("adds a Reformat button only for curly-brace languages", () => { + let ac = makeActiveCode(); + expect(ac.reformatButton).toBeUndefined(); + // livecode langs construct LiveCode normally; ActiveCode still honors + // the reformatable set for e.g. javascript + ac = makeActiveCode({ id: "test_ac_2", lang: "javascript" }); + expect(ac.reformatButton).toBeTruthy(); + }); + + it("relabels the Run button when save/load is enabled", () => { + const ac = makeActiveCode(); + ac.enableSaveLoad(); + expect(ac.runButton.textContent).toBe("Save & Run"); + }); +}); + +describe("hidecode", () => { + it("hides the editor and shows a Show Code button", () => { + const ac = makeActiveCode({ attrs: 'data-hidecode="true"' }); + expect(ac.codeDiv.style.display).toBe("none"); + expect(ac.showHideButt).toBeTruthy(); + expect(ac.showHideButt.textContent).toBe("Show Code"); + expect(ac.runButton.disabled).toBe(true); + }); + + it("toggles editor visibility and run button on click", () => { + const ac = makeActiveCode({ attrs: 'data-hidecode="true"' }); + ac.showHideButt.click(); + expect(ac.codeDiv.style.display).not.toBe("none"); + expect(ac.showHideButt.textContent).toBe("Hide Code"); + expect(ac.runButton.disabled).toBe(false); + ac.showHideButt.click(); + expect(ac.codeDiv.style.display).toBe("none"); + expect(ac.showHideButt.textContent).toBe("Show Code"); + expect(ac.runButton.disabled).toBe(true); + }); +}); + +describe("output area", () => { + it("creates stdout, graphics, coach, codelens and error containers", () => { + const ac = makeActiveCode(); + expect(ac.output.id).toBe("test_ac_1_stdout"); + expect(ac.output.getAttribute("role")).toBe("log"); + expect(ac.graphics.id).toBe("test_ac_1_graphics"); + expect(ac.codecoach.style.display).toBe("none"); + expect(ac.codelens.style.display).toBe("none"); + expect(ac.eContainer.id).toBe("test_ac_1_errinfo"); + expect(ac.eContainer.style.visibility).toBe("hidden"); + }); +}); + +describe("running python with skulpt", () => { + it("runs the program and writes escaped output to the stdout pre", async () => { + const ac = makeActiveCode({ + code: "print('2 < 3')\nprint('done')", + }); + await ac.runProg(); + expect(ac.errinfo).toBe("success"); + // stdout writes are appended from 1ms timeouts; let them flush + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(ac.output.innerHTML).toContain("2 < 3"); + expect(ac.output.textContent).toContain("done"); + }); + + it("reports errors in the error container", async () => { + const ac = makeActiveCode({ + id: "test_ac_err", + code: "print(undefined_name)", + }); + await ac.runProg(); + expect(ac.errinfo).toContain("NameError"); + expect(ac.eContainer.style.visibility).toBe("visible"); + // the detailed message is added on a timeout + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(ac.eContainer.textContent).toContain("Error"); + }); + + it("records history when the code changed", async () => { + const ac = makeActiveCode({ code: "print('one')" }); + ac.editor.setValue("print('two')"); + const before = ac.history.length; + await ac.runProg(); + expect(ac.history.length).toBe(before + 1); + expect(ac.history[ac.history.length - 1]).toBe("print('two')"); + }); +}); + +describe("error message formatting", () => { + it("names the error and offers a description and fix", () => { + const ac = makeActiveCode(); + ac.pretextLines = 0; + ac.progLines = 100; + ac.addErrorMessage(new Error("NameError: name 'x' is not defined")); + const text = ac.eContainer.textContent; + expect(text).toContain("Error"); + expect(text).toContain("Description"); + expect(text).toContain("To Fix"); + expect(text).toContain("NameError: name 'x' is not defined"); + }); +}); + +describe("history scrubber", () => { + it("builds a range-input scrubber after the run button", async () => { + const ac = makeActiveCode(); + await ac.addHistoryScrubber(true); + expect(ac.historyScrubber).toBeTruthy(); + expect(ac.historyScrubber.tagName).toBe("INPUT"); + expect(ac.historyScrubber.type).toBe("range"); + expect(ac.historyScrubber.getAttribute("aria-label")).toBe( + "History slider", + ); + expect(ac.timestampP.textContent).toContain("1 of 1"); + }); + + it("restores older code when the scrubber moves", async () => { + const ac = makeActiveCode({ code: "print('v1')" }); + ac.editor.setValue("print('v2')"); + await ac.manage_scrubber("False"); + expect(ac.history).toEqual(["print('v1')", "print('v2')"]); + expect(ac.timestampP.textContent).toContain("2 of 2"); + // drag back to the first revision + ac.historyScrubber.value = 0; + ac.historyScrubber.dispatchEvent(new Event("input")); + expect(ac.editor.getValue()).toBe("print('v1')"); + expect(ac.timestampP.textContent).toContain("1 of 2"); + }); +}); + +describe("resizable editor", () => { + it("tags the CodeMirror wrapper with the ac-resizable class", () => { + const ac = makeActiveCode(); + expect( + ac.editor.getWrapperElement().classList.contains("ac-resizable"), + ).toBe(true); + }); +}); + +describe("disableInteraction", () => { + it("hides the run button and disables the editor area", () => { + const ac = makeActiveCode(); + ac.disableInteraction(); + expect(ac.runButton.style.display).toBe("none"); + expect(ac.codeDiv.classList.contains("ac-disabled")).toBe(true); + }); +}); diff --git a/bases/rsptx/interactives/runestone/activecode/test/activecode_subclasses.test.js b/bases/rsptx/interactives/runestone/activecode/test/activecode_subclasses.test.js new file mode 100644 index 000000000..375ea7edf --- /dev/null +++ b/bases/rsptx/interactives/runestone/activecode/test/activecode_subclasses.test.js @@ -0,0 +1,78 @@ +// Tests for the JavaScript and HTML flavors of ActiveCode. +// No jquery-globals import -- these components must work without jQuery. +import { describe, it, expect, beforeEach } from "vitest"; +import JSActiveCode from "../js/activecode_js.js"; +import HTMLActiveCode from "../js/activecode_html.js"; + +function makeFixture({ id, code, lang }) { + document.body.innerHTML = ` +
+
+ +
+
`; + return document.getElementById(id); +} + +beforeEach(() => { + document.body.innerHTML = ""; + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); +}); + +describe("JSActiveCode", () => { + it("runs a JavaScript program and writes to the output", async () => { + const orig = makeFixture({ + id: "test_js_1", + code: "writeln('hello from js');\nwriteln(6 * 7);", + lang: "javascript", + }); + const ac = new JSActiveCode({ orig, useRunestoneServices: false }); + await ac.runProg(); + expect(ac.errinfo).toBe("success"); + expect(ac.output.textContent).toContain("hello from js"); + expect(ac.output.textContent).toContain("42"); + }); + + it("shows an error container for a broken program", async () => { + const orig = makeFixture({ + id: "test_js_2", + code: "no_such_function();", + lang: "javascript", + }); + const ac = new JSActiveCode({ orig, useRunestoneServices: false }); + await ac.runProg(); + expect(ac.errinfo).not.toBe("success"); + expect(ac.eContainer.className).toContain("error"); + // the message is added after a short screenreader-friendly delay + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(ac.eContainer.textContent).toContain("no_such_function"); + }); +}); + +describe("HTMLActiveCode", () => { + it("labels the run button Render and targets an iframe", () => { + const orig = makeFixture({ + id: "test_html_1", + code: "<h1>Hello</h1>", + lang: "html", + }); + const ac = new HTMLActiveCode({ orig, useRunestoneServices: false }); + expect(ac.runButton.textContent).toBe("Render"); + expect(ac.output.tagName).toBe("IFRAME"); + // entities were decoded by the textarea parser + expect(ac.editor.getValue()).toContain("

Hello

"); + }); + + it("renders the editor contents into the iframe srcdoc", async () => { + const orig = makeFixture({ + id: "test_html_2", + code: "<p>content</p>", + lang: "html", + }); + const ac = new HTMLActiveCode({ orig, useRunestoneServices: false }); + await ac.runProg(); + expect(ac.output.srcdoc).toContain("

content

"); + }); +}); diff --git a/bases/rsptx/interactives/runestone/common/js/domutil.js b/bases/rsptx/interactives/runestone/common/js/domutil.js new file mode 100644 index 000000000..0d4a4ef7c --- /dev/null +++ b/bases/rsptx/interactives/runestone/common/js/domutil.js @@ -0,0 +1,157 @@ +/* *********************************** + * |docname| - small DOM helpers + * *********************************** + * Helpers that replace common jQuery idioms as components are migrated to + * plain DOM APIs. + */ + +const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; + +/** + * Read a data-* attribute with the same type coercion jQuery's .data() + * performed: "true"/"false"/"null" become their JS values, numeric strings + * become numbers, JSON-looking strings are parsed, everything else is + * returned as a string. Returns undefined when the attribute is absent. + * + * @param {Element} element + * @param {string} name - the part of the attribute name after "data-" + */ +export function getDataValue(element, name) { + const data = element.getAttribute(`data-${name}`); + if (data === null) { + return undefined; + } + if (data === "true") return true; + if (data === "false") return false; + if (data === "null") return null; + // Only convert to a number if it doesn't change the string. + if (data === +data + "") return +data; + if (rbrace.test(data)) { + try { + return JSON.parse(data); + } catch (e) { + return data; + } + } + return data; +} + +/** + * Show or hide an element by toggling inline display, like jQuery's .toggle(). + */ +export function toggleDisplay(element) { + if (!element) return; + if (isHidden(element)) { + element.style.display = ""; + if (isHidden(element)) { + element.style.display = "block"; + } + } else { + element.style.display = "none"; + } +} + +function isHidden(element) { + if (element.style.display === "none") return true; + const win = element.ownerDocument.defaultView; + return win.getComputedStyle(element).display === "none"; +} + +/** + * Width of an element including margins, like jQuery's .outerWidth(true). + */ +export function outerWidth(element) { + const style = getComputedStyle(element); + return ( + element.getBoundingClientRect().width + + (parseFloat(style.marginLeft) || 0) + + (parseFloat(style.marginRight) || 0) + ); +} + +/** + * Height of an element including margins, like jQuery's .outerHeight(true). + */ +export function outerHeight(element) { + const style = getComputedStyle(element); + return ( + element.getBoundingClientRect().height + + (parseFloat(style.marginTop) || 0) + + (parseFloat(style.marginBottom) || 0) + ); +} + +// Properties that take a bare number rather than a px length. +const unitless = new Set(["opacity", "zIndex", "fontWeight"]); + +const kebab = /-([a-z])/g; +const camelCase = (name) => name.replace(kebab, (m, c) => c.toUpperCase()); + +function parseColor(value) { + if (!value) return null; + let m = value.match(/^#([0-9a-f]{3})$/i); + if (m) { + return [...m[1]].map((c) => parseInt(c + c, 16)); + } + m = value.match(/^#([0-9a-f]{6})$/i); + if (m) { + return [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16)); + } + m = value.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (m) { + return [+m[1], +m[2], +m[3]]; + } + return null; +} + +/** + * A requestAnimationFrame tween covering the subset of jQuery's .animate() + * the components use: numeric CSS properties (px, or bare for opacity-like + * properties) and colors, plus start/progress/complete callbacks. progress + * receives the 0..1 fraction as its second argument, matching the jQuery + * callback shape used by existing callers. + */ +export function animate(element, properties, options = {}) { + const duration = options.duration ?? 400; + const computed = getComputedStyle(element); + const tweens = []; + for (const [name, target] of Object.entries(properties)) { + const prop = camelCase(name); + if (typeof target === "number") { + const from = parseFloat(computed[prop]) || 0; + const unit = unitless.has(prop) ? "" : "px"; + tweens.push((p) => { + element.style[prop] = from + (target - from) * p + unit; + }); + } else { + const fromColor = parseColor(computed[prop]); + const toColor = parseColor(target); + if (fromColor && toColor) { + tweens.push((p) => { + const mixed = fromColor.map((f, i) => + Math.round(f + (toColor[i] - f) * p), + ); + element.style[prop] = `rgb(${mixed.join(", ")})`; + }); + } else { + // Not interpolatable; just apply the final value at the end. + tweens.push((p) => { + if (p === 1) element.style[prop] = target; + }); + } + } + } + if (options.start) options.start(); + const t0 = performance.now(); + function step(now) { + const p = Math.min(1, (now - t0) / duration); + for (const tween of tweens) tween(p); + if (options.progress) options.progress(null, p, null); + if (p < 1) { + requestAnimationFrame(step); + } else if (options.complete) { + options.complete(); + } + } + requestAnimationFrame(step); +} diff --git a/bases/rsptx/interactives/runestone/common/js/jquery.highlight.js b/bases/rsptx/interactives/runestone/common/js/jquery.highlight.js deleted file mode 100644 index 0d97ad9d7..000000000 --- a/bases/rsptx/interactives/runestone/common/js/jquery.highlight.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - -highlight v4 - -Highlights arbitrary terms. - - - -MIT license. - -Johann Burkard - - - -*/ - -jQuery.fn.highlight = function(pat) { - function innerHighlight(node, pat) { - var skip = 0; - if (node.nodeType == 3) { - var pos = node.data.toUpperCase().indexOf(pat); - if (pos >= 0) { - var spannode = document.createElement('span'); - spannode.className = 'highlight'; - var middlebit = node.splitText(pos); - var endbit = middlebit.splitText(pat.length); - var middleclone = middlebit.cloneNode(true); - spannode.appendChild(middleclone); - middlebit.parentNode.replaceChild(spannode, middlebit); - skip = 1; - } - } - else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { - for (var i = 0; i < node.childNodes.length; ++i) { - i += innerHighlight(node.childNodes[i], pat); - } - } - return skip; - } - return this.length && pat && pat.length ? this.each(function() { - innerHighlight(this, pat.toUpperCase()); - }) : this; -}; - -jQuery.fn.removeHighlight = function() { - return this.find("span.highlight").each(function() { - this.parentNode.firstChild.nodeName; - with (this.parentNode) { - replaceChild(this.firstChild, this); - normalize(); - } - }).end(); -}; diff --git a/bases/rsptx/interactives/runestone/common/js/jquery.idle-timer.js b/bases/rsptx/interactives/runestone/common/js/jquery.idle-timer.js deleted file mode 100644 index dff582b11..000000000 --- a/bases/rsptx/interactives/runestone/common/js/jquery.idle-timer.js +++ /dev/null @@ -1,262 +0,0 @@ -/*! - * jQuery idleTimer plugin - * version 0.9.100511 - * by Paul Irish. - * http://github.com/paulirish/yui-misc/tree/ - * MIT license - - * adapted from YUI idle timer by nzakas: - * http://github.com/nzakas/yui-misc/ -*/ -/* - * Copyright (c) 2009 Nicholas C. Zakas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* updated to fix Chrome setTimeout issue by Zaid Zawaideh */ - - // API available in <= v0.8 - /******************************* - - // idleTimer() takes an optional argument that defines the idle timeout - // timeout is in milliseconds; defaults to 30000 - $.idleTimer(10000); - - - $(document).bind("idle.idleTimer", function(){ - // function you want to fire when the user goes idle - }); - - - $(document).bind("active.idleTimer", function(){ - // function you want to fire when the user becomes active again - }); - - // pass the string 'destroy' to stop the timer - $.idleTimer('destroy'); - - // you can query if the user is idle or not with data() - $.data(document,'idleTimer'); // 'idle' or 'active' - - // you can get time elapsed since user when idle/active - $.idleTimer('getElapsedTime'); // time since state change in ms - - ********/ - - - - // API available in >= v0.9 - /************************* - - // bind to specific elements, allows for multiple timer instances - $(elem).idleTimer(timeout|'destroy'|'getElapsedTime'); - $.data(elem,'idleTimer'); // 'idle' or 'active' - - // if you're using the old $.idleTimer api, you should not do $(document).idleTimer(...) - - // element bound timers will only watch for events inside of them. - // you may just want page-level activity, in which case you may set up - // your timers on document, document.documentElement, and document.body - - // You can optionally provide a second argument to override certain options. - // Here are the defaults, so you can omit any or all of them. - $(elem).idleTimer(timeout, { - startImmediately: true, //starts a timeout as soon as the timer is set up; otherwise it waits for the first event. - idle: false, //indicates if the user is idle - enabled: true, //indicates if the idle timer is enabled - events: 'mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove' // activity is one of these events - }); - - ********/ - -(function($){ - -$.idleTimer = function(newTimeout, elem, opts){ - - // defaults that are to be stored as instance props on the elem - - opts = $.extend({ - startImmediately: true, //starts a timeout as soon as the timer is set up - idle: false, //indicates if the user is idle - enabled: true, //indicates if the idle timer is enabled - timeout: 30000, //the amount of time (ms) before the user is considered idle - events: 'mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove' // activity is one of these events - }, opts); - - - elem = elem || document; - - /* (intentionally not documented) - * Toggles the idle state and fires an appropriate event. - * @return {void} - */ - var toggleIdleState = function(myelem){ - - // curse you, mozilla setTimeout lateness bug! - if (typeof myelem === 'number'){ - myelem = undefined; - } - - var obj = $.data(myelem || elem,'idleTimerObj'); - - //toggle the state - obj.idle = !obj.idle; - - // reset timeout - var elapsed = (+new Date()) - obj.olddate; - obj.olddate = +new Date(); - - // handle Chrome always triggering idle after js alert or comfirm popup - if (obj.idle && (elapsed < opts.timeout)) { - obj.idle = false; - clearTimeout($.idleTimer.tId); - if (opts.enabled) - $.idleTimer.tId = setTimeout(toggleIdleState, opts.timeout); - return; - } - - //fire appropriate event - - // create a custom event, but first, store the new state on the element - // and then append that string to a namespace - var event = jQuery.Event( $.data(elem,'idleTimer', obj.idle ? "idle" : "active" ) + '.idleTimer' ); - - // we do want this to bubble, at least as a temporary fix for jQuery 1.7 - // event.stopPropagation(); - $(elem).trigger(event); - }, - - /** - * Stops the idle timer. This removes appropriate event handlers - * and cancels any pending timeouts. - * @return {void} - * @method stop - * @static - */ - stop = function(elem){ - - var obj = $.data(elem,'idleTimerObj') || {}; - - //set to disabled - obj.enabled = false; - - //clear any pending timeouts - clearTimeout(obj.tId); - - //detach the event handlers - $(elem).off('.idleTimer'); - }, - - - /* (intentionally not documented) - * Handles a user event indicating that the user isn't idle. - * @param {Event} event A DOM2-normalized event object. - * @return {void} - */ - handleUserEvent = function(){ - - var obj = $.data(this,'idleTimerObj'); - - //clear any existing timeout - clearTimeout(obj.tId); - - - - //if the idle timer is enabled - if (obj.enabled){ - - - //if it's idle, that means the user is no longer idle - if (obj.idle){ - toggleIdleState(this); - } - - //set a new timeout - obj.tId = setTimeout(toggleIdleState, obj.timeout); - - } - }; - - - /** - * Starts the idle timer. This adds appropriate event handlers - * and starts the first timeout. - * @param {int} newTimeout (Optional) A new value for the timeout period in ms. - * @return {void} - * @method $.idleTimer - * @static - */ - - - var obj = $.data(elem,'idleTimerObj') || {}; - - obj.olddate = obj.olddate || +new Date(); - - //assign a new timeout if necessary - if (typeof newTimeout === "number"){ - opts.timeout = newTimeout; - } else if (newTimeout === 'destroy') { - stop(elem); - return this; - } else if (newTimeout === 'getElapsedTime'){ - return (+new Date()) - obj.olddate; - } - - //assign appropriate event handlers - $(elem).on($.trim((opts.events+' ').split(' ').join('.idleTimer ')),handleUserEvent); - - - obj.idle = opts.idle; - obj.enabled = opts.enabled; - obj.timeout = opts.timeout; - - - //set a timeout to toggle state. May wish to omit this in some situations - if (opts.startImmediately) { - obj.tId = setTimeout(toggleIdleState, obj.timeout); - } - - // assume the user is active for the first x seconds. - $.data(elem,'idleTimer',"active"); - - // store our instance on the object - $.data(elem,'idleTimerObj',obj); - - - -}; // end of $.idleTimer() - - -// v0.9 API for defining multiple timers. -$.fn.idleTimer = function(newTimeout,opts){ - // Allow omission of opts for backward compatibility - if (!opts) { - opts = {}; - } - - if(this[0]){ - $.idleTimer(newTimeout,this[0],opts); - } - - return this; -}; - - -})(jQuery); diff --git a/bases/rsptx/interactives/runestone/common/js/presenter_mode.js b/bases/rsptx/interactives/runestone/common/js/presenter_mode.js index c3cd01247..3baf24bc1 100644 --- a/bases/rsptx/interactives/runestone/common/js/presenter_mode.js +++ b/bases/rsptx/interactives/runestone/common/js/presenter_mode.js @@ -1,86 +1,114 @@ -var codeExercises; -var presenterCssLink; +// Instructor-only "Code Presenter" view for Sphinx book pages: hides the +// prose and steps through the interactive exercises one at a time. + +var codeExercises = []; // Array of top-level .runestone exercise divs var presentModeInitialized = false; +// Elements a presenter wants to keep on screen. +const KEEP_VISIBLE = + "h1, .presentation-title, .btn-presenter, .runestone, .runestone *, section, .pre, code"; + +function sectionContent() { + return [...document.querySelectorAll("section *")].filter( + (el) => !el.matches(KEEP_VISIBLE), + ); +} + function presentToggle() { if (!presentModeInitialized) { presentModeSetup(); presentModeInitialized = true; } - let bod = $("body"); + let bod = document.body; let presentClass = "present"; let fullHeightClass = "full-height"; let bottomClass = "bottom"; - if (bod.hasClass(presentClass)) { - $("section *") - .not( - "h1, .presentation-title, .btn-presenter, .runestone, .runestone *, section, .pre, code", - ) - .removeClass("hidden"); //show everything - $("#completionButton").removeClass("hidden"); - bod.removeClass(presentClass); - $("." + fullHeightClass).removeClass(fullHeightClass); - $("." + bottomClass).removeClass(bottomClass); + if (bod.classList.contains(presentClass)) { + for (const el of sectionContent()) { + el.classList.remove("hidden"); //show everything + } + document + .getElementById("completionButton") + ?.classList.remove("hidden"); + bod.classList.remove(presentClass); + for (const el of document.querySelectorAll("." + fullHeightClass)) { + el.classList.remove(fullHeightClass); + } + for (const el of document.querySelectorAll("." + bottomClass)) { + el.classList.remove(bottomClass); + } localStorage.setItem("presentMode", "text"); - codeExercises.removeClass("hidden"); - presenterCssLink.disabled = true; // disable present_mode.css + for (const exercise of codeExercises) { + exercise.classList.remove("hidden"); + } } else { - $("section *") - .not( - "h1, .presentation-title, .btn-presenter, .runestone, .runestone *, section, .pre, code", - ) - .addClass("hidden"); // hide extraneous stuff - $("#completionButton").addClass("hidden"); - bod.addClass(presentClass); - bod.addClass(fullHeightClass); - $("html").addClass(fullHeightClass); - $("section .runestone").addClass(fullHeightClass); - $(".ac-caption").addClass(bottomClass); + for (const el of sectionContent()) { + el.classList.add("hidden"); // hide extraneous stuff + } + document.getElementById("completionButton")?.classList.add("hidden"); + bod.classList.add(presentClass); + bod.classList.add(fullHeightClass); + document.documentElement.classList.add(fullHeightClass); + for (const el of document.querySelectorAll("section .runestone")) { + el.classList.add(fullHeightClass); + } + for (const el of document.querySelectorAll(".ac-caption")) { + el.classList.add(bottomClass); + } localStorage.setItem("presentMode", presentClass); - // presenter_mode.css is loaded by webpack - //loadPresenterCss(); // present_mode.css should only apply when in presenter mode. activateExercise(); } } -function loadPresenterCss() { - presenterCssLink = document.createElement("link"); - presenterCssLink.type = "text/css"; - presenterCssLink.href = "../_static/presenter_mode.less"; - presenterCssLink.rel = "stylesheet"; - document.getElementsByTagName("head")[0].appendChild(presenterCssLink); -} - function presentModeSetup() { // moved this out of configure - let dataComponent = $("[data-childcomponent]"); // this still leaves some things semi-messed up when you exit presenter mode. // but instructors will probably just learn to refresh the page. - dataComponent.addClass("runestone"); - dataComponent.parent().closest("div").not("section").addClass("runestone"); - dataComponent.parent().closest("div").css("max-width", "none"); - - dataComponent.each(function (index) { - let me = $(this); - $(this) - .find(".ac_code_div, .ac_output") - .wrapAll("
"); - }); + for (const component of document.querySelectorAll( + "[data-childcomponent]", + )) { + component.classList.add("runestone"); + const parentDiv = component.parentElement?.closest("div"); + if (parentDiv && !parentDiv.matches("section")) { + parentDiv.classList.add("runestone"); + parentDiv.style.maxWidth = "none"; + } + const parts = component.querySelectorAll(".ac_code_div, .ac_output"); + if (parts.length) { + const block = document.createElement("div"); + block.className = "ac-block"; + block.style.width = "100%"; + parts[0].before(block); + for (const part of parts) { + block.appendChild(part); + } + } + } - codelensListener(500); - $("section img").wrap('
'); - codeExercises = $(".runestone").not(".runestone .runestone"); - // codeExercises.each(function(){ - $("h1").before( - "
\ + for (const img of document.querySelectorAll("section img")) { + const wrapper = document.createElement("div"); + wrapper.className = "runestone"; + img.replaceWith(wrapper); + wrapper.appendChild(img); + } + // top-level exercises only: skip .runestone nested inside another + codeExercises = [...document.querySelectorAll(".runestone")].filter( + (el) => !el.parentElement.closest(".runestone"), + ); + for (const heading of document.querySelectorAll("h1")) { + heading.insertAdjacentHTML( + "beforebegin", + "
\ \ \
", - ); + ); + } } + function getActiveExercise() { - return (active = codeExercises.filter(".active")); + return codeExercises.filter((el) => el.classList.contains("active")); } function activateExercise(index) { @@ -88,35 +116,44 @@ function activateExercise(index) { index = 0; } - let active = getActiveExercise(); - if (codeExercises.length) { - active.removeClass("active"); - active = $(codeExercises[index]).addClass("active"); - active.removeClass("hidden"); - codeExercises.not(codeExercises.filter(".active")).addClass("hidden"); + for (const el of getActiveExercise()) { + el.classList.remove("active"); + } + const active = codeExercises[index]; + active.classList.add("active"); + active.classList.remove("hidden"); + for (const el of codeExercises) { + if (el !== active) { + el.classList.add("hidden"); + } + } } } window.nextExercise = function () { - let active = getActiveExercise(); - let nextIndex = codeExercises.index(active) + 1; + let active = getActiveExercise()[0]; + let nextIndex = codeExercises.indexOf(active) + 1; if (nextIndex < codeExercises.length) { activateExercise(nextIndex); } }; window.prevExercise = function () { - let active = getActiveExercise(); - let prevIndex = codeExercises.index(active) - 1; + let active = getActiveExercise()[0]; + let prevIndex = codeExercises.indexOf(active) - 1; if (prevIndex >= 0) { activateExercise(prevIndex); } }; function configure() { - let rightNav = $(".navbar-right"); - rightNav.prepend( + let rightNav = document.querySelector(".navbar-right"); + if (!rightNav) { + return; + } + rightNav.insertAdjacentHTML( + "afterbegin", "
`; + return document.getElementById(id); +} + +const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function makeGroupSub(fixtureOpts = {}) { + const orig = makeFixture(fixtureOpts); + const gs = new GroupSub({ orig }); + await gs.initialize(); + return gs; +} + +function select(gs, ...sids) { + for (const option of gs.picker.options) { + if (sids.includes(option.value)) { + option.selected = true; + } + } +} + +beforeEach(() => { + document.body.innerHTML = ""; + document.body.classList.remove("pretext"); + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); + vi.restoreAllMocks(); + vi.stubGlobal("alert", vi.fn()); + eBookConfig.username = "leader"; +}); + +afterEach(() => { + delete eBookConfig.username; +}); + +describe("construction and initialize", () => { + it("adds a Submit Group button to the page's button container", async () => { + await makeGroupSub(); + const button = document.querySelector("#groupsub_button button"); + expect(button.textContent).toBe("Submit Group"); + expect(button.classList.contains("btn-success")).toBe(true); + }); + + it("fills the picker with the demo class list when services are off", async () => { + const gs = await makeGroupSub(); + expect(gs.picker).toBe(document.getElementById("assignment_group")); + const values = [...gs.picker.options].map((o) => o.value); + expect(values).toEqual(["s1", "s2", "s3", "s4", "s5"]); + expect(gs.picker.options[0].textContent).toBe("User 1"); + }); + + // Node's Request rejects the relative URLs the component uses; fetch is + // mocked in these tests, so a capturing stand-in is enough. + class FakeRequest { + constructor(url, opts) { + this.url = url; + Object.assign(this, opts); + } + } + + it("fills the picker from the course roster when services are on", async () => { + eBookConfig.useRunestoneServices = true; + vi.stubGlobal("Request", FakeRequest); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + redirected: false, + json: async () => ({ + detail: { + students: { alice: "Alice A", bob: "Bob B" }, + }, + }), + }), + ); + try { + const gs = await makeGroupSub(); + const values = [...gs.picker.options].map((o) => o.value); + expect(values).toEqual(["alice", "bob"]); + } finally { + eBookConfig.useRunestoneServices = false; + } + }); + + it("degrades to an explanatory entry when the roster fetch fails", async () => { + eBookConfig.useRunestoneServices = true; + vi.stubGlobal("Request", FakeRequest); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ ok: false, redirected: false }), + ); + try { + const gs = await makeGroupSub(); + expect([...gs.picker.options][0].textContent).toContain( + "Failed to load students", + ); + } finally { + eBookConfig.useRunestoneServices = false; + } + }); +}); + +describe("submitAll", () => { + it("submits every component on the page for every group member", async () => { + const gs = await makeGroupSub(); + const logSpy = vi + .spyOn(RunestoneBase.prototype, "logBookEvent") + .mockResolvedValue(undefined); + const question = { + logCurrentAnswer: vi.fn().mockResolvedValue(undefined), + }; + window.allComponents = [question]; + select(gs, "s1", "s2"); + await gs.submitAll(); + // once per (member, question); the leader is added automatically + const students = question.logCurrentAnswer.mock.calls.map( + (c) => c[0], + ); + expect(students).toEqual(["s1", "s2", "leader"]); + expect(logSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event: "group_start", + act: "s1,s2,leader", + }), + ); + expect(logSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event: "group_end", + act: "s1,s2,leader", + }), + ); + }); + + it("keeps submitting other members when one submission fails", async () => { + const gs = await makeGroupSub(); + vi.spyOn(RunestoneBase.prototype, "logBookEvent").mockResolvedValue( + undefined, + ); + const bad = { + logCurrentAnswer: vi.fn().mockRejectedValue(new Error("boom")), + }; + const good = { + logCurrentAnswer: vi.fn().mockResolvedValue(undefined), + }; + window.allComponents = [bad, good]; + select(gs, "s1"); + await gs.submitAll(); + expect(good.logCurrentAnswer).toHaveBeenCalledTimes(2); // s1 + leader + }); + + it("refuses a group larger than the size limit", async () => { + const gs = await makeGroupSub({ limit: 2 }); + const logSpy = vi + .spyOn(RunestoneBase.prototype, "logBookEvent") + .mockResolvedValue(undefined); + select(gs, "s1", "s2", "s3"); + await gs.submitAll(); + expect(alert).toHaveBeenCalledWith( + expect.stringContaining("You may not have more than 2"), + ); + expect(logSpy).not.toHaveBeenCalled(); + }); +}); + +describe("pretext pages", () => { + it("scopes the button and picker to the component element", async () => { + document.body.classList.add("pretext"); + document.body.innerHTML = ` +
+
+
+ +
+
`; + const gs = new GroupSub({ + orig: document.getElementById("gs_ptx"), + }); + await gs.initialize(); + expect( + document.querySelector("#gs_ptx .groupsub_button button") + .textContent, + ).toBe("Submit Group"); + expect(gs.picker).toBe( + document.querySelector("#gs_ptx .assignment_partner_select"), + ); + }); +}); diff --git a/bases/rsptx/interactives/runestone/parsons/js/parsons.js b/bases/rsptx/interactives/runestone/parsons/js/parsons.js index 861052ce4..b4da25f32 100644 --- a/bases/rsptx/interactives/runestone/parsons/js/parsons.js +++ b/bases/rsptx/interactives/runestone/parsons/js/parsons.js @@ -23,6 +23,12 @@ import RunestoneBase from "../../common/js/runestonebase.js"; import { t } from "../../common/js/rsi18n.js"; +import { + getDataValue, + animate, + outerHeight, + outerWidth, +} from "../../common/js/domutil.js"; import "./parsons-i18n.en.js"; import "./parsons-i18n.pt-br.js"; import "./parsons-i18n.sr-Cyrl.js"; @@ -62,9 +68,9 @@ export default class Parsons extends RunestoneBase { super(opts); var orig = opts.orig; // entire
 element that will be replaced by new HTML
         this.containerDiv = orig;
-        this.origElem = $(orig).find("pre.parsonsblocks")[0];
+        this.origElem = orig.querySelector("pre.parsonsblocks");
         // Find the question text and store it in .question
-        this.question = $(orig).find(`.parsons_question`)[0];
+        this.question = orig.querySelector(".parsons_question");
         this.useRunestoneServices = opts.useRunestoneServices;
         this.divid = opts.orig.id;
         // Set the storageId (key for storing data)
@@ -87,7 +93,7 @@ export default class Parsons extends RunestoneBase {
                 ? new DAGGrader(this)
                 : new LineBasedGrader(this);
         this.grader.showfeedback = this.showfeedback;
-        var fulltext = $(this.origElem).html();
+        var fulltext = this.origElem.innerHTML;
         this.blockIndex = 0;
         this.checkCount = 0;
         this.numDistinct = 0;
@@ -106,14 +112,14 @@ export default class Parsons extends RunestoneBase {
             pixelsPerIndent: 30,
         };
         // add maxdist and order if present
-        var maxdist = $(this.origElem).data("maxdist");
-        var order = $(this.origElem).data("order");
-        var noindent = $(this.origElem).data("noindent");
-        var adaptive = $(this.origElem).data("adaptive");
-        var numbered = $(this.origElem).data("numbered");
-        var grader = $(this.origElem).data("grader");
+        var maxdist = getDataValue(this.origElem, "maxdist");
+        var order = getDataValue(this.origElem, "order");
+        var noindent = getDataValue(this.origElem, "noindent");
+        var adaptive = getDataValue(this.origElem, "adaptive");
+        var numbered = getDataValue(this.origElem, "numbered");
+        var grader = getDataValue(this.origElem, "grader");
         // CodeTailor: proactively set the puzzle as scaffolding puzzle to differentiate with other puzzle practice formats
-        var scaffolding = $(this.origElem).data("scaffolding");
+        var scaffolding = getDataValue(this.origElem, "scaffolding");
         options["numbered"] = numbered;
         options["grader"] = grader;
         options["scaffolding"] = scaffolding;
@@ -145,7 +151,7 @@ export default class Parsons extends RunestoneBase {
             locale = "en";
         }
         options["locale"] = locale;
-        var language = $(this.origElem).data("language");
+        var language = getDataValue(this.origElem, "language");
         if (language == undefined) {
             language = eBookConfig.language;
             if (language == undefined) {
@@ -168,9 +174,9 @@ export default class Parsons extends RunestoneBase {
         }
         options["prettifyLanguage"] = prettifyLanguage;
         //runnable if the parent has a parsons-runnable attr
-        options["runnable"] = $(this.origElem).data("runnable");
+        options["runnable"] = getDataValue(this.origElem, "runnable");
         // Parse block explanations if present
-        var explanationsRaw = $(this.origElem).attr("data-explanations");
+        var explanationsRaw = this.origElem.getAttribute("data-explanations");
         if (explanationsRaw) {
             try {
                 this.blockExplanations = JSON.parse(explanationsRaw);
@@ -185,7 +191,7 @@ export default class Parsons extends RunestoneBase {
     // Based on what is specified in the original HTML, create the HTML view
     initializeView() {
         this.outerDiv = document.createElement("div");
-        $(this.outerDiv).addClass("parsons");
+        this.outerDiv.classList.add("parsons");
         this.outerDiv.id = this.counterId;
         // parsons-text exists in source, make sure it has exercise-statement class
         let parsonsTextDiv = this.containerDiv.querySelector(".parsons-text");
@@ -195,57 +201,57 @@ export default class Parsons extends RunestoneBase {
         )
             parsonsTextDiv.classList.add("exercise-statement");
         this.keyboardTip = document.createElement("div");
-        $(this.keyboardTip).attr("role", "tooltip");
+        this.keyboardTip.setAttribute("role", "tooltip");
         this.keyboardTip.id = this.counterId + "-tip";
         this.keyboardTip.innerHTML = t("msg_parson_arrow_navigate");
         this.outerDiv.appendChild(this.keyboardTip);
-        $(this.keyboardTip).hide();
+        this.keyboardTip.style.display = "none";
         this.sortContainerDiv = document.createElement("div");
-        $(this.sortContainerDiv).addClass("sortable-code-container");
-        $(this.sortContainerDiv).attr(
+        this.sortContainerDiv.classList.add("sortable-code-container");
+        this.sortContainerDiv.setAttribute(
             "aria-describedby",
             this.counterId + "-tip",
         );
         this.outerDiv.appendChild(this.sortContainerDiv);
         this.sourceRegionDiv = document.createElement("div");
         this.sourceRegionDiv.id = this.counterId + "-sourceRegion";
-        $(this.sourceRegionDiv).addClass("sortable-code");
+        this.sourceRegionDiv.classList.add("sortable-code");
         this.sourceLabel = document.createElement("div");
-        $(this.sourceLabel).attr("role", "tooltip");
+        this.sourceLabel.setAttribute("role", "tooltip");
         this.sourceLabel.id = this.counterId + "-sourceTip";
         this.sourceLabel.innerHTML = t("msg_parson_drag_from_here");
         this.sourceRegionDiv.appendChild(this.sourceLabel);
         this.sortContainerDiv.appendChild(this.sourceRegionDiv);
         this.sourceArea = document.createElement("div");
         this.sourceArea.id = this.counterId + "-source";
-        $(this.sourceArea).addClass("source");
-        $(this.sourceArea).attr(
+        this.sourceArea.classList.add("source");
+        this.sourceArea.setAttribute(
             "aria-describedby",
             this.counterId + "-sourceTip",
         );
         this.sourceRegionDiv.appendChild(this.sourceArea);
         this.answerRegionDiv = document.createElement("div");
         this.answerRegionDiv.id = this.counterId + "-answerRegion";
-        $(this.answerRegionDiv).addClass("sortable-code");
+        this.answerRegionDiv.classList.add("sortable-code");
         this.answerLabel = document.createElement("div");
-        $(this.answerLabel).attr("role", "tooltip");
+        this.answerLabel.setAttribute("role", "tooltip");
         this.answerLabel.id = this.counterId + "-answerTip";
         this.answerLabel.innerHTML = t("msg_parson_drag_to_here");
         this.answerRegionDiv.appendChild(this.answerLabel);
         this.sortContainerDiv.appendChild(this.answerRegionDiv);
         this.answerArea = document.createElement("div");
         this.answerArea.id = this.counterId + "-answer";
-        $(this.answerArea).attr(
+        this.answerArea.setAttribute(
             "aria-describedby",
             this.counterId + "-answerTip",
         );
         this.answerRegionDiv.appendChild(this.answerArea);
         this.parsonsControlDiv = document.createElement("div");
-        $(this.parsonsControlDiv).addClass("parsons-controls");
+        this.parsonsControlDiv.classList.add("parsons-controls");
         this.outerDiv.appendChild(this.parsonsControlDiv);
         var that = this;
         this.checkButton = document.createElement("button");
-        $(this.checkButton).attr("class", "btn btn-success");
+        this.checkButton.className = "btn btn-success";
         this.checkButton.textContent = t("msg_parson_check_me");
         this.checkButton.id = this.counterId + "-check";
         this.parsonsControlDiv.appendChild(this.checkButton);
@@ -257,7 +263,7 @@ export default class Parsons extends RunestoneBase {
             that.renderFeedback();
         });
         this.resetButton = document.createElement("button");
-        $(this.resetButton).attr("class", "btn btn-default");
+        this.resetButton.className = "btn btn-default";
         this.resetButton.textContent = t("msg_parson_reset");
         this.resetButton.id = this.counterId + "-reset";
         this.resetButton.type = "button";
@@ -265,7 +271,7 @@ export default class Parsons extends RunestoneBase {
         this.resetButton.addEventListener("click", function (event) {
             event.preventDefault();
             that.clearFeedback();
-            $(that.checkButton).prop("disabled", false);
+            that.checkButton.disabled = false;
             that.resetView();
             that.checkCount = 0;
             that.logMove("reset");
@@ -273,7 +279,7 @@ export default class Parsons extends RunestoneBase {
         });
         if (this.options.adaptive) {
             this.helpButton = document.createElement("button");
-            $(this.helpButton).attr("class", "btn btn-primary");
+            this.helpButton.className = "btn btn-primary";
             this.helpButton.textContent = t("msg_parson_help");
             this.helpButton.id = this.counterId + "-help";
             this.helpButton.disabled = false; // bje
@@ -289,13 +295,16 @@ export default class Parsons extends RunestoneBase {
         this.messageDiv.setAttribute("role", "status");
         this.parsonsControlDiv.appendChild(this.messageDiv);
         this.messageDiv.style.visibility = "hidden";
-        $(this.origElem).replaceWith(this.outerDiv);
-        $(this.outerDiv).closest(".sqcontainer").css("max-width", "none");
+        this.origElem.replaceWith(this.outerDiv);
+        const sqContainer = this.outerDiv.closest(".sqcontainer");
+        if (sqContainer) {
+            sqContainer.style.maxWidth = "none";
+        }
         if (this.outerDiv) {
-            if ($(this.question).html().match(/^\s+$/)) {
-                $(this.question).remove();
+            if (this.question.innerHTML.match(/^\s+$/)) {
+                this.question.remove();
             } else {
-                $(this.outerDiv).prepend(this.question);
+                this.outerDiv.prepend(this.question);
             }
         }
     }
@@ -444,7 +453,7 @@ export default class Parsons extends RunestoneBase {
                             }
                             solution.push(line);
                         }
-                        if ($.inArray(line.indent, indents) == -1) {
+                        if (indents.indexOf(line.indent) == -1) {
                             indents.push(line.indent);
                         }
                     }
@@ -626,7 +635,7 @@ export default class Parsons extends RunestoneBase {
                         }
                         solution.push(line);
                     }
-                    if ($.inArray(line.indent, indents) == -1) {
+                    if (indents.indexOf(line.indent) == -1) {
                         indents.push(line.indent);
                     }
                 }
@@ -1247,8 +1256,8 @@ export default class Parsons extends RunestoneBase {
 
                     if (hasNormalBlock) {
                         // if a placeholder has a normal block before or after, hide it;
-                        if (!$(placeholder.view).hasClass("hide")) {
-                            $(placeholder.view).addClass("hide");
+                        if (!placeholder.view.classList.contains("hide")) {
+                            placeholder.view.classList.add("hide");
                             for (
                                 var j = 0;
                                 j < answerBlocks.length - 1 - i;
@@ -1267,7 +1276,7 @@ export default class Parsons extends RunestoneBase {
             var contentLength = 0;
             while (
                 contentLength < answerBlocks.length &&
-                !$(answerBlocks[contentLength].view).hasClass("hide")
+                !answerBlocks[contentLength].view.classList.contains("hide")
             ) {
                 contentLength++;
             }
@@ -1319,8 +1328,8 @@ export default class Parsons extends RunestoneBase {
                     console.log("error moving up; no placeholder found");
                     return;
                 }
-                if ($(placeholder.view).hasClass("hide")) {
-                    $(placeholder.view).removeClass("hide");
+                if (placeholder.view.classList.contains("hide")) {
+                    placeholder.view.classList.remove("hide");
                     // should be moved right before the block answerBlocks[i]
                     for (var j = 0; j < placeholderIndex - i; j++) {
                         placeholder.moveUp();
@@ -1548,7 +1557,7 @@ export default class Parsons extends RunestoneBase {
             block = input[i];
             if (!block.isDistractor()) {
                 output.push(block);
-            } else if ($.inArray(block, distractors) == -1) {
+            } else if (distractors.indexOf(block) == -1) {
                 output.push(block);
             }
         }
@@ -1656,7 +1665,7 @@ export default class Parsons extends RunestoneBase {
                 for (i = 0; i < unorderedBlocks.length; i++) {
                     block = unorderedBlocks[i];
                     if (block.lines[0].distractor) {
-                        if ($.inArray(block, distractors) > -1) {
+                        if (distractors.indexOf(block) > -1) {
                             blocks.push(block);
                         } else {
                             removedBlocks.push(i);
@@ -1712,7 +1721,7 @@ export default class Parsons extends RunestoneBase {
                 block = originalBlocks[this.options.order[i]];
                 if (
                     block !== undefined &&
-                    $.inArray(this.options.order[i], removedBlocks) < 0
+                    removedBlocks.indexOf(this.options.order[i]) < 0
                 ) {
                     blocks.push(block);
                 }
@@ -1727,7 +1736,7 @@ export default class Parsons extends RunestoneBase {
                     var index =
                         this.options.order == undefined
                             ? Math.random(0, blocks.length)
-                            : $.inArray(removedBlocks[i], this.options.order);
+                            : this.options.order.indexOf(removedBlocks[i]);
                     blocks.splice(index, 0, originalBlocks[removedBlocks[i]]);
                 }
             }
@@ -1738,15 +1747,15 @@ export default class Parsons extends RunestoneBase {
             for (i = 1; i < originalBlocks.length; i++) {
                 if (
                     originalBlocks[i].lines[0].paired &&
-                    $.inArray(originalBlocks[i], blocks) >= 0
+                    blocks.indexOf(originalBlocks[i]) >= 0
                 ) {
                     var j = i;
-                    while ($.inArray(originalBlocks[j - 1], blocks) < 0) {
+                    while (blocks.indexOf(originalBlocks[j - 1]) < 0) {
                         // find the paired distractor or solution block it will be next to
                         j--;
                     }
-                    var indexTo = $.inArray(originalBlocks[j - 1], blocks);
-                    var indexFrom = $.inArray(originalBlocks[i], blocks);
+                    var indexTo = blocks.indexOf(originalBlocks[j - 1]);
+                    var indexFrom = blocks.indexOf(originalBlocks[i]);
                     blocks.splice(indexFrom, 1);
                     blocks.splice(indexTo, 0, originalBlocks[i]);
                 }
@@ -1887,10 +1896,10 @@ export default class Parsons extends RunestoneBase {
     // Return array of codeblocks based on what is in the source field
     sourceBlocks() {
         var sourceBlocks = [];
-        var children = this.sourceArea.childNodes;
+        var children = this.sourceArea.children;
         for (var i = 0; i < children.length; i++) {
             var child = children[i];
-            if ($(child).hasClass("block")) {
+            if (child.classList.contains("block")) {
                 sourceBlocks.push(this.getBlockById(child.id));
             }
         }
@@ -1982,7 +1991,7 @@ export default class Parsons extends RunestoneBase {
             if (this.grade == "correct") {
                 this.hasSolved = true;
                 this.correct = true;
-                $(this.checkButton).prop("disabled", true);
+                this.checkButton.disabled = true;
                 // CodeTailor: make the Copy Answer button visible when the puzzle is solved
                 if (this.hasSolved && this.options.scaffolding) {
                     const copyBtn = document.querySelector(
@@ -2060,40 +2069,38 @@ export default class Parsons extends RunestoneBase {
     renderFeedback() {
         this.grader.showfeedback = true;
         this.grade = this.grader.graderState;
-        var feedbackArea;
-        var answerArea = $(this.answerArea);
+        var answerArea = this.answerArea;
 
-        if (this.showfeedback === true) {
-            feedbackArea = $(this.messageDiv);
-        } else {
-            feedbackArea = $("#doesnotexist");
-        }
+        // When feedback is suppressed the messages are simply not written.
+        var feedbackArea = this.showfeedback === true ? this.messageDiv : null;
+        var setFeedback = (className, message) => {
+            if (feedbackArea) {
+                feedbackArea.setAttribute("class", className);
+                setTimeout(() => {
+                    feedbackArea.innerHTML = message;
+                }, 10);
+            }
+        };
 
         if (this.grade === "correct") {
-            answerArea.addClass("correct");
+            answerArea.classList.add("correct");
             this.messageDiv.style.visibility = "visible";
-            feedbackArea.attr("class", "alert alert-info");
             let message =
                 this.checkCount > 1
                     ? t("msg_parson_correct", this.checkCount)
                     : t("msg_parson_correct_first_try");
             if (this.options.runnable)
                 message += " " + t("msg_parson_correct_runnable");
-            setTimeout(() => {
-                feedbackArea.html(message);
-            }, 10);
+            setFeedback("alert alert-info", message);
             // Show block explanations after solving
             this.showBlockExplanations();
         }
 
         if (this.grade === "incorrectTooShort") {
             // too little code
-            answerArea.addClass("incorrect");
+            answerArea.classList.add("incorrect");
             this.messageDiv.style.visibility = "visible";
-            feedbackArea.attr("class", "alert alert-danger");
-            setTimeout(() => {
-                feedbackArea.html(t("msg_parson_too_short"));
-            }, 10);
+            setFeedback("alert alert-danger", t("msg_parson_too_short"));
         }
 
         if (this.grade === "incorrectIndent") {
@@ -2102,25 +2109,23 @@ export default class Parsons extends RunestoneBase {
                 block = this.grader.indentLeft[i].block();
                 if (incorrectBlocks.indexOf(block) == -1) {
                     incorrectBlocks.push(block);
-                    $(block.view).addClass("indentLeft");
+                    block.view.classList.add("indentLeft");
                 }
             }
             for (let i = 0; i < this.grader.indentRight.length; i++) {
                 block = this.grader.indentRight[i].block();
                 if (incorrectBlocks.indexOf(block) == -1) {
                     incorrectBlocks.push(block);
-                    $(block.view).addClass("indentRight");
+                    block.view.classList.add("indentRight");
                 }
             }
             this.messageDiv.style.visibility = "visible";
-            feedbackArea.attr("class", "alert alert-danger");
-            setTimeout(() => {
-                if (incorrectBlocks.length == 1) {
-                    feedbackArea.html(t("msg_parson_wrong_indent"));
-                } else {
-                    feedbackArea.html(t("msg_parson_wrong_indents"));
-                }
-            }, 10);
+            setFeedback(
+                "alert alert-danger",
+                incorrectBlocks.length == 1
+                    ? t("msg_parson_wrong_indent")
+                    : t("msg_parson_wrong_indents"),
+            );
         }
 
         if (this.grade === "incorrectMoveBlocks") {
@@ -2145,17 +2150,14 @@ export default class Parsons extends RunestoneBase {
             for (let i = 0; i < lisIndexes.length; i++) {
                 notInSolution.push(inSolution[lisIndexes[i]]);
             }
-            answerArea.addClass("incorrect");
+            answerArea.classList.add("incorrect");
             this.messageDiv.style.visibility = "visible";
-            feedbackArea.attr("class", "alert alert-danger");
             if (this.showfeedback === true) {
                 for (let i = 0; i < notInSolution.length; i++) {
-                    $(notInSolution[i].view).addClass("incorrectPosition");
+                    notInSolution[i].view.classList.add("incorrectPosition");
                 }
             }
-            setTimeout(() => {
-                feedbackArea.html(t("msg_parson_wrong_order"));
-            }, 10);
+            setFeedback("alert alert-danger", t("msg_parson_wrong_order"));
         }
     }
 
@@ -2185,20 +2187,21 @@ export default class Parsons extends RunestoneBase {
                 // set data-toggle="tooltip" and title attribute for hover display
                 block.view.setAttribute("data-toggle", "tooltip");
                 block.view.setAttribute("title", explanation);
-                $(block.view).addClass("has-explanation");
+                block.view.classList.add("has-explanation");
             }
         }
     }
 
     // Remove all block explanations (tooltip attributes)
     hideBlockExplanations() {
-        var blocks = $(this.answerArea).find(".has-explanation");
-        blocks.each(function () {
-            this.removeAttribute("data-toggle");
-            this.removeAttribute("title");
-            this.removeAttribute("data-original-title");
-        });
-        blocks.removeClass("has-explanation");
+        for (const view of this.answerArea.querySelectorAll(
+            ".has-explanation",
+        )) {
+            view.removeAttribute("data-toggle");
+            view.removeAttribute("title");
+            view.removeAttribute("data-original-title");
+            view.classList.remove("has-explanation");
+        }
     }
 
     /* =====================================================================
@@ -2299,11 +2302,11 @@ export default class Parsons extends RunestoneBase {
     // Remove this distractors to make the problem easier
     removeDistractor(block) {
         // Alert the user to what is happening
-        var feedbackArea = $(this.messageDiv);
+        var feedbackArea = this.messageDiv;
         this.messageDiv.style.visibility = "visible";
-        feedbackArea.attr("class", "alert alert-info");
+        feedbackArea.setAttribute("class", "alert alert-info");
         setTimeout(() => {
-            feedbackArea.html(t("msg_parson_not_solution"));
+            feedbackArea.innerHTML = t("msg_parson_not_solution");
         }, 10);
         // Stop ability to select
         if (block.lines[0].distractHelptext) {
@@ -2326,14 +2329,14 @@ export default class Parsons extends RunestoneBase {
             if (slideUnderBlock !== undefined) {
                 endY +=
                     slideUnderBlock.view.getBoundingClientRect().height + 20;
-                endY += parseInt($(slideUnderBlock.view).css("top"));
+                endY +=
+                    parseInt(getComputedStyle(slideUnderBlock.view).top) || 0;
             }
             var that = this;
-            $(block.view).css({
-                "border-color": "#000",
-                "background-color": "#fff",
-            });
-            $(block.view).animate(
+            block.view.style.borderColor = "#000";
+            block.view.style.backgroundColor = "#fff";
+            animate(
+                block.view,
                 {
                     opacity: 1.0,
                 },
@@ -2361,7 +2364,8 @@ export default class Parsons extends RunestoneBase {
                         delete that.movingX;
                         delete that.movingY;
                         that.updateView();
-                        $(block.view).animate(
+                        animate(
+                            block.view,
                             {
                                 opacity: 0.3,
                                 "border-color": "#d3d3d3",
@@ -2370,12 +2374,10 @@ export default class Parsons extends RunestoneBase {
                             {
                                 duration: 1000,
                                 complete: function () {
-                                    $(block.view).css({
-                                        opacity: "",
-                                        "border-color": "",
-                                        "background-color": "",
-                                    });
-                                    $(block.view).addClass("disabled");
+                                    block.view.style.opacity = "";
+                                    block.view.style.borderColor = "";
+                                    block.view.style.backgroundColor = "";
+                                    block.view.classList.add("disabled");
                                 },
                             },
                         );
@@ -2383,11 +2385,10 @@ export default class Parsons extends RunestoneBase {
                 },
             );
         } else {
-            $(block.view).css({
-                "border-color": "#000",
-                "background-color": "#fff",
-            });
-            $(block.view).animate(
+            block.view.style.borderColor = "#000";
+            block.view.style.backgroundColor = "#fff";
+            animate(
+                block.view,
                 {
                     opacity: 0.3,
                     "border-color": "#d3d3d3",
@@ -2396,10 +2397,8 @@ export default class Parsons extends RunestoneBase {
                 {
                     duration: 2000,
                     complete: function () {
-                        $(block.view).css({
-                            "border-color": "",
-                            "background-color": "",
-                        });
+                        block.view.style.borderColor = "";
+                        block.view.style.backgroundColor = "";
                     },
                 },
             );
@@ -2408,11 +2407,11 @@ export default class Parsons extends RunestoneBase {
     // Give the user the indentation
     removeIndentation() {
         // Alert the user to what is happening
-        var feedbackArea = $(this.messageDiv);
+        var feedbackArea = this.messageDiv;
         this.messageDiv.style.visibility = "visible";
-        feedbackArea.attr("class", "alert alert-info");
+        feedbackArea.setAttribute("class", "alert alert-info");
         setTimeout(() => {
-            feedbackArea.html(t("msg_parson_provided_indent"));
+            feedbackArea.innerHTML = t("msg_parson_provided_indent");
         }, 10);
         // Move and resize blocks
         var blockWidth = 200;
@@ -2432,7 +2431,8 @@ export default class Parsons extends RunestoneBase {
             block = sourceBlocks[i];
             indent = block.solutionIndent();
             if (indent == 0) {
-                $(block.view).animate(
+                animate(
+                    block.view,
                     {
                         width: blockWidth,
                     },
@@ -2441,7 +2441,8 @@ export default class Parsons extends RunestoneBase {
                     },
                 );
             } else {
-                $(block.view).animate(
+                animate(
+                    block.view,
                     {
                         width:
                             blockWidth - indent * this.options.pixelsPerIndent,
@@ -2455,7 +2456,8 @@ export default class Parsons extends RunestoneBase {
             }
         }
         for (let i = 0; i < this.pairedDivs.length; i++) {
-            $(this.pairedDivs[i]).animate(
+            animate(
+                this.pairedDivs[i],
                 {
                     width: blockWidth + 34,
                 },
@@ -2469,7 +2471,8 @@ export default class Parsons extends RunestoneBase {
             block = answerBlocks[i];
             indent = block.solutionIndent();
             if (indent == 0) {
-                $(block.view).animate(
+                animate(
+                    block.view,
                     {
                         left: 0,
                         width: blockWidth,
@@ -2479,7 +2482,8 @@ export default class Parsons extends RunestoneBase {
                     },
                 );
             } else {
-                $(block.view).animate(
+                animate(
+                    block.view,
                     {
                         left: 0,
                         width:
@@ -2494,11 +2498,17 @@ export default class Parsons extends RunestoneBase {
             }
         }
         // Resize answer and source area
-        $(this.answerArea).removeClass("answer1 answer2 answer3 answer4");
-        $(this.answerArea).addClass("answer");
+        this.answerArea.classList.remove(
+            "answer1",
+            "answer2",
+            "answer3",
+            "answer4",
+        );
+        this.answerArea.classList.add("answer");
         this.indent = 0;
         this.noindent = true;
-        $(this.sourceArea).animate(
+        animate(
+            this.sourceArea,
             {
                 width: this.areaWidth + 2,
             },
@@ -2506,7 +2516,8 @@ export default class Parsons extends RunestoneBase {
                 duration: 1000,
             },
         );
-        $(this.answerArea).animate(
+        animate(
+            this.answerArea,
             {
                 width: this.areaWidth + 2,
             },
@@ -2514,17 +2525,15 @@ export default class Parsons extends RunestoneBase {
                 duration: 1000,
             },
         );
-        // Change the model (with view)
-        $(this.answerArea).animate(
+        // Change the model (with view) once the resize animations are done
+        animate(
+            this.answerArea,
             {
                 opacity: 1.0,
             },
             {
                 duration: 1100,
                 complete: function () {
-                    $(this.answerArea).css({
-                        opacity: "",
-                    });
                     // Update the model
                     for (let i = 0; i < sourceBlocks.length; i++) {
                         sourceBlocks[i].addIndent();
@@ -2603,11 +2612,11 @@ export default class Parsons extends RunestoneBase {
         var sourceBlocks = this.sourceBlocks();
 
         // Alert the user to what is happening
-        var feedbackArea = $(this.messageDiv);
+        var feedbackArea = this.messageDiv;
         this.messageDiv.style.visibility = "visible";
-        feedbackArea.attr("class", "alert alert-info");
+        feedbackArea.setAttribute("class", "alert alert-info");
         setTimeout(() => {
-            feedbackArea.html(t("msg_parson_combined_blocks"));
+            feedbackArea.innerHTML = t("msg_parson_combined_blocks");
         }, 10);
         var block1 = null;
         var block2 = null;
@@ -2667,21 +2676,18 @@ export default class Parsons extends RunestoneBase {
                 endY += block2.view.getBoundingClientRect().height / 2;
             }
             var that = this;
-            $(block2.view).animate(
+            animate(
+                block2.view,
                 {
                     opacity: 1,
                 },
                 {
                     duration: 1000, // 1 seccond
                     start: function () {
-                        $(block1.view).css({
-                            "border-color": "#000",
-                            "background-color": "#fff",
-                        });
-                        $(block2.view).css({
-                            "border-color": "#000",
-                            "background-color": "#fff",
-                        });
+                        block1.view.style.borderColor = "#000";
+                        block1.view.style.backgroundColor = "#fff";
+                        block2.view.style.borderColor = "#000";
+                        block2.view.style.backgroundColor = "#fff";
                         block2.lines[0].index += 1000;
                         that.moving = block2;
                         that.movingX = startX;
@@ -2700,7 +2706,8 @@ export default class Parsons extends RunestoneBase {
                         that.updateView();
                         block2.lines[0].index -= 1000;
                         block1.consumeBlock(block2);
-                        $(block1.view).animate(
+                        animate(
+                            block1.view,
                             {
                                 "border-color": "#d3d3d3",
                                 "background-color": "#efefef",
@@ -2708,10 +2715,8 @@ export default class Parsons extends RunestoneBase {
                             {
                                 duration: 1000,
                                 complete: function () {
-                                    $(block1.view).css({
-                                        "border-color": "",
-                                        "background-color": "",
-                                    });
+                                    block1.view.style.borderColor = "";
+                                    block1.view.style.backgroundColor = "";
                                 },
                             },
                         );
@@ -2719,25 +2724,23 @@ export default class Parsons extends RunestoneBase {
                 },
             );
         } else {
-            $(block2.view).animate(
+            animate(
+                block2.view,
                 {
                     opacity: 1,
                 },
                 {
                     duration: 1000,
                     start: function () {
-                        $(block1.view).css({
-                            "border-color": "#000",
-                            "background-color": "#fff",
-                        });
-                        $(block2.view).css({
-                            "border-color": "#000",
-                            "background-color": "#fff",
-                        });
+                        block1.view.style.borderColor = "#000";
+                        block1.view.style.backgroundColor = "#fff";
+                        block2.view.style.borderColor = "#000";
+                        block2.view.style.backgroundColor = "#fff";
                     },
                     complete: function () {
                         block1.consumeBlock(block2);
-                        $(block1.view).animate(
+                        animate(
+                            block1.view,
                             {
                                 "border-color": "#d3d3d3",
                                 "background-color": "#efefef",
@@ -2745,10 +2748,8 @@ export default class Parsons extends RunestoneBase {
                             {
                                 duration: 1000,
                                 complete: function () {
-                                    $(block1.view).css({
-                                        "border-color": "",
-                                        "background-color": "",
-                                    });
+                                    block1.view.style.borderColor = "";
+                                    block1.view.style.backgroundColor = "";
                                 },
                             },
                         );
@@ -2842,27 +2843,30 @@ export default class Parsons extends RunestoneBase {
     ===================================================================== */
     // When the user has entered the Parsons problem via keyboard mode
     enterKeyboardMode() {
-        $(this.keyboardTip).show();
-        $(this.sourceLabel).hide();
-        $(this.answerLabel).hide();
+        this.keyboardTip.style.display = "";
+        this.sourceLabel.style.display = "none";
+        this.answerLabel.style.display = "none";
         this.clearFeedback();
     }
     // When the user leaves the Parsons problem via keyboard mode
     exitKeyboardMode() {
-        $(this.keyboardTip).hide();
-        $(this.sourceLabel).show();
-        $(this.answerLabel).show();
+        this.keyboardTip.style.display = "none";
+        this.sourceLabel.style.display = "";
+        this.answerLabel.style.display = "";
     }
     /* =====================================================================
     ==== VIEW ==============================================================
     ===================================================================== */
     // Clear any feedback from the answer area
     clearFeedback() {
-        $(this.answerArea).removeClass("incorrect correct");
-        var children = this.answerArea.childNodes;
+        this.answerArea.classList.remove("incorrect", "correct");
+        var children = this.answerArea.children;
         for (var i = 0; i < children.length; i++) {
-            $(children[i]).removeClass(
-                "correctPosition incorrectPosition indentLeft indentRight",
+            children[i].classList.remove(
+                "correctPosition",
+                "incorrectPosition",
+                "indentLeft",
+                "indentRight",
             );
         }
         this.messageDiv.style.visibility = "hidden";
@@ -2880,8 +2884,8 @@ export default class Parsons extends RunestoneBase {
             }
         }
         // Hide buttons
-        $(this.checkButton).hide();
-        $(this.resetButton).hide();
+        this.checkButton.style.display = "none";
+        this.resetButton.style.display = "none";
     }
     // Based on the moving element, etc., establish the moving state
     //   rest: not moving
@@ -2916,6 +2920,13 @@ export default class Parsons extends RunestoneBase {
         }
         return "moving";
     }
+    // Absolutely position a block or indicator view inside its area
+    placeView(el, left, top, width, zIndex) {
+        el.style.left = left + "px";
+        el.style.top = top + "px";
+        el.style.width = width + "px";
+        el.style.zIndex = zIndex;
+    }
     // Update the Parsons view
     // This gets called when dragging a block
     updateView() {
@@ -2941,8 +2952,8 @@ export default class Parsons extends RunestoneBase {
         var movingHeight;
         if (this.moving !== undefined) {
             // Must get height here as detached items don't have height
-            movingHeight = $(this.moving.view).outerHeight(true);
-            $(this.moving.view).detach();
+            movingHeight = outerHeight(this.moving.view);
+            this.moving.view.remove();
         }
         var positionTop, width;
         var baseWidth = this.areaWidth - 22;
@@ -3003,12 +3014,12 @@ export default class Parsons extends RunestoneBase {
                     var item = blocks[i];
                     var j;
                     if (!hasInserted && insertPositions.includes(i)) {
-                        var testHeight = $(item.view).outerHeight(true);
+                        var testHeight = outerHeight(item.view);
                         for (j = i + 1; j < blocks.length; j++) {
                             if (insertPositions.includes(j)) {
                                 break;
                             }
-                            testHeight += $(blocks[j].view).outerHeight(true);
+                            testHeight += outerHeight(blocks[j].view);
                         }
                         if (
                             y - positionTop < movingHeight + testHeight / 2 ||
@@ -3019,44 +3030,34 @@ export default class Parsons extends RunestoneBase {
                                 this.moving.view,
                                 item.view,
                             );
-                            $(this.moving.view).css({
-                                left: x,
-                                top: y - movingHeight / 2,
-                                width: baseWidth,
-                                "z-index": 3,
-                            });
+                            this.placeView(
+                                this.moving.view,
+                                x,
+                                y - movingHeight / 2,
+                                baseWidth,
+                                3,
+                            );
                             positionTop = positionTop + movingHeight;
                         }
                     }
-                    $(item.view).css({
-                        left: 0,
-                        top: positionTop,
-                        width: baseWidth,
-                        "z-index": 2,
-                    });
-                    positionTop = positionTop + $(item.view).outerHeight(true);
+                    this.placeView(item.view, 0, positionTop, baseWidth, 2);
+                    positionTop = positionTop + outerHeight(item.view);
                 }
                 if (!hasInserted) {
-                    $(this.moving.view).appendTo(
-                        "#" + this.counterId + "-source",
+                    this.sourceArea.appendChild(this.moving.view);
+                    this.placeView(
+                        this.moving.view,
+                        x,
+                        y - outerHeight(this.moving.view) / 2,
+                        baseWidth,
+                        3,
                     );
-                    $(this.moving.view).css({
-                        left: x,
-                        top: y - $(this.moving.view).outerHeight(true) / 2,
-                        width: baseWidth,
-                        "z-index": 3,
-                    });
                 }
             } else {
                 for (var i = 0; i < blocks.length; i++) {
                     item = blocks[i];
-                    $(item.view).css({
-                        left: 0,
-                        top: positionTop,
-                        width: baseWidth,
-                        "z-index": 2,
-                    });
-                    positionTop = positionTop + $(item.view).outerHeight(true);
+                    this.placeView(item.view, 0, positionTop, baseWidth, 2);
+                    positionTop = positionTop + outerHeight(item.view);
                 }
             }
             // Update the Paired Distractor Indicators
@@ -3071,36 +3072,30 @@ export default class Parsons extends RunestoneBase {
                 }
                 var div = this.pairedDivs[i];
                 if (matching.length == 0) {
-                    $(div).hide();
+                    div.style.display = "none";
                 } else {
-                    $(div).show();
+                    div.style.display = "";
                     var height = -5;
-                    height += parseInt(
-                        $(matching[matching.length - 1].view).css("top"),
-                    );
-                    height -= parseInt($(matching[0].view).css("top"));
-                    height += $(matching[matching.length - 1].view).outerHeight(
-                        true,
-                    );
-                    $(div).css({
-                        left: -6,
-                        top: $(matching[0].view).css("top"),
-                        width: baseWidth + 34,
-                        height: height,
-                        "z-index": 1,
-                        "text-indent": -30,
-                        "padding-top": (height - 70) / 2,
-                        overflow: "visible",
-                        "font-size": 43,
-                        "vertical-align": "middle",
-                        color: "#7e7ee7",
-                    });
-                    $(div).html(
-                        "or{",
-                    );
+                    var lastView = matching[matching.length - 1].view;
+                    height += parseInt(getComputedStyle(lastView).top);
+                    height -= parseInt(getComputedStyle(matching[0].view).top);
+                    height += outerHeight(lastView);
+                    div.style.left = "-6px";
+                    div.style.top = getComputedStyle(matching[0].view).top;
+                    div.style.width = baseWidth + 34 + "px";
+                    div.style.height = height + "px";
+                    div.style.zIndex = 1;
+                    div.style.textIndent = "-30px";
+                    div.style.paddingTop = (height - 70) / 2 + "px";
+                    div.style.overflow = "visible";
+                    div.style.fontSize = "43px";
+                    div.style.verticalAlign = "middle";
+                    div.style.color = "#7e7ee7";
+                    div.innerHTML =
+                        "or{";
                 }
                 if (matching.length == 1) {
-                    $(div).html("");
+                    div.innerHTML = "";
                 }
             }
         }
@@ -3139,53 +3134,55 @@ export default class Parsons extends RunestoneBase {
                     if (!hasInserted) {
                         if (
                             y - positionTop <
-                            (movingHeight + $(block.view).outerHeight(true)) / 2
+                            (movingHeight + outerHeight(block.view)) / 2
                         ) {
                             hasInserted = true;
                             this.answerArea.insertBefore(
                                 this.moving.view,
                                 block.view,
                             );
-                            $(this.moving.view).css({
-                                left: x,
-                                top: y - movingHeight / 2,
-                                width: baseWidth,
-                                "z-index": 3,
-                            });
+                            this.placeView(
+                                this.moving.view,
+                                x,
+                                y - movingHeight / 2,
+                                baseWidth,
+                                3,
+                            );
                             positionTop = positionTop + movingHeight;
                         }
                     }
                     indent = block.indent * this.options.pixelsPerIndent;
-                    $(block.view).css({
-                        left: indent,
-                        top: positionTop,
-                        width: width - indent,
-                        "z-index": 2,
-                    });
-                    positionTop = positionTop + $(block.view).outerHeight(true);
+                    this.placeView(
+                        block.view,
+                        indent,
+                        positionTop,
+                        width - indent,
+                        2,
+                    );
+                    positionTop = positionTop + outerHeight(block.view);
                 }
                 if (!hasInserted) {
-                    $(this.moving.view).appendTo(
-                        "#" + this.counterId + "-answer",
+                    this.answerArea.appendChild(this.moving.view);
+                    this.placeView(
+                        this.moving.view,
+                        x,
+                        y - outerHeight(this.moving.view) / 2,
+                        baseWidth,
+                        3,
                     );
-                    $(this.moving.view).css({
-                        left: x,
-                        top: y - $(this.moving.view).outerHeight(true) / 2,
-                        width: baseWidth,
-                        "z-index": 3,
-                    });
                 }
             } else {
                 for (let i = 0; i < blocks.length; i++) {
                     block = blocks[i];
                     indent = block.indent * this.options.pixelsPerIndent;
-                    $(block.view).css({
-                        left: indent,
-                        top: positionTop,
-                        width: width - indent,
-                        "z-index": 2,
-                    });
-                    positionTop = positionTop + $(block.view).outerHeight(true);
+                    this.placeView(
+                        block.view,
+                        indent,
+                        positionTop,
+                        width - indent,
+                        2,
+                    );
+                    positionTop = positionTop + outerHeight(block.view);
                 }
             }
         }
@@ -3194,7 +3191,7 @@ export default class Parsons extends RunestoneBase {
             // Add it to the lowest place in the source area
             movingBin = this.moving.pairedBin();
             if (movingBin == -1) {
-                $(this.moving.view).appendTo("#" + this.counterId + "-source");
+                this.sourceArea.appendChild(this.moving.view);
             } else {
                 var before;
                 blocks = this.sourceBlocks;
@@ -3205,9 +3202,7 @@ export default class Parsons extends RunestoneBase {
                     }
                 }
                 if (before == undefined || before == blocks.length) {
-                    $(this.moving.view).appendTo(
-                        "#" + this.counterId + "-source",
-                    );
+                    this.sourceArea.appendChild(this.moving.view);
                 } else {
                     this.sourceArea.insertBefore(
                         this.moving.view,
@@ -3216,20 +3211,19 @@ export default class Parsons extends RunestoneBase {
                 }
             }
             // Place in the middle of the mouse cursor
-            $(this.moving.view).css({
-                left:
-                    this.movingX -
+            this.placeView(
+                this.moving.view,
+                this.movingX -
                     this.sourceArea.getBoundingClientRect().left -
                     window.pageXOffset -
-                    $(this.moving.view).outerWidth(true) / 2,
-                top:
-                    this.movingY -
+                    outerWidth(this.moving.view) / 2,
+                this.movingY -
                     this.sourceArea.getBoundingClientRect().top -
                     window.pageYOffset -
                     movingHeight / 2,
-                width: baseWidth,
-                "z-index": 3,
-            });
+                baseWidth,
+                3,
+            );
         }
         state = newState;
         this.state = state;
@@ -3268,12 +3262,14 @@ export default class Parsons extends RunestoneBase {
         }
         if (blocksNotInBins + this.pairedBins.length >= 10) {
             this.areaWidth += 5;
-            $(this.sourceArea).css({
-                width: $(this.sourceArea).width() + 5,
-            });
-            $(this.answerArea).css({
-                width: $(this.answerArea).width() + 5,
-            });
+            this.sourceArea.style.width =
+                (parseFloat(getComputedStyle(this.sourceArea).width) || 0) +
+                5 +
+                "px";
+            this.answerArea.style.width =
+                (parseFloat(getComputedStyle(this.answerArea).width) || 0) +
+                5 +
+                "px";
         }
     }
     // Put all the blocks back into the source area, reshuffling as necessary
@@ -3294,24 +3290,25 @@ export default class Parsons extends RunestoneBase {
         for (let i = 0; i < this.blocks.length; i++) {
             block = this.blocks[i];
             for (var j = 0; j < block.lines.length; j++) {
-                var children = $(block.lines[j].view).find(".block-label");
+                var children =
+                    block.lines[j].view.querySelectorAll(".block-label");
                 for (var c = 0; c < children.length; c++) {
                     children[c].remove();
                 }
             }
             block.destroy();
-            $(this.blocks[i].view).detach();
+            this.blocks[i].view.remove();
         }
         delete this.blocks;
         this.blockIndex = 0;
         if (this.pairedDivs) {
             for (let i = 0; i < this.pairedDivs.length; i++) {
-                $(this.pairedDivs[i]).detach();
+                this.pairedDivs[i].remove();
             }
         }
-        $(this.sourceArea).attr("style", "");
-        $(this.answerArea).removeClass();
-        $(this.answerArea).attr("style", "");
+        this.sourceArea.setAttribute("style", "");
+        this.answerArea.className = "";
+        this.answerArea.setAttribute("style", "");
         this.noindent = this.options.noindent;
         // Reinitialize
         if (this.hasSolved) {
@@ -3351,19 +3348,19 @@ export default class Parsons extends RunestoneBase {
 
 Parsons.counter = 0;
 
-$(document).on("runestone:login-complete", function () {
-    $("[data-component=parsons]").each(function (index) {
-        if ($(this).closest("[data-component=timedAssessment]").length == 0) {
+document.addEventListener("runestone:login-complete", function () {
+    for (const el of document.querySelectorAll("[data-component=parsons]")) {
+        if (el.closest("[data-component=timedAssessment]") === null) {
             try {
-                window.componentMap[this.id] = new Parsons({
-                    orig: this,
+                window.componentMap[el.id] = new Parsons({
+                    orig: el,
                     useRunestoneServices: eBookConfig.useRunestoneServices,
                 });
             } catch (err) {
-                console.log(`Error rendering Parsons Problem ${this.id}
+                console.log(`Error rendering Parsons Problem ${el.id}
                              Details: ${err}`);
                 console.log(err.stack);
             }
         }
-    });
+    }
 });
diff --git a/bases/rsptx/interactives/runestone/parsons/js/parsonsBlock.js b/bases/rsptx/interactives/runestone/parsons/js/parsonsBlock.js
index 0f4c54a99..d9035d4cc 100644
--- a/bases/rsptx/interactives/runestone/parsons/js/parsonsBlock.js
+++ b/bases/rsptx/interactives/runestone/parsons/js/parsonsBlock.js
@@ -12,6 +12,15 @@
 
 import Hammer from "./hammer.min.js";
 
+const INDENT_CLASSES = [
+    "indent1",
+    "indent2",
+    "indent3",
+    "indent4",
+    "indent5",
+    "indent6",
+];
+
 // Initialize based on the problem and the lines
 export default class ParsonsBlock {
     constructor(problem, lines) {
@@ -23,14 +32,14 @@ export default class ParsonsBlock {
         var view = document.createElement("div");
         view.id = problem.counterId + "-block-" + problem.blockIndex;
         problem.blockIndex += 1;
-        $(view).addClass("block");
+        view.classList.add("block");
         var sharedIndent = lines[0].indent;
         for (let i = 1; i < lines.length; i++) {
             sharedIndent = Math.min(sharedIndent, lines[i].indent);
         }
         var lineDiv = document.createElement("div");
-        $(lineDiv).addClass("lines");
-        $(view).append(lineDiv);
+        lineDiv.classList.add("lines");
+        view.append(lineDiv);
         for (let i = 0; i < lines.length; i++) {
             var line = lines[i];
             var lineIndent;
@@ -39,36 +48,28 @@ export default class ParsonsBlock {
             } else {
                 lineIndent = line.indent - sharedIndent;
             }
-            $(line.view).removeClass(
-                "indent1 indent2 indent3 indent4 indent5 indent6",
-            );
+            line.view.classList.remove(...INDENT_CLASSES);
             if (
                 this.problem.options.language != "natural" &&
                 this.problem.options.language != "math"
             ) {
                 if (lineIndent > 0) {
-                    $(line.view).addClass("indent" + lineIndent);
+                    line.view.classList.add("indent" + lineIndent);
                 }
             }
             lineDiv.appendChild(line.view);
         }
         var labelDiv = document.createElement("div");
-        $(labelDiv).addClass("labels");
+        labelDiv.classList.add("labels");
         if (this.problem.options.numbered == "left") {
-            $(lineDiv).addClass("border_left");
-            $(view).prepend(labelDiv);
-            $(view).css({
-                "justify-content": "flex-start",
-            });
+            lineDiv.classList.add("border_left");
+            view.prepend(labelDiv);
+            view.style.justifyContent = "flex-start";
         } else if (this.problem.options.numbered == "right") {
-            $(labelDiv).addClass("border_left");
-            $(labelDiv).css({
-                float: "right",
-            });
-            $(view).css({
-                "justify-content": "space-between",
-            });
-            $(view).append(labelDiv);
+            labelDiv.classList.add("border_left");
+            labelDiv.style.float = "right";
+            view.style.justifyContent = "space-between";
+            view.append(labelDiv);
         }
         this.view = view;
         // is not placeholder by default
@@ -76,12 +77,10 @@ export default class ParsonsBlock {
     }
     // Add a line (from another block) to this block
     addLine(line) {
-        $(line.view).removeClass(
-            "indent1 indent2 indent3 indent4 indent5 indent6",
-        );
+        line.view.classList.remove(...INDENT_CLASSES);
         if (this.problem.noindent) {
             if (line.indent > 0) {
-                $(line.view).addClass("indent" + line.indent);
+                line.view.classList.add("indent" + line.indent);
             }
         } else {
             var lines = this.lines;
@@ -90,18 +89,18 @@ export default class ParsonsBlock {
                 sharedIndent = Math.min(sharedIndent, lines[i].indent);
             }
             if (sharedIndent < line.indent) {
-                $(line.view).addClass("indent" + (line.indent - sharedIndent));
+                line.view.classList.add(
+                    "indent" + (line.indent - sharedIndent),
+                );
             } else if (sharedIndent > line.indent) {
                 for (let i = 0; i < lines.length; i++) {
-                    $(lines[i].view).removeClass(
-                        "indent1 indent2 indent3 indent4 indent5 indent6",
-                    );
+                    lines[i].view.classList.remove(...INDENT_CLASSES);
                     // todo: if language is natural or math then don't do this
                     if (
                         this.problem.options.language !== "natural" &&
                         this.problem.options.language !== "math"
                     ) {
-                        $(lines[i].view).addClass(
+                        lines[i].view.classList.add(
                             "indent" + (lines[i].indent - line.indent),
                         );
                     }
@@ -109,17 +108,17 @@ export default class ParsonsBlock {
             }
         }
         this.lines.push(line);
-        $(this.view).children(".lines")[0].appendChild(line.view);
+        this.view.querySelector(".lines").appendChild(line.view);
     }
     // Add the contents of that block to myself and then delete that block
     consumeBlock(block) {
         for (let i = 0; i < block.lines.length; i++) {
             this.addLine(block.lines[i]);
         }
-        if ($(block.view).attr("tabindex") == "0") {
+        if (block.view.getAttribute("tabindex") == "0") {
             this.makeTabIndex();
         }
-        $(block.view).detach();
+        block.view.remove();
         var newBlocks = [];
         for (let i = 0; i < this.problem.blocks.length; i++) {
             if (this.problem.blocks[i] !== block) {
@@ -142,46 +141,41 @@ export default class ParsonsBlock {
         for (let i = 0; i < this.lines.length; i++) {
             var line = this.lines[i];
             if (line.indent > 0) {
-                $(line.view).removeClass(
-                    "indent1 indent2 indent3 indent4 indent5 indent6",
-                );
-                $(line.view).addClass("indent" + line.indent);
+                line.view.classList.remove(...INDENT_CLASSES);
+                line.view.classList.add("indent" + line.indent);
             }
         }
         // Update the block model / view
         this.indent = 0;
-        $(this.view).css({
-            "padding-left": "",
-            width: this.problem.areaWidth - 22,
-        });
+        this.view.style.paddingLeft = "";
+        this.view.style.width = this.problem.areaWidth - 22 + "px";
     }
     // Add a label to block and update its view
     addLabel(label, line) {
         var div = document.createElement("div");
-        $(div).addClass("block-label");
+        div.classList.add("block-label");
         if (this.problem.options.numbered == "right") {
-            $(div).addClass("right-label");
+            div.classList.add("right-label");
         }
         if (this.problem.options.numbered == "left") {
-            $(div).addClass("left-label");
+            div.classList.add("left-label");
         }
-        $(div).append(document.createTextNode(label));
-        $(this.view).children(".labels")[0].append(div);
+        div.append(document.createTextNode(label));
+        this.view.querySelector(".labels").append(div);
         if (this.labels.length != 0) {
-            $(div).css(
-                "margin-top",
+            div.style.marginTop =
                 (line - this.labels[this.labels.length - 1][1] - 1) *
-                    this.lines[line].view.offsetHeight,
-            );
+                    this.lines[line].view.offsetHeight +
+                "px";
         }
         this.labels.push([label, line]);
     }
     // Initialize Interactivity
     initializeInteractivity() {
-        if ($(this.view).hasClass("disabled")) {
+        if (this.view.classList.contains("disabled")) {
             return this;
         }
-        $(this.view).attr("tabindex", "-1");
+        this.view.setAttribute("tabindex", "-1");
         this.hammer = new Hammer.Manager(this.view, {
             recognizers: [
                 [
@@ -207,7 +201,7 @@ export default class ParsonsBlock {
     }
     // Return a boolean as to whether this block is able to be selected
     enabled() {
-        return $(this.view).attr("tabindex") !== undefined;
+        return this.view.getAttribute("tabindex") !== null;
     }
     // Return a boolean as to whether this block is a distractor
     isDistractor() {
@@ -350,24 +344,24 @@ export default class ParsonsBlock {
             this.problem.enterKeyboardMode();
             this.problem.textFocus = this;
             this.problem.textMove = false;
-            $(this.view).addClass("down");
+            this.view.classList.add("down");
         } else if (this.problem.textFocus == this) {
             if (this.problem.textMove) {
-                $(this.view).addClass("up");
+                this.view.classList.add("up");
             } else {
-                $(this.view).addClass("down");
+                this.view.classList.add("down");
             }
         } else {
             // already in keyboard mode
             this.problem.textFocus = this;
             this.problem.textMove = false;
-            $(this.view).addClass("down");
+            this.view.classList.add("down");
         }
         this.problem.textMoving = false;
     }
     // This block just lost textual focus
     releaseFocus() {
-        $(this.view).removeClass("down up");
+        this.view.classList.remove("down", "up");
         if (this.problem.textFocus == this) {
             if (!this.problem.textMoving) {
                 // exit out of problem but stay way into problem
@@ -382,37 +376,36 @@ export default class ParsonsBlock {
             }
         } else {
             // become selectable, but not active
-            $(this.view).attr("tabindex", "-1");
-            $(this.view).unbind("focus");
-            $(this.view).unbind("blur");
-            $(this.view).unbind("keydown");
+            this.view.setAttribute("tabindex", "-1");
+            this.view.removeEventListener("focus", this.focusHandler);
+            this.view.removeEventListener("blur", this.blurHandler);
+            this.view.removeEventListener("keydown", this.keydownHandler);
         }
     }
     // Make this block into the keyboard entry point
     makeTabIndex() {
-        $(this.view).attr("tabindex", "0");
-        var that = this;
-        $(this.view).focus(function () {
-            that.newFocus();
-        });
-        $(this.view).blur(function () {
-            that.releaseFocus();
-        });
-        $(this.view).keydown(function (event) {
-            that.keyDown(event);
-        });
+        this.view.setAttribute("tabindex", "0");
+        if (this.focusHandler === undefined) {
+            this.focusHandler = () => this.newFocus();
+            this.blurHandler = () => this.releaseFocus();
+            this.keydownHandler = (event) => this.keyDown(event);
+        }
+        // re-adding an identical listener is a no-op, so repeated calls are safe
+        this.view.addEventListener("focus", this.focusHandler);
+        this.view.addEventListener("blur", this.blurHandler);
+        this.view.addEventListener("keydown", this.keydownHandler);
     }
     // Called to disable interaction for the future
     disable() {
         if (this.hammer !== undefined) {
             this.hammer.set({ enable: false });
         }
-        if ($(this.view).attr("tabindex") == "0") {
+        if (this.view.getAttribute("tabindex") == "0") {
             this.releaseFocus();
-            $(this.view).removeAttr("tabindex");
+            this.view.removeAttribute("tabindex");
             this.problem.initializeTabIndex();
         } else {
-            $(this.view).removeAttr("tabindex");
+            this.view.removeAttribute("tabindex");
         }
     }
     // Enable functionality after reset button has been pressed
@@ -420,10 +413,10 @@ export default class ParsonsBlock {
         if (this.hammer !== undefined) {
             this.hammer.set({ enable: true });
         }
-        if (!$(this.view)[0].hasAttribute("tabindex")) {
-            $(this.view).attr("tabindex", "-1");
+        if (!this.view.hasAttribute("tabindex")) {
+            this.view.setAttribute("tabindex", "-1");
         }
-        $(this.view).css({ opacity: "" });
+        this.view.style.opacity = "";
     }
     // Called to destroy interaction for the future
     destroy() {
@@ -431,10 +424,10 @@ export default class ParsonsBlock {
             this.hammer.destroy();
             delete this.hammer;
         }
-        if ($(this.view).attr("tabindex") == "0") {
+        if (this.view.getAttribute("tabindex") == "0") {
             this.releaseFocus();
         }
-        $(this.view).removeAttr("tabindex");
+        this.view.removeAttribute("tabindex");
     }
     // Called when a block is picked up
     panStart(event) {
@@ -534,7 +527,7 @@ export default class ParsonsBlock {
                     if (index == blocks.length) {
                         this.problem.textMoving = true;
                         this.problem.sourceArea.appendChild(this.view);
-                        $(this.view).focus();
+                        this.view.focus();
                         this.problem.state = undefined;
                         this.problem.updateView();
                         return this;
@@ -547,7 +540,7 @@ export default class ParsonsBlock {
                 }
                 this.problem.textMoving = true;
                 this.problem.sourceArea.insertBefore(this.view, block.view);
-                $(this.view).focus();
+                this.view.focus();
             } else {
                 // reduce indent
                 this.indent = this.indent - 1;
@@ -573,7 +566,7 @@ export default class ParsonsBlock {
                             this.view,
                             block.view,
                         );
-                        $(this.view).focus();
+                        this.view.focus();
                         this.problem.state = undefined;
                         this.problem.updateView();
                         return this;
@@ -592,7 +585,7 @@ export default class ParsonsBlock {
                         this.view,
                         blocks[i - 1].view,
                     );
-                    $(this.view).focus();
+                    this.view.focus();
                     this.problem.state = undefined;
                     this.problem.updateView();
                 }
@@ -612,7 +605,7 @@ export default class ParsonsBlock {
                 if (itemOffset >= offset) {
                     this.problem.textMoving = true;
                     this.problem.answerArea.insertBefore(this.view, item.view);
-                    $(this.view).focus();
+                    this.view.focus();
                     this.problem.state = undefined;
                     this.problem.updateView();
                     return this;
@@ -620,7 +613,7 @@ export default class ParsonsBlock {
             }
             this.problem.textMoving = true;
             this.problem.answerArea.appendChild(this.view);
-            $(this.view).focus();
+            this.view.focus();
             this.problem.state = undefined;
             this.problem.updateView();
         } else {
@@ -647,7 +640,7 @@ export default class ParsonsBlock {
                 if (index == blocks.length) {
                     this.problem.textMoving = true;
                     this.problem.sourceArea.appendChild(this.view);
-                    $(this.view).focus();
+                    this.view.focus();
                     this.problem.state = undefined;
                     this.problem.updateView();
                     return this;
@@ -657,7 +650,7 @@ export default class ParsonsBlock {
                         this.view,
                         blocks[index].view,
                     );
-                    $(this.view).focus();
+                    this.view.focus();
                     this.problem.state = undefined;
                     this.problem.updateView();
                     return this;
@@ -679,7 +672,7 @@ export default class ParsonsBlock {
                             blocks[i + 2].view,
                         );
                     }
-                    $(this.view).focus();
+                    this.view.focus();
                     this.problem.state = undefined;
                     this.problem.updateView();
                 }
@@ -706,7 +699,7 @@ export default class ParsonsBlock {
             }
             this.problem.textFocus = chooseNext;
             chooseNext.makeTabIndex();
-            $(chooseNext.view).focus();
+            chooseNext.view.focus();
         }
     }
     // Move selection up
@@ -723,7 +716,7 @@ export default class ParsonsBlock {
             if (chooseNext) {
                 this.problem.textFocus = item;
                 item.makeTabIndex();
-                $(item.view).focus();
+                item.view.focus();
                 return this;
             } else {
                 if (item.view.id == this.view.id) {
@@ -752,7 +745,7 @@ export default class ParsonsBlock {
             }
             this.problem.textFocus = chooseNext;
             chooseNext.makeTabIndex();
-            $(chooseNext.view).focus();
+            chooseNext.view.focus();
         }
     }
     // Move selection down
@@ -769,7 +762,7 @@ export default class ParsonsBlock {
             if (chooseNext) {
                 this.problem.textFocus = item;
                 item.makeTabIndex();
-                $(item.view).focus();
+                item.view.focus();
                 return this;
             } else {
                 if (item.view.id == this.view.id) {
@@ -781,16 +774,16 @@ export default class ParsonsBlock {
     // Toggle whether to move this block
     toggleMove() {
         if (this.problem.textMove) {
-            $(this.view).removeClass("up");
-            $(this.view).addClass("down");
+            this.view.classList.remove("up");
+            this.view.classList.add("down");
             this.problem.textMove = false;
             this.problem.logMove("kmove");
             if (!this.isPlaceholder) {
                 this.problem.updatePlaceholders();
             }
         } else {
-            $(this.view).removeClass("down");
-            $(this.view).addClass("up");
+            this.view.classList.remove("down");
+            this.view.classList.add("up");
             this.problem.textMove = true;
         }
     }
diff --git a/bases/rsptx/interactives/runestone/parsons/js/parsonsLine.js b/bases/rsptx/interactives/runestone/parsons/js/parsonsLine.js
index e7f56633c..5bc9f8e73 100644
--- a/bases/rsptx/interactives/runestone/parsons/js/parsonsLine.js
+++ b/bases/rsptx/interactives/runestone/parsons/js/parsonsLine.js
@@ -17,6 +17,8 @@
 ===================================================================== */
 // Initialize from codestring
 
+import { outerWidth } from "../../common/js/domutil.js";
+
 export default class ParsonsLine {
     constructor(problem, codestring, displaymath) {
         this.problem = problem;
@@ -88,7 +90,11 @@ export default class ParsonsLine {
             }
         } else {
             view = document.createElement("code");
-            $(view).addClass(problem.options.prettifyLanguage);
+            if (problem.options.prettifyLanguage) {
+                view.classList.add(
+                    ...problem.options.prettifyLanguage.split(" "),
+                );
+            }
         }
         view.id = problem.counterId + "-line-" + this.index;
         view.innerHTML += this.text;
@@ -100,7 +106,7 @@ export default class ParsonsLine {
         // this.width does not appear to be used anywhere later
         // since changing the value of this.width appears to have no effect. - Vincent Qiu (September 2020)
         this.width =
-            $(this.view).outerWidth(true) -
+            outerWidth(this.view) -
             this.problem.options.pixelsPerIndent * this.indent;
 
         // Pass this information on to be used in class Parsons function initializeAreas
diff --git a/bases/rsptx/interactives/runestone/parsons/js/placeholderBlock.js b/bases/rsptx/interactives/runestone/parsons/js/placeholderBlock.js
index 88caddb8d..c7511f034 100644
--- a/bases/rsptx/interactives/runestone/parsons/js/placeholderBlock.js
+++ b/bases/rsptx/interactives/runestone/parsons/js/placeholderBlock.js
@@ -17,19 +17,18 @@ export default class PlaceholderBlock extends ParsonsBlock {
         this.afterSettledBlockCount = afterSettledBlockCount;
 
         // create a normal parsons block, but use css to control visibility of normal content
-        $(this.view).addClass("placeholder-block");
-        $(this.view).addClass("disabled");
+        this.view.classList.add("placeholder-block", "disabled");
 
         // create a new div displaying how many blocks are missing
         this.placeholderSize = placeholderSize;
         var content = document.createElement("div");
-        $(content).addClass("placeholder-text");
+        content.classList.add("placeholder-text");
         if (placeholderSize > 1) {
             content.innerText = `${placeholderSize} blocks are missing here`;
         } else {
             content.innerText = `${placeholderSize} block is missing here`;
         }
-        $(this.view).append(content);
+        this.view.append(content);
 
         this.isPlaceholder = true;
     }
diff --git a/bases/rsptx/interactives/runestone/parsons/js/settledBlock.js b/bases/rsptx/interactives/runestone/parsons/js/settledBlock.js
index 6e3181a35..c9747d963 100644
--- a/bases/rsptx/interactives/runestone/parsons/js/settledBlock.js
+++ b/bases/rsptx/interactives/runestone/parsons/js/settledBlock.js
@@ -10,13 +10,12 @@ export default class SettledBlock extends ParsonsBlock {
         //update its indent to follow the indentation of the lines inside
 
         // add a css class
-        $(this.view).addClass("settled-block");
-        $(this.view).addClass("disabled");
+        this.view.classList.add("settled-block", "disabled");
 
-        let tooltipSpan = document.createElement("span");
-        tooltipSpan.classList.add("settled-tooltip");
-        $(this.view).append(tooltipSpan);
-        $(this.view).find(".settled-tooltip").text("OK");
+        this.tooltipSpan = document.createElement("span");
+        this.tooltipSpan.classList.add("settled-tooltip");
+        this.view.append(this.tooltipSpan);
+        this.tooltipSpan.textContent = "OK";
     }
 
     setBlocksBefore(num) {
@@ -24,15 +23,9 @@ export default class SettledBlock extends ParsonsBlock {
         var beforePlural = num > 1 ? "s" : "";
         if (this.blocksAfter) {
             var afterPlural = this.blocksAfter > 1 ? "s" : "";
-            $(this.view)
-                .find(".settled-tooltip")
-                .html(
-                    `${num} block${beforePlural} ⬆️️), ${this.blocksAfter} block${afterPlural} ⬇️`,
-                );
+            this.tooltipSpan.innerHTML = `${num} block${beforePlural} ⬆️️), ${this.blocksAfter} block${afterPlural} ⬇️`;
         } else {
-            $(this.view)
-                .find(".settled-tooltip")
-                .html(`${num} block${beforePlural} ⬆️️`);
+            this.tooltipSpan.innerHTML = `${num} block${beforePlural} ⬆️️`;
         }
     }
 
@@ -41,15 +34,9 @@ export default class SettledBlock extends ParsonsBlock {
         var afterPlural = num > 1 ? "s" : "";
         if (this.blocksBefore) {
             var beforePlural = this.blocksBefore > 1 ? "s" : "";
-            $(this.view)
-                .find(".settled-tooltip")
-                .html(
-                    `${this.blocksBefore} block${beforePlural} ⬆️, ${num} block${afterPlural} ⬇️`,
-                );
+            this.tooltipSpan.innerHTML = `${this.blocksBefore} block${beforePlural} ⬆️, ${num} block${afterPlural} ⬇️`;
         } else {
-            $(this.view)
-                .find(".settled-tooltip")
-                .html(`${num} block${afterPlural} ️️⬇️`);
+            this.tooltipSpan.innerHTML = `${num} block${afterPlural} ️️⬇️`;
         }
     }
 }
diff --git a/bases/rsptx/interactives/runestone/parsons/js/timedparsons.js b/bases/rsptx/interactives/runestone/parsons/js/timedparsons.js
index 2cecedebb..8de9290f5 100644
--- a/bases/rsptx/interactives/runestone/parsons/js/timedparsons.js
+++ b/bases/rsptx/interactives/runestone/parsons/js/timedparsons.js
@@ -11,15 +11,17 @@ export default class TimedParsons extends Parsons {
         }
         this.grader.showfeedback = this.showfeedback;
         this.hideFeedback();
-        $(this.checkButton).hide();
-        $(this.helpButton).hide();
-        $(this.resetButton).hide();
+        this.checkButton.style.display = "none";
+        if (this.helpButton) {
+            this.helpButton.style.display = "none";
+        }
+        this.resetButton.style.display = "none";
     }
     checkCorrectTimed() {
         return this.correct ? "T" : "F";
     }
     hideFeedback() {
-        $(this.messageDiv).hide();
+        this.messageDiv.style.display = "none";
     }
 }
 
diff --git a/bases/rsptx/interactives/runestone/parsons/test/parsons.test.js b/bases/rsptx/interactives/runestone/parsons/test/parsons.test.js
new file mode 100644
index 000000000..68b42e4b5
--- /dev/null
+++ b/bases/rsptx/interactives/runestone/parsons/test/parsons.test.js
@@ -0,0 +1,734 @@
+// Characterization tests for the Parsons component. These were first written
+// against the jQuery implementation and now guard the jQuery-free version.
+// Note: deliberately NO jquery-globals import here -- parsons must work
+// without jQuery.
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import Parsons from "../js/parsons.js";
+import "../js/timedparsons.js";
+
+// Build the same DOM a book page provides (see parsonsPreview.tsx and the
+// Sphinx/PreTeXt templates): div.runestone > [data-component=parsons] with a
+// question div and a pre.parsonsblocks holding the block source text.
+function makeFixture({
+    id = "test_parsons_1",
+    blocks,
+    attrs = "",
+    question = "

Arrange the code.

", +} = {}) { + document.body.innerHTML = ` +
+
+
${question}
+
+${blocks}
+          
+
+
`; + return document.getElementById(id); +} + +// data-order pins block order so tests are deterministic (no shuffling). +const FLAT_BLOCKS = "line_a = 1\n---\nline_b = 2\n---\nline_c = 3"; +const FLAT_ATTRS = 'data-language="python" data-order="0,1,2"'; + +const INDENTED_BLOCKS = + 'def main():\n---\n print("Hello")\n---\n print("Bye") #distractor: not needed'; +const INDENTED_ATTRS = 'data-language="python" data-order="0,1,2"'; + +const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function makeParsons(fixtureOpts = {}, extraOpts = {}) { + const orig = makeFixture(fixtureOpts); + const parsons = new Parsons({ + orig: orig, + useRunestoneServices: false, + ...extraOpts, + }); + await parsons.component_ready_promise; + await tick(); + return parsons; +} + +// Place source blocks (by their current source-area position) into the answer +// area the way a drag would: answerBlocks()/sourceBlocks() read the DOM. +function answer(parsons, sourceIndexes) { + const blocks = parsons.sourceBlocks(); + for (const i of sourceIndexes) { + parsons.answerArea.appendChild(blocks[i].view); + } +} + +beforeEach(() => { + document.body.innerHTML = ""; + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); + vi.stubGlobal("alert", vi.fn()); +}); + +describe("construction", () => { + it("replaces the pre with a rendered view holding source/answer areas", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect(p.divid).toBe("test_parsons_1"); + expect(p.outerDiv.classList.contains("parsons")).toBe(true); + // The original pre.parsonsblocks is gone from the document. + expect(document.querySelector("pre.parsonsblocks")).toBeNull(); + // Areas exist in the document with derived ids. + expect(document.getElementById(`${p.counterId}-source`)).toBe( + p.sourceArea, + ); + expect(document.getElementById(`${p.counterId}-answer`)).toBe( + p.answerArea, + ); + expect(p.sourceArea.getAttribute("aria-describedby")).toBe( + `${p.counterId}-sourceTip`, + ); + }); + + it("creates Check and Reset buttons; Help only when adaptive", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect(p.checkButton.textContent).toBe("Check"); + expect(p.checkButton.type).toBe("button"); + expect(p.resetButton.textContent).toBe("Reset"); + expect(p.helpButton).toBeUndefined(); + + const adaptive = await makeParsons({ + id: "test_parsons_adaptive", + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-adaptive="true"', + }); + expect(adaptive.helpButton.textContent).toBe("Help me"); + }); + + it("renders drag labels and a hidden keyboard tip", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect(p.sourceLabel.textContent).toBe("Drag from here"); + expect(p.answerLabel.textContent).toBe("Drop blocks here"); + expect(p.keyboardTip.textContent).toContain("Arrow keys to navigate"); + expect(p.keyboardTip.style.display).toBe("none"); + }); + + it("moves the question above the problem; drops a blank question", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect(p.outerDiv.firstElementChild).toBe(p.question); + expect(p.question.textContent).toContain("Arrange the code."); + + const blank = await makeParsons({ + id: "test_parsons_blank_q", + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS, + question: " ", + }); + expect(document.contains(blank.question)).toBe(false); + }); + + it("starts with all blocks in the source area and none answered", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect(p.blocks).toHaveLength(3); + expect(p.sourceBlocks()).toHaveLength(3); + expect(p.answerBlocks()).toHaveLength(0); + for (const block of p.sourceBlocks()) { + expect(block.view.classList.contains("block")).toBe(true); + } + }); + + it("marks the component ready and adds a caption", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect( + p.containerDiv.classList.contains("runestone-component-ready"), + ).toBe(true); + const cap = p.containerDiv.querySelector("p.runestone_caption"); + expect(cap.textContent).toContain("Parsons"); + }); + + it("makes exactly one block the keyboard entry point", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + const tabZero = p.blocks.filter( + (b) => b.view.getAttribute("tabindex") === "0", + ); + expect(tabZero).toHaveLength(1); + for (const block of p.blocks) { + expect(block.enabled()).toBe(true); + } + }); +}); + +describe("option parsing", () => { + it("defaults to python and its prettify class", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: 'data-order="0,1,2"', + }); + expect(p.options.language).toBe("python"); + expect(p.options.prettifyLanguage).toBe("prettyprint lang-py"); + expect(p.lines[0].view.tagName).toBe("CODE"); + expect(p.lines[0].view.classList.contains("lang-py")).toBe(true); + }); + + it("maps other languages; unknown language gets no prettify class", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: 'data-language="java" data-order="0,1,2"', + }); + expect(p.options.prettifyLanguage).toBe("prettyprint lang-java"); + + const q = await makeParsons({ + id: "test_parsons_weird_lang", + blocks: FLAT_BLOCKS, + attrs: 'data-language="brainfudge" data-order="0,1,2"', + }); + expect(q.options.prettifyLanguage).toBe(""); + }); + + it("parses order, maxdist, noindent and numbered attributes", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: 'data-language="python" data-order="2,0,1" data-maxdist="1" data-noindent="true" data-numbered="left"', + }); + expect(p.options.order).toEqual([2, 0, 1]); + expect(p.options.maxdist).toBe(1); + expect(p.noindent).toBe(true); + expect(p.options.numbered).toBe("left"); + // Source blocks follow data-order. + const texts = p.sourceBlocks().map((b) => b.lines[0].text); + expect(texts).toEqual(["line_c = 3", "line_a = 1", "line_b = 2"]); + }); + + it("numbered problems label each block", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-numbered="left"', + }); + const labels = p + .sourceBlocks() + .map((b) => b.view.querySelector(".block-label").textContent); + expect(labels).toEqual(["1 ", "2 ", "3 "]); + }); + + it("parses data-explanations JSON and tolerates bad JSON", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-explanations=\'{"0": "first line"}\'', + }); + expect(p.blockExplanations).toEqual({ 0: "first line" }); + + const q = await makeParsons({ + id: "test_parsons_bad_json", + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + " data-explanations='{not json'", + }); + expect(q.blockExplanations).toEqual({}); + }); +}); + +describe("line initialization", () => { + it("normalizes indents to levels and keeps line text", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + expect(p.lines.map((l) => l.text)).toEqual([ + "def main():", + 'print("Hello")', + 'print("Bye")', + ]); + expect(p.lines.map((l) => l.indent)).toEqual([0, 1, 1]); + }); + + it("flags distractors and stores their help text", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + expect(p.lines[2].distractor).toBe(true); + expect(p.lines[2].paired).toBe(false); + expect(p.lines[2].distractHelptext).toBe("not needed"); + // Distractors are not part of the solution. + expect(p.solution).toEqual([p.lines[0], p.lines[1]]); + }); + + it("marks paired distractors", async () => { + const p = await makeParsons({ + blocks: "a = 1\n---\na = 2 #paired: close but no", + attrs: FLAT_ATTRS, + }); + expect(p.lines[1].distractor).toBe(true); + expect(p.lines[1].paired).toBe(true); + expect(p.lines[1].distractHelptext).toBe("close but no"); + }); + + it("groups multi-line segments into a single block", async () => { + const p = await makeParsons({ + blocks: "a = 1\nb = 2\n---\nc = 3", + attrs: FLAT_ATTRS, + }); + expect(p.lines[0].groupWithNext).toBe(true); + expect(p.lines[1].groupWithNext).toBe(false); + expect(p.blocks).toHaveLength(2); + expect(p.blocks[0].lines).toHaveLength(2); + }); +}); + +describe("hashing", () => { + it("hashes empty areas as '-' and blocks as lineIndexes_indent", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + expect(p.answerHash()).toBe("-"); + expect(p.sourceHash()).toBe("0_0-1_0-2_0"); + answer(p, [0, 1]); + p.answerBlocks()[1].indent = 1; + expect(p.answerHash()).toBe("0_0-1_1"); + expect(p.sourceHash()).toBe("2_0"); + }); + + it("round-trips blocks through blocksFromHash", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS, + }); + const blocks = p.blocksFromHash("0_1_0-2_0"); + expect(blocks).toHaveLength(2); + expect(blocks[0].lines.map((l) => l.index)).toEqual([0, 1]); + expect(blocks[0].hash()).toBe("0_1_0"); + expect(blocks[1].hash()).toBe("2_0"); + }); + + it("decodes adaptive state hashes", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + expect(p.optionsFromHash("d2-i-c3-s")).toEqual({ + disabled: [2], + noindent: true, + checkCount: 3, + hasSolved: true, + }); + expect(p.optionsFromHash("-")).toEqual({}); + }); + + it("encodes adaptive state for adaptive problems", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-adaptive="true"', + }); + expect(p.adaptiveHash()).toBe("c0"); + expect(p.adaptiveHash()).not.toContain("s"); + }); +}); + +describe("grading", () => { + it("reports incorrectTooShort when blocks are missing", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [0]); + p.checkCurrentAnswer(); + expect(p.grade).toBe("incorrectTooShort"); + expect(p.hasSolved).toBe(false); + expect(p.checkCount).toBe(1); + }); + + it("reports incorrectMoveBlocks for wrong order", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [1, 0, 2]); + p.checkCurrentAnswer(); + expect(p.grade).toBe("incorrectMoveBlocks"); + expect(p.percent).toBeCloseTo(0.2 + 0.4 / 3 + 0.4 / 3, 5); + }); + + it("reports correct, locks the check button and records the solve", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [0, 1, 2]); + p.checkCurrentAnswer(); + expect(p.grade).toBe("correct"); + expect(p.hasSolved).toBe(true); + expect(p.correct).toBe(true); + expect(p.percent).toBe(1); + expect(p.checkButton.disabled).toBe(true); + expect(localStorage.getItem(p.storageId + "Solved")).toBe("true"); + }); + + it("reports incorrectIndent when order is right but indent wrong", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + answer(p, [0, 1]); // print("Hello") left at indent 0 + p.checkCurrentAnswer(); + expect(p.grade).toBe("incorrectIndent"); + + p.answerBlocks()[1].indent = 1; + p.checkCurrentAnswer(); + expect(p.grade).toBe("correct"); + }); + + it("counts distinct attempts, not repeats (adaptive only)", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-adaptive="true"', + }); + answer(p, [1, 0, 2]); + p.checkCurrentAnswer(); + p.checkCurrentAnswer(); + expect(p.numDistinct).toBe(1); + expect(p.checkCount).toBe(2); + }); +}); + +describe("feedback rendering", () => { + it("shows the success message and marks the answer area", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [0, 1, 2]); + p.checkCurrentAnswer(); + p.renderFeedback(); + expect(p.answerArea.classList.contains("correct")).toBe(true); + expect(p.messageDiv.style.visibility).toBe("visible"); + expect(p.messageDiv.getAttribute("class")).toBe("alert alert-info"); + await tick(30); + expect(p.messageDiv.textContent).toContain("Perfect!"); + }); + + it("highlights out-of-place blocks on wrong order", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [1, 0, 2]); + p.checkCurrentAnswer(); + p.renderFeedback(); + expect(p.answerArea.classList.contains("incorrect")).toBe(true); + expect(p.messageDiv.getAttribute("class")).toBe("alert alert-danger"); + const flagged = p + .answerBlocks() + .filter((b) => b.view.classList.contains("incorrectPosition")); + expect(flagged.length).toBeGreaterThan(0); + await tick(30); + expect(p.messageDiv.textContent).toContain( + "wrong or are in the wrong order", + ); + }); + + it("marks misindented blocks", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + answer(p, [0, 1]); + p.checkCurrentAnswer(); + p.renderFeedback(); + const hello = p.answerBlocks()[1]; + expect(hello.view.classList.contains("indentRight")).toBe(true); + await tick(30); + expect(p.messageDiv.textContent).toContain("not indented correctly"); + }); + + it("clearFeedback removes classes and hides the message", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [1, 0, 2]); + p.checkCurrentAnswer(); + p.renderFeedback(); + p.clearFeedback(); + expect(p.answerArea.classList.contains("incorrect")).toBe(false); + expect(p.messageDiv.style.visibility).toBe("hidden"); + for (const b of p.answerBlocks()) { + expect(b.view.classList.contains("incorrectPosition")).toBe(false); + } + }); + + it("adds explanation tooltips to answer blocks after solving", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-explanations=\'{"0": "first!"}\'', + }); + answer(p, [0, 1, 2]); + p.checkCurrentAnswer(); + p.renderFeedback(); + const first = p.answerBlocks()[0]; + expect(first.view.getAttribute("title")).toBe("first!"); + expect(first.view.classList.contains("has-explanation")).toBe(true); + p.clearFeedback(); + expect(first.view.getAttribute("title")).toBeNull(); + expect(first.view.classList.contains("has-explanation")).toBe(false); + }); +}); + +describe("reset", () => { + it("returns all blocks to the source area and clears state", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [0, 1, 2]); + p.checkCurrentAnswer(); + expect(p.checkButton.disabled).toBe(true); + p.resetButton.click(); + await tick(); + expect(p.answerBlocks()).toHaveLength(0); + expect(p.sourceBlocks()).toHaveLength(3); + expect(p.checkCount).toBe(0); + expect(p.hasSolved).toBe(false); + expect(p.checkButton.disabled).toBe(false); + expect(p.messageDiv.style.visibility).toBe("hidden"); + }); +}); + +describe("persistence", () => { + it("stores source/answer hashes in localStorage", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + answer(p, [0, 1]); + p.answerBlocks()[1].indent = 1; + p.setLocalStorage(); + const stored = JSON.parse(localStorage.getItem(p.storageId)); + expect(stored.source).toBe("2_0"); + expect(stored.answer).toBe("0_0-1_1"); + expect(stored.timestamp).toBeTruthy(); + }); + + it("restores a saved answer on construction and grades it", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + answer(p, [0, 1]); + p.answerBlocks()[1].indent = 1; + p.setLocalStorage(); + + const restored = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + expect(restored.answerBlocks()).toHaveLength(2); + expect(restored.answerHash()).toBe("0_0-1_1"); + expect(restored.sourceHash()).toBe("2_0"); + expect(restored.correct).toBe(true); + }); +}); + +describe("keyboard movement model", () => { + it("moveRight moves a source block into the answer area", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + const block = p.sourceBlocks()[0]; + block.moveRight(); + expect(block.inSourceArea()).toBe(false); + expect(p.answerBlocks()).toContain(block); + }); + + it("moveRight in the answer area increases indent up to the limit", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + expect(p.indent).toBe(1); + const block = p.sourceBlocks()[1]; + block.moveRight(); // into answer, indent reset to 0 + expect(block.indent).toBe(0); + block.moveRight(); + expect(block.indent).toBe(1); + block.moveRight(); // at the limit, stays + expect(block.indent).toBe(1); + }); + + it("moveLeft reduces indent then returns the block to the source", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS, + }); + const block = p.sourceBlocks()[1]; + block.moveRight(); + block.moveRight(); + block.moveLeft(); + expect(block.indent).toBe(0); + expect(block.inSourceArea()).toBe(false); + block.moveLeft(); + expect(block.inSourceArea()).toBe(true); + }); + + it("moveUp and moveDown reorder blocks in the answer area", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + answer(p, [0, 1, 2]); + const [a, b, c] = p.answerBlocks(); + b.moveDown(); + expect(p.answerBlocks()).toEqual([a, c, b]); + b.moveUp(); + expect(p.answerBlocks()).toEqual([a, b, c]); + a.moveUp(); // already first: no-op + expect(p.answerBlocks()).toEqual([a, b, c]); + }); + + it("focusing a block enters keyboard mode; blur leaves it", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + const block = p.blocks.find( + (b) => b.view.getAttribute("tabindex") === "0", + ); + block.view.focus(); + expect(p.textFocus).toBe(block); + expect(block.view.classList.contains("down")).toBe(true); + expect(p.keyboardTip.style.display).not.toBe("none"); + expect(p.sourceLabel.style.display).toBe("none"); + block.view.blur(); + expect(p.textFocus).toBeUndefined(); + expect(p.keyboardTip.style.display).toBe("none"); + expect(p.sourceLabel.style.display).not.toBe("none"); + }); + + it("space toggles a focused block between select and move", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + const block = p.blocks.find( + (b) => b.view.getAttribute("tabindex") === "0", + ); + block.view.focus(); + block.keyDown({ keyCode: 32, preventDefault() {} }); + expect(p.textMove).toBe(true); + expect(block.view.classList.contains("up")).toBe(true); + block.keyDown({ keyCode: 32, preventDefault() {} }); + expect(p.textMove).toBe(false); + expect(block.view.classList.contains("down")).toBe(true); + }); + + it("disable() removes a block from keyboard rotation", async () => { + const p = await makeParsons({ blocks: FLAT_BLOCKS, attrs: FLAT_ATTRS }); + const block = p.sourceBlocks()[0]; + block.disable(); + expect(block.enabled()).toBe(false); + // Another block takes over as the keyboard entry point. + const tabZero = p.blocks.filter( + (b) => b.view.getAttribute("tabindex") === "0", + ); + expect(tabZero).toHaveLength(1); + expect(tabZero[0]).not.toBe(block); + }); +}); + +describe("adaptive problems", () => { + it("initializes adaptive bookkeeping in localStorage", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-adaptive="true"', + }); + expect(p.canHelp).toBe(true); + expect(localStorage.getItem(p.adaptiveId + "Problem")).toBe(p.divid); + expect(localStorage.getItem(p.adaptiveId + "recentAttempts")).toBe("3"); + expect(localStorage.getItem(p.adaptiveId + "Solved")).toBe("false"); + }); + + it("helpMe demands three distinct attempts first", async () => { + const p = await makeParsons({ + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS + ' data-adaptive="true"', + }); + p.helpMe(); + expect(alert).toHaveBeenCalledWith( + expect.stringContaining("three distinct full attempts"), + ); + expect(p.gotHelp).toBe(false); + }); + + it("removeDistractor disables the block and reveals its help text", async () => { + const p = await makeParsons({ + blocks: INDENTED_BLOCKS, + attrs: INDENTED_ATTRS + ' data-adaptive="true"', + }); + const distractor = p.sourceBlocks().find((b) => b.isDistractor()); + p.removeDistractor(distractor); + expect(distractor.enabled()).toBe(false); + expect(distractor.view.getAttribute("title")).toBe("not needed"); + await tick(30); + expect(p.messageDiv.textContent).toContain( + "Disabled an unneeded code block", + ); + }); +}); + +describe("paired distractor presentation", () => { + it("builds paired bins and an 'or' bracket div", async () => { + const p = await makeParsons({ + blocks: "a = 1\n---\na = 2 #paired\n---\nb = 3", + attrs: FLAT_ATTRS, + }); + expect(p.pairedBins).toEqual([[0, 1]]); + expect(p.pairedDivs).toHaveLength(1); + expect(p.sourceArea.querySelector(".paired")).toBe(p.pairedDivs[0]); + // Both paired lines land in the same bin, so their blocks report it. + const blocks = p.sourceBlocks(); + const bins = blocks.map((b) => b.pairedBin()); + expect(bins.filter((b) => b === 0)).toHaveLength(2); + expect(bins.filter((b) => b === -1)).toHaveLength(1); + }); +}); + +describe("scaffolding (CodeTailor) problems", () => { + it("pre-places settled blocks and placeholders in the answer area", async () => { + const p = await makeParsons({ + blocks: 'def main(): #settled\n---\n print("Hello")\n---\n print("Bye")', + attrs: 'data-language="python" data-order="0,1" data-scaffolding="true"', + }); + const answerBlocks = p.answerBlocks(); + expect(answerBlocks.some((b) => b.isSettled)).toBe(true); + expect(answerBlocks.some((b) => b.isPlaceholder)).toBe(true); + const placeholder = answerBlocks.find((b) => b.isPlaceholder); + // (the count message is set via innerText, which jsdom doesn't render, + // so assert the message container rather than its text) + expect(placeholder.placeholderSize).toBe(2); + expect( + placeholder.view.querySelector(".placeholder-text"), + ).toBeTruthy(); + expect(placeholder.view.classList.contains("placeholder-block")).toBe( + true, + ); + const settled = answerBlocks.find((b) => b.isSettled); + expect(settled.view.classList.contains("settled-block")).toBe(true); + expect(settled.view.classList.contains("disabled")).toBe(true); + expect( + settled.view.querySelector(".settled-tooltip").textContent, + ).toContain("2 block"); + // Settled/placeholder lines stay out of the draggable source blocks. + expect(p.sourceBlocks()).toHaveLength(2); + }); +}); + +describe("dag grading", () => { + it("accepts any topological order of the dependency graph", async () => { + const p = await makeParsons({ + blocks: "a = 1 #tag:1;depends:;\n---\nb = 2 #tag:2;depends:;\n---\nc = a + b #tag:3;depends:1,2;", + attrs: 'data-language="python" data-order="0,1,2" data-grader="dag"', + }); + expect(p.lines[0].tag).toBe("1"); + expect(p.lines[2].depends).toEqual(["1", "2"]); + answer(p, [1, 0, 2]); // b before a: still a valid topological order + p.checkCurrentAnswer(); + expect(p.grade).toBe("correct"); + }); +}); + +describe("timed parsons", () => { + it("component factory returns a feedback-suppressed timed variant", async () => { + const orig = makeFixture({ + id: "test_parsons_timed", + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS, + }); + const p = window.component_factory["parsons"]({ + orig: orig, + timed: true, + useRunestoneServices: false, + }); + await p.component_ready_promise; + await tick(); + expect(p.showfeedback).toBe(false); + expect(p.checkButton.style.display).toBe("none"); + expect(p.resetButton.style.display).toBe("none"); + expect(p.messageDiv.style.display).toBe("none"); + answer(p, [0, 1, 2]); + p.checkCurrentAnswer(); + expect(p.checkCorrectTimed()).toBe("T"); + }); +}); + +describe("page integration", () => { + it("renders every [data-component=parsons] on login-complete", async () => { + makeFixture({ + id: "test_parsons_page", + blocks: FLAT_BLOCKS, + attrs: FLAT_ATTRS, + }); + document.dispatchEvent(new Event("runestone:login-complete")); + const p = window.componentMap["test_parsons_page"]; + expect(p).toBeInstanceOf(Parsons); + await p.component_ready_promise; + }); +}); diff --git a/bases/rsptx/interactives/runestone/selectquestion/js/selectone.js b/bases/rsptx/interactives/runestone/selectquestion/js/selectone.js index b6cd8a66e..1197e3104 100644 --- a/bases/rsptx/interactives/runestone/selectquestion/js/selectone.js +++ b/bases/rsptx/interactives/runestone/selectquestion/js/selectone.js @@ -8,6 +8,7 @@ import { createTimedComponent, } from "../../common/js/renderComponent.js"; import RunestoneBase from "../../common/js/runestonebase.js"; +import { getDataValue } from "../../common/js/domutil.js"; import "../css/selectquestion.css"; export default class SelectOne extends RunestoneBase { @@ -29,19 +30,19 @@ export default class SelectOne extends RunestoneBase { constructor(opts) { super(opts); this.origOpts = opts; - this.questions = $(opts.orig).data("questionlist"); - this.proficiency = $(opts.orig).data("proficiency"); - this.minDifficulty = $(opts.orig).data("minDifficulty"); - this.maxDifficulty = $(opts.orig).data("maxDifficulty"); - this.points = $(opts.orig).data("points"); - this.autogradable = $(opts.orig).data("autogradable"); - this.not_seen_ever = $(opts.orig).data("not_seen_ever"); - this.selector_id = $(opts.orig).first().attr("id"); - this.primaryOnly = $(opts.orig).data("primary"); - this.ABExperiment = $(opts.orig).data("ab"); - this.toggleOptions = $(opts.orig).data("toggleoptions"); - this.toggleLabels = $(opts.orig).data("togglelabels"); - this.limitBaseCourse = $(opts.orig).data("limit-basecourse"); + this.questions = getDataValue(opts.orig, "questionlist"); + this.proficiency = getDataValue(opts.orig, "proficiency"); + this.minDifficulty = getDataValue(opts.orig, "min-difficulty"); + this.maxDifficulty = getDataValue(opts.orig, "max-difficulty"); + this.points = getDataValue(opts.orig, "points"); + this.autogradable = getDataValue(opts.orig, "autogradable"); + this.not_seen_ever = getDataValue(opts.orig, "not_seen_ever"); + this.selector_id = opts.orig.getAttribute("id"); + this.primaryOnly = getDataValue(opts.orig, "primary"); + this.ABExperiment = getDataValue(opts.orig, "ab"); + this.toggleOptions = getDataValue(opts.orig, "toggleoptions"); + this.toggleLabels = getDataValue(opts.orig, "togglelabels"); + this.limitBaseCourse = getDataValue(opts.orig, "limit-basecourse"); opts.orig.id = this.selector_id; } /** @@ -234,7 +235,7 @@ export default class SelectOne extends RunestoneBase { useRunestoneServices: true, }); if (data.toggleOptions) { - $("#component-preview").hide(); + hideElement(document.getElementById("component-preview")); var toggleQuestionSelect = document.getElementById( selectorId + "-toggleQuestion", ); @@ -243,14 +244,11 @@ export default class SelectOne extends RunestoneBase { toggleQuestionSelect.options[i].value == toggleFirstID ) { toggleQuestionSelect.value = toggleFirstID; - $("#" + selectorId).data( - "toggle_current", - toggleFirstID, - ); - $("#" + selectorId).data( - "toggle_current_type", - toggleQuestionTypes[0], - ); + let selectorElement = + document.getElementById(selectorId); + selectorElement.dataset.toggleCurrent = toggleFirstID; + selectorElement.dataset.toggleCurrentType = + toggleQuestionTypes[0]; break; } } @@ -290,7 +288,8 @@ export default class SelectOne extends RunestoneBase { // on changing the value of toggle select dropdown, render selected question in preview panel, add appropriate buttons, then make preview panel visible async togglePreview(parentID, toggleOptions, toggleQuestionTypes) { - $("#toggle-buttons").html(""); + const toggleButtons = document.getElementById("toggle-buttons"); + toggleButtons.innerHTML = ""; var parentDiv = document.getElementById(parentID); var toggleQuestionSelect = parentDiv.getElementsByTagName("select")[0]; var selectedQuestion = @@ -306,16 +305,15 @@ export default class SelectOne extends RunestoneBase { // add "Close Preview" button to the preview panel let closeButton = document.createElement("button"); - $(closeButton).text("Close Preview"); - $(closeButton).addClass("btn btn-default"); + closeButton.textContent = "Close Preview"; + closeButton.classList.add("btn", "btn-default"); closeButton.addEventListener( "click", function () { - $("#toggle-preview").html(""); - toggleQuestionSelect.value = $("#" + parentID).data( - "toggle_current", - ); - $("#component-preview").hide(); + document.getElementById("toggle-preview").innerHTML = ""; + toggleQuestionSelect.value = + document.getElementById(parentID).dataset.toggleCurrent; + hideElement(document.getElementById("component-preview")); this.logBookEvent({ event: "close_toggle", act: toggleQuestionSelect.value, @@ -323,14 +321,15 @@ export default class SelectOne extends RunestoneBase { }); }.bind(this), ); - $("#toggle-buttons").append(closeButton); + toggleButtons.appendChild(closeButton); // if "lock" is not in toggle options, then allow adding more buttons to the preview panel if (!toggleOptions.includes("lock")) { let setButton = document.createElement("button"); - $(setButton).text("Select this Problem"); - $(setButton).addClass("btn btn-primary"); - $(setButton).click( + setButton.textContent = "Select this Problem"; + setButton.classList.add("btn", "btn-primary"); + setButton.addEventListener( + "click", async function () { await this.toggleSet( parentID, @@ -338,7 +337,7 @@ export default class SelectOne extends RunestoneBase { htmlsrc, toggleQuestionTypes, ); - $("#component-preview").hide(); + hideElement(document.getElementById("component-preview")); this.logBookEvent({ event: "select_toggle", act: selectedQuestion, @@ -346,11 +345,13 @@ export default class SelectOne extends RunestoneBase { }); }.bind(this), ); - $("#toggle-buttons").append(setButton); + toggleButtons.appendChild(setButton); // if "transfer" in toggle options, and if current question type is Parsons and selected question type is active code, then add "Transfer" button to preview panel if (toggleOptions.includes("transfer")) { - var currentType = $("#" + parentID).data("toggle_current_type"); + var currentType = + document.getElementById(parentID).dataset + .toggleCurrentType; var selectedType = toggleQuestionTypes[toggleQuestionSelect.selectedIndex]; if ( @@ -358,9 +359,10 @@ export default class SelectOne extends RunestoneBase { selectedType == "Active Write Code" ) { let transferButton = document.createElement("button"); - $(transferButton).text("Transfer Response"); - $(transferButton).addClass("btn btn-primary"); - $(transferButton).click( + transferButton.textContent = "Transfer Response"; + transferButton.classList.add("btn", "btn-primary"); + transferButton.addEventListener( + "click", async function () { await this.toggleTransfer( parentID, @@ -370,12 +372,12 @@ export default class SelectOne extends RunestoneBase { ); }.bind(this), ); - $("#toggle-buttons").append(transferButton); + toggleButtons.appendChild(transferButton); } } } - $("#component-preview").show(); + showElement(document.getElementById("component-preview")); } // on clicking "Select this Problem" button, close preview panel, replace current question in assignments page with selected question, and send request to update grading database @@ -397,12 +399,11 @@ export default class SelectOne extends RunestoneBase { {}, ); await fetch(request); - $("#toggle-preview").html(""); - $("#" + parentID).data("toggle_current", selectedQuestion); - $("#" + parentID).data( - "toggle_current_type", - toggleQuestionTypes[toggleQuestionSelect.selectedIndex], - ); + document.getElementById("toggle-preview").innerHTML = ""; + let parentDiv = document.getElementById(parentID); + parentDiv.dataset.toggleCurrent = selectedQuestion; + parentDiv.dataset.toggleCurrentType = + toggleQuestionTypes[toggleQuestionSelect.selectedIndex]; } // on clicking "Transfer" button, extract the current text and indentation of the Parsons blocks in the answer space, then paste that into the selected active code question @@ -505,7 +506,21 @@ export default class SelectOne extends RunestoneBase { htmlsrc, toggleQuestionTypes, ); - $("#component-preview").hide(); + hideElement(document.getElementById("component-preview")); + } +} + +// The preview panel is shared by all toggle questions on a page and may not +// exist yet when hidden/shown. +function hideElement(element) { + if (element) { + element.style.display = "none"; + } +} + +function showElement(element) { + if (element) { + element.style.display = ""; } } @@ -521,13 +536,13 @@ window.component_factory.selectquestion = function (opts) { * When the page is loaded and the login checks are complete find and render * each selectquestion component that is not part of a timedAssessment. **/ -$(document).on("runestone:login-complete", async function () { +document.addEventListener("runestone:login-complete", async function () { let selQuestions = document.querySelectorAll( "[data-component=selectquestion]", ); for (let cq of selQuestions) { try { - if ($(cq).closest("[data-component=timedAssessment]").length == 0) { + if (!cq.closest("[data-component=timedAssessment]")) { // If this element exists within a timed component, don't render it here let tmp = new SelectOne({ orig: cq }); await tmp.initialize(); diff --git a/bases/rsptx/interactives/runestone/selectquestion/test/selectone.test.js b/bases/rsptx/interactives/runestone/selectquestion/test/selectone.test.js new file mode 100644 index 000000000..b3f255662 --- /dev/null +++ b/bases/rsptx/interactives/runestone/selectquestion/test/selectone.test.js @@ -0,0 +1,213 @@ +// Characterization tests for the SelectOne (selectquestion) component. These +// were first written against the jQuery implementation and now guard the +// jQuery-free version. renderComponent.js is replaced by a stub (see +// vitest.config.js), so rendering means "html lands in the target div". +// Note: deliberately NO jquery-globals import here -- selectquestion must +// work without jQuery. +import { describe, it, expect, beforeEach, vi } from "vitest"; +import SelectOne from "../js/selectone.js"; +import RunestoneBase from "../../common/js/runestonebase.js"; + +const MC_SRC = + '

Pick one.

'; + +function makeFixture({ id = "test_selq_1", attrs = "" } = {}) { + document.body.innerHTML = ` +
+
+
`; + return document.getElementById(id); +} + +// Node's Request rejects relative URLs; fetch is mocked, so a capturing +// stand-in suffices. +class FakeRequest { + constructor(url, opts = {}) { + this.url = url; + Object.assign(this, opts); + } +} + +// Route the component's fetches: get_question_source POSTs resolve with +// `source`, htmlsrc?acid= GETs resolve from `toggleSrcs`. +function stubFetch({ source = MC_SRC, toggleSrcs = {} } = {}) { + const fetchMock = vi.fn(async (request) => { + let detail = source; + const m = request.url.match(/htmlsrc\?acid=([\w-]+)/); + if (m) { + detail = toggleSrcs[m[1]]; + } + return { ok: true, json: async () => ({ detail }) }; + }); + vi.stubGlobal("Request", FakeRequest); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +beforeEach(() => { + document.body.innerHTML = ""; + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); + vi.restoreAllMocks(); + vi.stubGlobal("alert", vi.fn()); +}); + +describe("constructor", () => { + it("reads its configuration from data attributes with jQuery-style coercion", () => { + const orig = makeFixture({ + attrs: `data-questionlist="q_a, q_b" data-points="3" + data-proficiency="loops" data-min-difficulty="2" + data-max-difficulty="4" data-autogradable="true" + data-not_seen_ever="true" data-primary="true" + data-ab="mainq" data-limit-basecourse="true"`, + }); + const so = new SelectOne({ orig }); + expect(so.selector_id).toBe("test_selq_1"); + expect(so.questions).toBe("q_a, q_b"); + expect(so.points).toBe(3); + expect(so.proficiency).toBe("loops"); + expect(so.minDifficulty).toBe(2); + expect(so.maxDifficulty).toBe(4); + expect(so.autogradable).toBe(true); + expect(so.not_seen_ever).toBe(true); + expect(so.primaryOnly).toBe(true); + expect(so.ABExperiment).toBe("mainq"); + expect(so.limitBaseCourse).toBe(true); + }); +}); + +describe("initialize", () => { + it("asks the server for a question and renders it in place", async () => { + const fetchMock = stubFetch(); + const orig = makeFixture({ + attrs: 'data-questionlist="q_a, q_b" data-points="3"', + }); + const so = new SelectOne({ orig }); + await so.initialize(); + const body = JSON.parse(fetchMock.mock.calls[0][0].body); + expect(fetchMock.mock.calls[0][0].url).toContain( + "/assessment/get_question_source", + ); + expect(body.selector_id).toBe("test_selq_1"); + expect(body.questions).toBe("q_a, q_b"); + expect(body.points).toBe(3); + expect( + document.querySelector("#test_selq_1 #real_q_1"), + ).not.toBe(null); + }); + + it("alerts and throws when the server has no matching question", async () => { + stubFetch({ source: "No preview available for this question" }); + const orig = makeFixture(); + const so = new SelectOne({ orig }); + await expect(so.initialize()).rejects.toThrow( + "Unable to find a question", + ); + expect(alert).toHaveBeenCalledWith( + expect.stringContaining("test_selq_1"), + ); + }); + + it("defers rendering to createTimedComponent inside a timed exam", async () => { + stubFetch(); + const orig = makeFixture({ attrs: 'data-questionlist="q_a"' }); + const so = new SelectOne({ orig, timed: true, assessmentTaken: false }); + const rqaEntry = { question: so }; + so.origOpts.rqa = [rqaEntry]; + await so.initialize(); + // the placeholder in the timed assessment's question list is replaced + expect(rqaEntry.question).not.toBe(so); + expect(rqaEntry.question.htmlsrc).toBe(MC_SRC); + expect(so.realComponent).toBe(rqaEntry.question); + expect(so.realComponent.selectorId).toBe("test_selq_1"); + expect(so.containerDiv).toBe(rqaEntry.question.containerDiv); + }); +}); + +describe("page wiring", () => { + it("initializes selectquestions at login-complete, skipping timed ones", async () => { + stubFetch(); + document.body.innerHTML = ` +
+
+
+
+
+
`; + document.dispatchEvent(new CustomEvent("runestone:login-complete")); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(document.querySelector("#selq_free #real_q_1")).not.toBe(null); + expect( + document.querySelector("#selq_timed #real_q_1"), + ).toBe(null); + }); +}); + +describe("toggle questions (assignment page)", () => { + const PARSONS_SRC = + '

Arrange.

'; + + function makeToggleFixture() { + stubFetch({ + source: '
', + toggleSrcs: { + q_a: '
', + q_b: PARSONS_SRC, + }, + }); + return makeFixture({ + attrs: 'data-questionlist="q_a, q_b" data-toggleoptions="toggle"', + }); + } + + it("builds the toggle dropdown and hidden preview panel", async () => { + const orig = makeToggleFixture(); + const so = new SelectOne({ orig }); + await so.initialize(); + const select = document.getElementById( + "test_selq_1-toggleQuestion", + ); + const options = [...select.options]; + expect(options.map((o) => o.value)).toEqual(["q_a", "q_b"]); + expect(options[0].textContent).toContain("Multiple Choice - q_a"); + expect(options[1].textContent).toContain("Parsons Mixed-Up Code - q_b"); + expect(select.value).toBe("q_a"); + const preview = document.getElementById("component-preview"); + expect(preview.style.display).toBe("none"); + expect( + document.getElementById("test_selq_1-toggleSelectedQuestion"), + ).not.toBe(null); + }); + + it("previews the chosen question and can close the preview", async () => { + const orig = makeToggleFixture(); + const so = new SelectOne({ orig }); + vi.spyOn(RunestoneBase.prototype, "logBookEvent").mockResolvedValue( + undefined, + ); + await so.initialize(); + const select = document.getElementById("test_selq_1-toggleQuestion"); + select.value = "q_b"; + select.dispatchEvent(new Event("change")); + await new Promise((resolve) => setTimeout(resolve, 20)); + const preview = document.getElementById("component-preview"); + expect(preview.style.display).not.toBe("none"); + expect( + document.querySelector("#toggle-preview [data-component=parsons]"), + ).not.toBe(null); + const buttons = [ + ...document.querySelectorAll("#toggle-buttons button"), + ]; + expect(buttons.map((b) => b.textContent)).toEqual([ + "Close Preview", + "Select this Problem", + ]); + buttons[0].click(); + expect(preview.style.display).toBe("none"); + expect(document.getElementById("toggle-preview").innerHTML).toBe(""); + expect(select.value).toBe("q_a"); + }); +}); diff --git a/bases/rsptx/interactives/runestone/shortanswer/test/shortanswer.test.js b/bases/rsptx/interactives/runestone/shortanswer/test/shortanswer.test.js new file mode 100644 index 000000000..dbb6969ee --- /dev/null +++ b/bases/rsptx/interactives/runestone/shortanswer/test/shortanswer.test.js @@ -0,0 +1,327 @@ +// Characterization tests for the ShortAnswer component. shortanswer.js was +// already free of jQuery when these were written; they guard that state. +// Note: deliberately NO jquery-globals import here -- shortanswer must work +// without jQuery. +import { describe, it, expect, beforeEach, vi } from "vitest"; +import ShortAnswer from "../js/shortanswer.js"; +import TimedShortAnswer from "../js/timed_shortanswer.js"; +import RunestoneBase from "../../common/js/runestonebase.js"; + +// Book pages provide a [data-component=shortanswer] element whose innerHTML is +// the question text (see the Sphinx/PreTeXt templates). +function makeFixture({ + id = "test_shortanswer_1", + question = "

Explain your reasoning.

", + attrs = "", +} = {}) { + document.body.innerHTML = ` +
+
+ ${question} +
+
`; + return document.getElementById(id); +} + +const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function makeShortAnswer(fixtureOpts = {}, extraOpts = {}) { + const orig = makeFixture(fixtureOpts); + const sa = new ShortAnswer({ + orig: orig, + useRunestoneServices: false, + ...extraOpts, + }); + await sa.component_ready_promise; + await tick(); + return sa; +} + +beforeEach(() => { + document.body.innerHTML = ""; + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); + vi.restoreAllMocks(); + vi.stubGlobal("alert", vi.fn()); +}); + +describe("construction", () => { + it("replaces the original element with a form holding a textarea and save button", async () => { + const sa = await makeShortAnswer(); + const container = document.getElementById("test_shortanswer_1"); + expect(container).toBe(sa.containerDiv); + expect(container.querySelector("form").id).toBe( + "test_shortanswer_1_journal", + ); + const textarea = container.querySelector("textarea"); + expect(textarea.id).toBe("test_shortanswer_1_solution"); + expect(textarea.placeholder).toBe("Write your answer here"); + expect(container.querySelector("button.btn-success").textContent).toBe( + "Save", + ); + }); + + it("moves the question content into the fieldset", async () => { + await makeShortAnswer({ question: "

Why?

" }); + const legend = document.querySelector(".journal-question"); + expect(legend.querySelector("#qmark").textContent).toBe("Why?"); + }); + + it("keeps the original element's classes on the new container", async () => { + await makeShortAnswer(); + const container = document.getElementById("test_shortanswer_1"); + expect(container.classList.contains("journal")).toBe(true); + expect(container.classList.contains("alert-warning")).toBe(true); + }); + + it("honors data-placeholder", async () => { + await makeShortAnswer({ attrs: 'data-placeholder="Type here"' }); + expect(document.querySelector("textarea").placeholder).toBe( + "Type here", + ); + }); + + it("sets the optional flag from data-optional", async () => { + const sa = await makeShortAnswer({ attrs: "data-optional" }); + expect(sa.optional).toBe(true); + }); + + it("starts with the feedback div hidden", async () => { + const sa = await makeShortAnswer(); + expect(sa.feedbackDiv.style.display).toBe("none"); + }); + + it("adds a file input when data-attachment is set (student mode)", async () => { + // getAttachmentName fetches; stub it out. + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ ok: false, statusText: "nope" }), + ); + const sa = await makeShortAnswer({ attrs: "data-attachment" }); + expect(sa.attachment).toBe(true); + expect( + document.getElementById("test_shortanswer_1_fileme").type, + ).toBe("file"); + }); +}); + +describe("editing feedback", () => { + it("warns that the answer is unsaved on keydown", async () => { + const sa = await makeShortAnswer(); + sa.jTextArea.dispatchEvent( + new KeyboardEvent("keydown", { key: "a", bubbles: true }), + ); + expect(sa.feedbackDiv.innerHTML).toBe( + "Your answer has not been saved yet!", + ); + expect(sa.feedbackDiv.classList.contains("alert-danger")).toBe(true); + }); + + it("shows the autosave message instead when timed", async () => { + const sa = await makeShortAnswer(); + sa.isTimed = true; + sa.jTextArea.dispatchEvent( + new KeyboardEvent("keydown", { key: "a", bubbles: true }), + ); + expect(sa.feedbackDiv.innerHTML).toBe( + "Your answer is automatically saved.", + ); + }); + + it("marks the question answered when the textarea changes", async () => { + const sa = await makeShortAnswer(); + sa.jTextArea.value = "an answer"; + sa.jTextArea.dispatchEvent(new Event("change")); + expect(sa.isAnswered).toBe(true); + }); +}); + +describe("saving", () => { + it("stores the answer in localStorage and logs it on Save", async () => { + const sa = await makeShortAnswer(); + const logSpy = vi + .spyOn(RunestoneBase.prototype, "logBookEvent") + .mockResolvedValue(undefined); + sa.jTextArea.value = "my thoughtful answer"; + sa.submitButton.click(); + await tick(); + const stored = JSON.parse( + localStorage.getItem(sa.localStorageKey()), + ); + expect(stored.answer).toBe("my thoughtful answer"); + expect(logSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event: "shortanswer", + act: "my thoughtful answer", + answer: "my thoughtful answer", + div_id: "test_shortanswer_1", + }), + ); + expect(sa.feedbackDiv.innerHTML).toBe("Your answer has been saved."); + expect(sa.feedbackDiv.classList.contains("alert-success")).toBe(true); + expect(sa.feedbackDiv.style.display).toBe("block"); + }); + + it("passes a student id through to the log event when given", async () => { + const sa = await makeShortAnswer(); + const logSpy = vi + .spyOn(RunestoneBase.prototype, "logBookEvent") + .mockResolvedValue(undefined); + sa.jTextArea.value = "group answer"; + await sa.logCurrentAnswer("student_42"); + expect(logSpy).toHaveBeenCalledWith( + expect.objectContaining({ sid: "student_42" }), + ); + }); +}); + +describe("checkLocalStorage", () => { + it("restores a previously saved answer into the textarea", async () => { + const first = await makeShortAnswer(); + first.jTextArea.value = "saved before"; + await first.checkCurrentAnswer(); + // A fresh render of the same question picks the answer up. + const second = await makeShortAnswer(); + expect(second.jTextArea.value).toBe("saved before"); + }); + + it("drops malformed stored data", async () => { + const sa = await makeShortAnswer(); + localStorage.setItem(sa.localStorageKey(), "not json{"); + sa.checkLocalStorage(); + expect(localStorage.getItem(sa.localStorageKey())).toBe(null); + }); + + it("does not restore when grading", async () => { + const first = await makeShortAnswer(); + first.jTextArea.value = "student work"; + await first.checkCurrentAnswer(); + const second = await makeShortAnswer({}, { graderactive: true }); + expect(second.jTextArea.value).toBe(""); + }); +}); + +describe("restoreAnswers", () => { + it("fills the textarea and shows the submission timestamp", async () => { + const sa = await makeShortAnswer(); + const when = new Date("2026-01-15T10:30:00"); + sa.restoreAnswers({ answer: "server answer", timestamp: when }); + expect(sa.jTextArea.value).toBe("server answer"); + const p = document.querySelector(".shortanswer__timestamp"); + expect(p.textContent).toBe(when.toLocaleString()); + }); + + it("treats a null answer as empty", async () => { + const sa = await makeShortAnswer(); + sa.restoreAnswers({ answer: null }); + expect(sa.jTextArea.value).toBe(""); + }); + + it("shows score and comment feedback", async () => { + const sa = await makeShortAnswer(); + sa.restoreAnswers({ answer: "x", score: 4, comment: "good work" }); + expect(sa.feedbackDiv.innerHTML).toBe("Score: 4 -- good work"); + expect(sa.feedbackDiv.style.display).toBe("block"); + }); + + it("adds a toggle button that swaps between on-time and late answers", async () => { + const sa = await makeShortAnswer(); + sa.restoreAnswers({ + answer: "on time", + timestamp: new Date("2026-01-15T10:30:00"), + last_answer: "late edit", + last_timestamp: new Date("2026-01-20T10:30:00"), + }); + const toggle = sa.buttonDiv.querySelector("button.btn-warning"); + expect(toggle.textContent).toBe("Show Late Answer"); + toggle.click(); + expect(sa.jTextArea.value).toBe("late edit"); + expect(toggle.textContent).toBe("Show on-Time Answer"); + toggle.click(); + expect(sa.jTextArea.value).toBe("on time"); + }); +}); + +describe("renderMath", () => { + it("renders LaTeX-looking answers into a live region with converted delimiters", async () => { + const sa = await makeShortAnswer(); + sa.renderMath("The area is $$x^2$$ and $y$"); + const rendered = document.querySelector( + ".shortanswer__rendered-answer", + ); + expect(rendered.innerHTML).toContain("\\[ x^2 \\]"); + expect(rendered.innerHTML).toContain("\\( y \\)"); + expect(rendered.getAttribute("aria-live")).toBe("polite"); + expect(sa.rederedAnswerDiv.style.display).toBe("block"); + }); + + it("hides the rendered answer again for plain text", async () => { + const sa = await makeShortAnswer(); + sa.renderMath("has math $x^2$"); + sa.renderMath("plain prose"); + expect(sa.rederedAnswerDiv.style.display).toBe("none"); + }); + + it("creates no rendered-answer div for plain text", async () => { + const sa = await makeShortAnswer(); + sa.renderMath("plain prose"); + expect(document.querySelector(".shortanswer__rendered-answer")).toBe( + null, + ); + }); +}); + +describe("disableInteraction", () => { + it("disables the textarea", async () => { + const sa = await makeShortAnswer(); + sa.disableInteraction(); + expect(sa.jTextArea.disabled).toBe(true); + }); +}); + +describe("TimedShortAnswer", () => { + async function makeTimed() { + const orig = makeFixture(); + const tsa = new TimedShortAnswer({ + orig: orig, + useRunestoneServices: false, + timed: true, + }); + await tsa.component_ready_promise; + await tick(); + return tsa; + } + + it("hides the save button and shows the clock icon", async () => { + const tsa = await makeTimed(); + expect(tsa.submitButton.style.display).toBe("none"); + expect(document.querySelector(".timeTip img").src).toContain( + "clock.png", + ); + expect(tsa.isTimed).toBe(true); + }); + + it("is ignored in grading and can hide its feedback", async () => { + const tsa = await makeTimed(); + expect(tsa.checkCorrectTimed()).toBe("I"); + tsa.feedbackDiv.style.display = "block"; + tsa.hideFeedback(); + expect(tsa.feedbackDiv.style.display).toBe("none"); + }); +}); + +describe("component factory", () => { + it("builds the timed variant only when opts.timed is set", async () => { + const plain = window.component_factory.shortanswer({ + orig: makeFixture({ id: "sa_plain" }), + }); + expect(plain).toBeInstanceOf(ShortAnswer); + expect(plain).not.toBeInstanceOf(TimedShortAnswer); + const timed = window.component_factory.shortanswer({ + orig: makeFixture({ id: "sa_timed" }), + timed: true, + }); + expect(timed).toBeInstanceOf(TimedShortAnswer); + }); +}); diff --git a/bases/rsptx/interactives/runestone/showeval/js/showEval-0.10.0.js b/bases/rsptx/interactives/runestone/showeval/js/showEval-0.10.0.js deleted file mode 100644 index f63632a68..000000000 --- a/bases/rsptx/interactives/runestone/showeval/js/showEval-0.10.0.js +++ /dev/null @@ -1,238 +0,0 @@ -/* -ShowEval, a JS module for creating visualizations of expression evaluation. Mainly for programming tutorials. - -Al Sweigart -al@inventwithpython.com -https://github.com/asweigart/ -*/ - -var SHOWEVAL = (function () { - var thisModule = {}; - - - thisModule.ShowEval = function(container, steps, showTrace, addButtons) { - if (typeof container === 'string') { - container = document.querySelector(container); - } - this.container = container; - this.container.classList.add('showEval'); - this.steps = steps.slice(); - this.currentStep = 0; - this.createTrace = showTrace; // TODO - reset doesn't work for traces - - if (addButtons == true) { - let nextButton = document.createElement("button"); - nextButton.textContent = 'Next'; - nextButton.addEventListener("click", () => { - this.evaluateStep(); - }); - this.container.appendChild(nextButton); - - let resetButton = document.createElement("button"); - resetButton.textContent = 'Reset'; - resetButton.addEventListener("click", () => { - this.evaluateStep(); - }); - this.container.appendChild(resetButton); - - let codeDiv = document.createElement("div"); - this.container.appendChild(codeDiv); - this.container = codeDiv; - } - - // create elements - this.currentStepDiv = document.createElement('div'); - this.currentStepDiv.classList.add('currentStepDiv'); - this.container.appendChild(this.currentStepDiv); - - let preSpan = document.createElement('span'); - preSpan.style.verticalAlign = 'text-top'; - preSpan.classList.add('pre'); - this.currentStepDiv.appendChild(preSpan); - - let evalSpan = document.createElement('span'); - evalSpan.style.verticalAlign = 'text-top'; - evalSpan.classList.add('eval'); - this.currentStepDiv.appendChild(evalSpan); - - let postSpan = document.createElement('span'); - postSpan.style.verticalAlign = 'text-top'; - postSpan.classList.add('post'); - this.currentStepDiv.appendChild(postSpan); - - // parse steps and turn into a 4-string array: ['pre', 'before eval', 'after eval', 'post'] - for (var i = 0 ; i < this.steps.length; i++) { - var s = this.steps[i]; - this.steps[i] = [s.substring(0, s.indexOf('{{')), // 'pre' - s.substring(s.indexOf('{{') + 2, s.indexOf('}}{{')), // 'before eval' - s.substring(s.indexOf('}}{{') + 4, s.indexOf('}}', s.indexOf('}}{{') + 4)), // 'after eval' - s.substring(s.indexOf('}}', s.indexOf('}}{{') + 4) + 2)]; // 'post' - } - this.reset(); - }; - - thisModule.ShowEval.prototype.reset = function() { - this.container.querySelectorAll('.previousStep').forEach(el => el.remove()); - this.setStep(0); - }; - - thisModule.ShowEval.prototype.setStep = function(step) { - this.currentStep = step; - newWidth = this.getWidth(this.steps[this.currentStep][1]); - this.currentStepDiv.querySelector('.eval').style.width = newWidth + 'px'; - this.currentStepDiv.querySelector('.pre').innerHTML = this.steps[step][0]; - this.currentStepDiv.querySelector('.eval').innerHTML = this.steps[step][1]; - this.currentStepDiv.querySelector('.post').innerHTML = this.steps[step][3]; - }; - - thisModule.ShowEval.prototype.getWidth = function(text) { // TODO - class style must match or else width will be off. - var newElem = document.createElement('div'); - newElem.classList.add('showEval'); - newElem.style.display = 'none'; - newElem.innerHTML = text; - document.body.appendChild(newElem); - var newWidth = newElem.offsetWidth + 1; // +1 is a hack - newElem.remove(); - - return newWidth; - }; - - thisModule.ShowEval.prototype.createPreviousStepDiv = function(step) { - let prevDiv = document.createElement('div'); - prevDiv.classList.add('previousStep'); - prevDiv.innerHTML = this.steps[step][0] + this.steps[step][1] + this.steps[step][3]; - this.currentStepDiv.parentNode.insertBefore(prevDiv, this.currentStepDiv); - }; - - thisModule.ShowEval.prototype.evaluateStep = function(step) { - if (step === undefined) { - step = this.currentStep; - } - if (this.currentStep >= this.steps.length) { - //this.currentStep = 0; - //step = 0; - return; // do nothing if on last step - } - this.setStep(step); - - var fadeInSpeed = 0; - if (this.createTrace) { - this.createPreviousStepDiv(step); - this.currentStepDiv.style.display = 'none'; - fadeInSpeed = 200; - } - - newWidth = this.getWidth(this.steps[step][2]); - var evalElem = this.currentStepDiv.querySelector('.eval'); - - var thisShowEval = this; - - evalElem.style.color = 'red'; - - // Fade in currentStepDiv - this.fadeToOpacity(this.currentStepDiv, 1, fadeInSpeed, function() { - window.setTimeout(function() { - // Fade out evalElem - thisShowEval.fadeToOpacity(evalElem, 0, 400, function() { - //evalElem.style.overflow = 'hidden'; - // Animate width - thisShowEval.animateWidth(evalElem, newWidth, 400, function() { - evalElem.innerHTML = thisShowEval.steps[step][2]; - // Fade in evalElem - thisShowEval.fadeToOpacity(evalElem, 1, 400, function() { - window.setTimeout(function() { - //evalElem.style.overflow = 'visible'; - evalElem.style.color = 'black'; - thisShowEval.currentStep += 1; - if (thisShowEval.currentStep < thisShowEval.steps.length) { - thisShowEval.setStep(thisShowEval.currentStep); - } - }, 600); - }); - }); - }); - }, 600); - }); - }; - - // Helper function to fade element to specific opacity - thisModule.ShowEval.prototype.fadeToOpacity = function(element, targetOpacity, duration, callback) { - if (duration === 0) { - element.style.opacity = targetOpacity; - if (targetOpacity === 1 && element.style.display === 'none') { - element.style.display = ''; - } - if (callback) callback(); - return; - } - - var startOpacity = parseFloat(window.getComputedStyle(element).opacity) || 0; - var startTime = performance.now(); - - if (targetOpacity === 1 && element.style.display === 'none') { - element.style.display = ''; - element.style.opacity = startOpacity; - } - - function animate(currentTime) { - var elapsed = currentTime - startTime; - var progress = Math.min(elapsed / duration, 1); - - element.style.opacity = startOpacity + (targetOpacity - startOpacity) * progress; - - if (progress < 1) { - requestAnimationFrame(animate); - } else { - if (callback) callback(); - } - } - - requestAnimationFrame(animate); - }; - - // Helper function to animate width - thisModule.ShowEval.prototype.animateWidth = function(element, targetWidth, duration, callback) { - var startWidth = element.offsetWidth; - var startTime = performance.now(); - - function animate(currentTime) { - var elapsed = currentTime - startTime; - var progress = Math.min(elapsed / duration, 1); - - element.style.width = (startWidth + (targetWidth - startWidth) * progress) + 'px'; - - if (progress < 1) { - requestAnimationFrame(animate); - } else { - if (callback) callback(); - } - } - - requestAnimationFrame(animate); - }; - - return thisModule; -}()); - -var s; -let next = document.querySelector("#nextStep"); -let reset = document.querySelector("#reset"); -reset.addEventListener("click", function () { - console.log("Reset button clicked"); - s.reset(0); -}); -let nextStep = document.querySelector("#nextStep"); -nextStep.addEventListener("click", function () { - const frameid = window.frameElement.id; - console.log(`Next Step button clicked in frame ${frameid}`); - const message = { - subject: "SPLICE.sendEvent", - name: "nextStepClicked", - activity_id: frameid, - message_id: 10, - }; - window.parent.postMessage(message, "*"); - s.evaluateStep(); -}); - - diff --git a/bases/rsptx/interactives/runestone/showeval/js/showEval.js b/bases/rsptx/interactives/runestone/showeval/js/showEval.js index 0387d0dca..46c250525 100644 --- a/bases/rsptx/interactives/runestone/showeval/js/showEval.js +++ b/bases/rsptx/interactives/runestone/showeval/js/showEval.js @@ -1,15 +1,21 @@ /* -This component is based on ... -ShowEval, a JS module for creating visualizations of expression evaluation. Mainly for programming tutorials. -0.9.1 +ShowEval, a JS module for creating visualizations of expression evaluation. +Mainly for programming tutorials. Al Sweigart al@inventwithpython.com https://github.com/asweigart/ + +This is Al's jQuery-free 0.10.0 core wrapped as a Runestone component (the +component features -- annotations, logging, per-question buttons -- come +from the old 0.9.1-based showEval.js it replaces). The standalone variant +used by PreTeXt slates is served from the Runestone CDN as +showEval-0.10.0.js and is not built from this module. */ "use strict"; import RunestoneBase from "../../common/js/runestonebase"; +import { getDataValue } from "../../common/js/domutil.js"; import "../css/showEval.css"; export class ShowEval extends RunestoneBase { @@ -24,22 +30,29 @@ export class ShowEval extends RunestoneBase { } this.steps = steps.slice(); this.currentStep = 0; - this.createTrace = $(opts.orig).data("tracemode"); // TODO - reset doesn't work for traces - this.rb = new RunestoneBase(opts); + this.createTrace = getDataValue(opts.orig, "tracemode"); // TODO - reset doesn't work for traces + // create elements - this.currentStepDiv = $("
").addClass("currentStepDiv"); - $(this.containerDiv).append(this.currentStepDiv); - this.currentStepDiv.append($("").addClass("pre")); - this.currentStepDiv.append($("").addClass("eval")); - this.currentStepDiv.append($("").addClass("post")); - this.currentStepDiv.append($("
").addClass("anno")); + this.currentStepDiv = document.createElement("div"); + this.currentStepDiv.classList.add("currentStepDiv"); + this.containerDiv.appendChild(this.currentStepDiv); + for (const part of ["pre", "eval", "post"]) { + let span = document.createElement("span"); + span.classList.add(part); + this.currentStepDiv.appendChild(span); + } + this.annoDiv = document.createElement("div"); + this.annoDiv.classList.add("anno"); + this.currentStepDiv.appendChild(this.annoDiv); + this.evalSpan = this.currentStepDiv.querySelector(".eval"); this.setNextButton(`#${this.divid}_nextStep`); this.setResetButton(`#${this.divid}_reset`); - // parse steps and turn into a 4-string array: ['pre', 'before eval', 'after eval', 'post'] + // parse steps and turn into a 5-string array: + // ['pre', 'before eval', 'after eval', 'post', 'anno'] for (var i = 0; i < this.steps.length; i++) { var s = this.steps[i]; - let endpoint, pItem, comment; + let endpoint, comment; if (s.includes("##")) { // If there is an annotation @@ -59,9 +72,8 @@ export class ShowEval extends RunestoneBase { s.substring( s.indexOf("}}", s.indexOf("}}{{") + 4) + 2, endpoint, - ), - ]; // 'post' - + ), // 'post' + ]; this.steps[i].push(comment); // 'anno' } this.reset(); @@ -71,23 +83,28 @@ export class ShowEval extends RunestoneBase { } setNextButton(nextButtonSelector) { - var thisObj = this; // uhg, javascript - $(nextButtonSelector).click(function () { - thisObj.evaluateStep(nextButtonSelector); + const button = document.querySelector(nextButtonSelector); + button?.addEventListener("click", () => { + this.evaluateStep(button); }); } setResetButton(resetButtonSelector) { - var thisObj = this; // uhg, javascript - $(resetButtonSelector).click(function () { - thisObj.reset(0); - }); + document + .querySelector(resetButtonSelector) + ?.addEventListener("click", () => { + this.reset(); + }); } reset() { - $(this.containerDiv).find(".previousStep").remove(); + for (const el of this.containerDiv.querySelectorAll( + ".previousStep", + )) { + el.remove(); + } this.setStep(0); - this.rb.logBookEvent({ + this.logBookEvent({ event: "showeval", act: "reset", div_id: this.containerDiv.id, @@ -98,125 +115,195 @@ export class ShowEval extends RunestoneBase { this.currentStep = step; let newWidth = this.getWidth(this.steps[this.currentStep][1]); if (this.steps[step][4]) { - this.currentStepDiv.children(".anno").html(this.steps[step][4]); - this.currentStepDiv.children(".anno").show(); + this.annoDiv.innerHTML = this.steps[step][4]; + this.annoDiv.style.display = ""; } else { - this.currentStepDiv.children(".anno").hide(); + this.annoDiv.style.display = "none"; } - this.currentStepDiv.children(".eval").width(newWidth); - this.currentStepDiv.children(".pre").html(this.steps[step][0]); - this.currentStepDiv.children(".eval").html(this.steps[step][1]); - this.currentStepDiv.children(".post").html(this.steps[step][3]); + this.evalSpan.style.width = newWidth + "px"; + this.currentStepDiv.querySelector(".pre").innerHTML = + this.steps[step][0]; + this.evalSpan.innerHTML = this.steps[step][1]; + this.currentStepDiv.querySelector(".post").innerHTML = + this.steps[step][3]; } getWidth(text) { // TODO - class style must match or else width will be off. - var newElem = $("
") - .addClass("showEval evalCont") - .hide() - .html(text); - $("body").append(newElem); - var newWidth = newElem.width() + 1; // +1 is a hack + var newElem = document.createElement("div"); + newElem.classList.add("showEval", "evalCont"); + newElem.style.display = "none"; + newElem.innerHTML = text; + document.body.appendChild(newElem); + var newWidth = newElem.offsetWidth + 1; // +1 is a hack newElem.remove(); return newWidth; } createPreviousStepDiv(step) { - this.currentStepDiv.before( - $("
") - .addClass("previousStep") - .html( - this.steps[step][0] + - this.steps[step][1] + - this.steps[step][3], - ), - ); + let prevDiv = document.createElement("div"); + prevDiv.classList.add("previousStep"); + prevDiv.innerHTML = + this.steps[step][0] + this.steps[step][1] + this.steps[step][3]; + this.currentStepDiv.before(prevDiv); } - evaluateStep(buttonId, step) { - this.currentStepDiv.children(".anno").hide(); - $(buttonId).attr("disabled", true); + evaluateStep(button, step) { + this.annoDiv.style.display = "none"; + if (button) { + button.disabled = true; + } if (step === undefined) { step = this.currentStep; } if (this.currentStep >= this.steps.length) { - //this.currentStep = 0; - //step = 0; - $(buttonId).attr("disabled", false); + if (button) { + button.disabled = false; + } return; // do nothing if on last step } - //this.setStep(step); var fadeInSpeed = 0; if (this.createTrace) { this.createPreviousStepDiv(step); - this.currentStepDiv.hide(); + this.currentStepDiv.style.display = "none"; fadeInSpeed = 200; } let newWidth = this.getWidth(this.steps[step][2]); - var evalElem = this.currentStepDiv.children(".eval"); + var evalElem = this.evalSpan; var thisShowEval = this; - evalElem.css("color", "red"); + evalElem.style.color = "red"; - this.currentStepDiv.fadeTo(fadeInSpeed, 1, function () { + // Fade in currentStepDiv + this.fadeToOpacity(this.currentStepDiv, 1, fadeInSpeed, function () { window.setTimeout(function () { - evalElem.fadeTo(400, 0, function () { - //evalElem.css('overflow', 'hidden'); - evalElem.animate( - { width: newWidth, duration: 400 }, + // Fade out evalElem + thisShowEval.fadeToOpacity(evalElem, 0, 400, function () { + // Animate width + thisShowEval.animateWidth( + evalElem, + newWidth, + 400, function () { - evalElem.html(thisShowEval.steps[step][2]); - evalElem.fadeTo(400, 1, function () { - window.setTimeout(function () { - //evalElem.css('overflow', 'visible'); - evalElem.css("color", "#333"); - thisShowEval.currentStep += 1; - if ( - thisShowEval.currentStep < - thisShowEval.steps.length - ) { - thisShowEval.setStep( - thisShowEval.currentStep, - ); - } - $(buttonId).attr("disabled", false); - }, 600); - }); + evalElem.innerHTML = thisShowEval.steps[step][2]; + // Fade in evalElem + thisShowEval.fadeToOpacity( + evalElem, + 1, + 400, + function () { + window.setTimeout(function () { + evalElem.style.color = "#333"; + thisShowEval.currentStep += 1; + if ( + thisShowEval.currentStep < + thisShowEval.steps.length + ) { + thisShowEval.setStep( + thisShowEval.currentStep, + ); + } + if (button) { + button.disabled = false; + } + }, 600); + }, + ); }, ); }); }, 600); }); - this.rb.logBookEvent({ + this.logBookEvent({ event: "showeval", act: "next", div_id: this.containerDiv.id, }); } + + // Helper function to fade element to specific opacity + fadeToOpacity(element, targetOpacity, duration, callback) { + if (duration === 0) { + element.style.opacity = targetOpacity; + if (targetOpacity === 1 && element.style.display === "none") { + element.style.display = ""; + } + if (callback) callback(); + return; + } + + var startOpacity = + parseFloat(window.getComputedStyle(element).opacity) || 0; + var startTime = performance.now(); + + if (targetOpacity === 1 && element.style.display === "none") { + element.style.display = ""; + element.style.opacity = startOpacity; + } + + function animate(currentTime) { + var elapsed = currentTime - startTime; + var progress = Math.min(elapsed / duration, 1); + + element.style.opacity = + startOpacity + (targetOpacity - startOpacity) * progress; + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + if (callback) callback(); + } + } + + requestAnimationFrame(animate); + } + + // Helper function to animate width + animateWidth(element, targetWidth, duration, callback) { + var startWidth = element.offsetWidth; + var startTime = performance.now(); + + function animate(currentTime) { + var elapsed = currentTime - startTime; + var progress = Math.min(elapsed / duration, 1); + + element.style.width = + startWidth + (targetWidth - startWidth) * progress + "px"; + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + if (callback) callback(); + } + } + + requestAnimationFrame(animate); + } } /*================================= == Find the custom HTML tags and == == execute our code on them == =================================*/ -$(document).on("runestone:login-complete", function () { - $("[data-component=showeval]").each(function (index) { - // MC +document.addEventListener("runestone:login-complete", function () { + for (let element of document.querySelectorAll( + "[data-component=showeval]", + )) { var opts = { - orig: this, + orig: element, useRunestoneServices: eBookConfig.useRunestoneServices, }; - opts.raw = window.raw_steps[this.id]; - if ($(this).closest("[data-component=timedAssessment]").length == 0) { + opts.raw = window.raw_steps[element.id]; + if (!element.closest("[data-component=timedAssessment]")) { // If this element exists within a timed component, don't render it here - window.componentMap[this.id] = new ShowEval(opts); + window.componentMap[element.id] = new ShowEval(opts); } - }); + } }); if (typeof window.component_factory === "undefined") { diff --git a/bases/rsptx/interactives/runestone/showeval/test/showeval.test.js b/bases/rsptx/interactives/runestone/showeval/test/showeval.test.js new file mode 100644 index 000000000..379dabca7 --- /dev/null +++ b/bases/rsptx/interactives/runestone/showeval/test/showeval.test.js @@ -0,0 +1,189 @@ +// Characterization tests for the ShowEval component. Written first against +// the jQuery implementation (showEval.js 0.9.1); they now guard the version +// built on Al Sweigart's jQuery-free 0.10.0 core. +// Note: deliberately NO jquery-globals import here -- showeval must work +// without jQuery. +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { ShowEval } from "../js/showEval.js"; +import RunestoneBase from "../../common/js/runestonebase.js"; + +// The showeval directive emits the component div with its buttons and the +// step strings in window.raw_steps keyed by the div id (see README.md). +const STEPS = [ + "print({{2 + 2}}{{4}}) ## Addition happens first", + "print({{4}}{{4}})", +]; + +function makeFixture({ + id = "test_se_1", + steps = STEPS, + tracemode = "false", +} = {}) { + document.body.innerHTML = ` +
+
+ + +
+
+
`; + window.raw_steps = { [id]: steps }; + return document.getElementById(id); +} + +function makeShowEval(fixtureOpts = {}) { + const orig = makeFixture(fixtureOpts); + return window.component_factory.showeval({ + orig, + raw: window.raw_steps[orig.id], + useRunestoneServices: false, + }); +} + +// text of the current-step display parts +function shownParts(se) { + const div = document.querySelector(`#${se.divid} .currentStepDiv`); + return { + pre: div.querySelector(".pre").innerHTML, + eval: div.querySelector(".eval").innerHTML, + post: div.querySelector(".post").innerHTML, + }; +} + +let logSpy; +beforeEach(() => { + document.body.innerHTML = ""; + window.componentMap = {}; + window.allComponents = []; + localStorage.clear(); + vi.restoreAllMocks(); + logSpy = vi + .spyOn(RunestoneBase.prototype, "logBookEvent") + .mockResolvedValue(undefined); +}); + +describe("construction", () => { + it("renders the first step into pre/eval/post spans", () => { + const se = makeShowEval(); + expect(shownParts(se)).toEqual({ + pre: "print(", + eval: "2 + 2", + post: ") ", + }); + }); + + it("parses ## annotations and shows them for the current step", () => { + const se = makeShowEval(); + expect(se.steps[0][4]).toBe(" Addition happens first"); + expect(se.steps[1][4]).toBe(false); + const anno = document.querySelector(`#${se.divid} .anno`); + expect(anno.innerHTML).toBe(" Addition happens first"); + expect(anno.style.display).not.toBe("none"); + }); + + it("strips authoring backslashes from the raw steps", () => { + const se = makeShowEval({ + id: "test_se_esc", + steps: ["val \\= {{1 \\+ 1}}{{2}}"], + }); + expect(se.steps[0][0]).toBe("val = "); + expect(se.steps[0][1]).toBe("1 + 1"); + }); + + it("adds the runestone caption", () => { + const se = makeShowEval(); + expect( + se.containerDiv.querySelector(".runestone_caption").textContent, + ).toBe("ShowEval"); + }); +}); + +describe("stepping", () => { + it("animates the evaluation and advances to the next step", async () => { + const se = makeShowEval(); + const next = document.getElementById("test_se_1_nextStep"); + next.click(); + expect(next.disabled).toBe(true); + await vi.waitFor( + () => { + expect(next.disabled).toBe(false); + }, + { timeout: 8000, interval: 100 }, + ); + // the display has moved on to step 1 + expect(shownParts(se)).toEqual({ + pre: "print(", + eval: "4", + post: ")", + }); + expect(se.currentStep).toBe(1); + expect(logSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event: "showeval", + act: "next", + div_id: "test_se_1", + }), + ); + }, 15000); + + it("does nothing past the final step", () => { + const se = makeShowEval(); + se.currentStep = se.steps.length; + const next = document.getElementById("test_se_1_nextStep"); + next.click(); + expect(next.disabled).toBe(false); + expect(se.currentStep).toBe(se.steps.length); + }); + + it("keeps a visible trace of previous steps in trace mode", () => { + const se = makeShowEval({ id: "test_se_trace", tracemode: "true" }); + expect(se.createTrace).toBe(true); + document.getElementById("test_se_trace_nextStep").click(); + const prev = document.querySelector("#test_se_trace .previousStep"); + expect(prev).not.toBe(null); + expect(prev.nextElementSibling.classList.contains("currentStepDiv")).toBe( + true, + ); + }); +}); + +describe("reset", () => { + it("returns to step 0, clears the trace, and logs it", () => { + const se = makeShowEval({ id: "test_se_r", tracemode: "true" }); + document.getElementById("test_se_r_nextStep").click(); + expect( + document.querySelectorAll("#test_se_r .previousStep").length, + ).toBeGreaterThan(0); + document.getElementById("test_se_r_reset").click(); + expect( + document.querySelectorAll("#test_se_r .previousStep").length, + ).toBe(0); + expect(se.currentStep).toBe(0); + expect(shownParts(se).eval).toBe("2 + 2"); + expect(logSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event: "showeval", + act: "reset", + div_id: "test_se_r", + }), + ); + }); +}); + +describe("page wiring", () => { + it("builds components at login-complete, skipping timed exam questions", async () => { + makeFixture({ id: "se_free" }); + document.body.insertAdjacentHTML( + "beforeend", + `
+
+
`, + ); + window.raw_steps = { se_free: STEPS, se_timed: STEPS }; + document.dispatchEvent(new CustomEvent("runestone:login-complete")); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(window.componentMap.se_free).toBeInstanceOf(ShowEval); + expect(window.componentMap.se_timed).toBeUndefined(); + }); +}); diff --git a/bases/rsptx/interactives/runestone/tabbedStuff/js/tabbedstuff.js b/bases/rsptx/interactives/runestone/tabbedStuff/js/tabbedstuff.js index 8da6db44a..8b3ead0d7 100644 --- a/bases/rsptx/interactives/runestone/tabbedStuff/js/tabbedstuff.js +++ b/bases/rsptx/interactives/runestone/tabbedStuff/js/tabbedstuff.js @@ -18,16 +18,13 @@ import RunestoneBase from "../../common/js/runestonebase"; import "../css/tabbedstuff.css"; // Define TabbedStuff object -class TabbedStuff extends RunestoneBase { +export class TabbedStuff extends RunestoneBase { constructor(opts) { super(opts); var orig = opts.orig; this.origElem = orig; // entire original
element that will be replaced by new HTML this.divid = orig.id; - this.inactive = false; - if ($(this.origElem).is("[data-inactive]")) { - this.inactive = true; - } + this.inactive = orig.hasAttribute("data-inactive"); this.togglesList = []; // For use in Codemirror/Disqus this.childTabs = []; this.populateChildTabs(); @@ -42,14 +39,18 @@ class TabbedStuff extends RunestoneBase { ===========================================*/ populateChildTabs() { for (var i = 0; i < this.origElem.childNodes.length; i++) { - if ($(this.origElem.childNodes[i]).data("component") === "tab") { - this.childTabs.push(this.origElem.childNodes[i]); + const node = this.origElem.childNodes[i]; + if ( + node.nodeType === Node.ELEMENT_NODE && + node.dataset.component === "tab" + ) { + this.childTabs.push(node); } } } findActiveTab() { for (var i = 0; i < this.childTabs.length; i++) { - if ($(this.childTabs[i]).is("[data-active]")) { + if (this.childTabs[i].hasAttribute("data-active")) { this.activeTab = i; } } @@ -60,74 +61,86 @@ class TabbedStuff extends RunestoneBase { createTabContainer() { this.containerDiv = document.createElement("div"); this.containerDiv.id = this.divid; - $(this.containerDiv).addClass(this.origElem.getAttribute("class")); - $(this.containerDiv).attr({ role: "tabpanel" }); + const origClass = this.origElem.getAttribute("class"); + if (origClass) { + this.containerDiv.classList.add( + ...origClass.split(" ").filter(Boolean), + ); + } + this.containerDiv.setAttribute("role", "tabpanel"); this.tabsUL = document.createElement("ul"); this.tabsUL.id = this.divid + "_tab"; - $(this.tabsUL).addClass("nav nav-tabs"); - $(this.tabsUL).attr({ role: "tablist" }); + this.tabsUL.classList.add("nav", "nav-tabs"); + this.tabsUL.setAttribute("role", "tablist"); this.tabContentDiv = document.createElement("div"); // Create tab content container that holds tab panes w/content - $(this.tabContentDiv).addClass("tab-content"); + this.tabContentDiv.classList.add("tab-content"); this.createTabs(); // create and append tabs to the